
1. 从一块开发板到全屋联动这套系统到底在解决什么问题很多人第一次接触智能家居都是从买一个现成的智能音箱或者智能插座开始的。用起来确实方便但用久了就会发现一个问题不同品牌的设备各自为政A品牌的灯只能用A品牌的App控制B品牌的传感器数据又锁在B品牌的云里想做个人走灯灭、温度高了自动开风扇的联动要么买同一生态的贵价设备要么忍受各种不兼容。这套基于STM32、云IoT平台和Qt的跨平台控制系统本质上就是给你一个自己说了算的方案——从底层的传感器采集、网络通信到云端的设备管理、数据转发再到桌面端的可视化控制整条链路全部打通你想怎么改就怎么改。我最初做这个项目是因为家里老人总是忘记关厨房的排气扇想装个能远程看状态、能定时关的东西。市面上的成品要么功能太单一要么数据要经过第三方服务器心里总觉得不踏实。后来干脆自己动手用STM32做终端节点采集温湿度和烟雾浓度通过云IoT平台做设备接入和消息转发再用Qt写一个跑在电脑上的控制面板。整套东西做下来成本不到两百块但灵活度远超成品。这套系统适合谁看如果你有基本的C语言基础玩过STM32的GPIO和串口对网络通信有一点概念那这篇内容就是为你准备的。如果你完全没接触过嵌入式也没关系我会把每个环节的为什么讲清楚你照着做也能跑通。核心关键词就三个STM32终端采集、云IoT平台接入、Qt跨平台控制。这三个词分别对应系统的感知层、网络层和应用层缺一不可。先说清楚这套系统能做什么。终端节点负责采集环境数据温度、湿度、烟雾浓度等通过串口或者直接联网的方式把数据发出去云IoT平台负责设备注册、数据接收、命令下发相当于一个中转站Qt控制端则负责把数据可视化展示出来同时提供按钮、滑块等控件让你远程控制终端上的继电器比如开关灯、开关风扇。整个链路是双向的终端可以上报数据控制端也可以下发指令。跨平台的意思是Qt写的控制端可以跑在Windows、Linux甚至macOS上不用为每个系统单独写一套界面。为什么选这三个技术栈而不是别的STM32的优势在于生态成熟、资料多、成本低一块最小系统板十几块钱就能搞定而且HAL库把底层寄存器操作封装得很好新手也能快速上手。云IoT平台的优势在于免去了自己搭服务器的麻烦设备接入、鉴权、消息路由这些脏活累活平台都帮你做了你只需要关注业务逻辑。Qt的优势在于一套代码多平台编译而且信号槽机制做界面和逻辑的解耦非常舒服比MFC或者WinForm写起来清爽得多。这三个凑在一起刚好覆盖了物联网端-云-控的完整闭环。2. 终端节点的硬件选型与传感器数据采集逻辑2.1 为什么选STM32F103而不是更便宜的51单片机市面上做物联网终端可选的MCU很多从几块钱的51单片机到几十块的ESP32都有。我最终选STM32F103C8T6这颗国民芯片理由有三个。第一是外设资源够用它有两个USART、两个SPI、两个I2C、多个定时器和ADC通道接温湿度传感器、烟雾传感器、继电器模块绰绰有余后期想加个OLED屏幕显示本地数据也不用换芯片。第二是开发工具链成熟STM32CubeMX可以图形化配置引脚和时钟自动生成初始化代码省去了手动查手册配寄存器的痛苦。第三是社区资料丰富遇到问题搜一下基本都能找到答案不像某些冷门芯片出了问题只能自己啃数据手册。51单片机虽然更便宜但它的RAM和Flash太小跑不动稍微复杂一点的协议栈而且没有硬件I2C和SPI驱动传感器只能软件模拟时序稳定性差很多。ESP32虽然自带WiFi但它的开发环境和STM32差异较大而且价格比F103贵不少。如果你的项目只需要采集一两个数据点51确实够用但要做一套可扩展的系统F103是更稳妥的选择。具体型号我选的是F103C8T664KB Flash、20KB RAM对于这个项目来说绰绰有余。如果你手头有F103ZET6或者其他型号也没问题代码基本不用改只需要在CubeMX里重新配置一下引脚就行。2.2 DHT11和MQ-2的驱动细节与常见坑温湿度传感器我用的是DHT11虽然精度一般温度±2℃湿度±5%但对于家居环境监测完全够用而且价格便宜、单总线协议简单。烟雾浓度检测用的是MQ-2它对液化气、丙烷、氢气、烟雾都有响应模拟输出接STM32的ADC引脚就能读出浓度值。DHT11的驱动有几个坑必须提前说。第一是时序要求严格主机拉低总线至少18毫秒然后拉高20-40微秒再释放总线等待传感器响应。这个延时不能用HAL_Delay因为HAL_Delay是毫秒级的精度不够得用定时器或者DWT做微秒级延时。第二是数据格式DHT11一次发送40位数据前16位是湿度整数和小数中间16位是温度整数和小数最后8位是校验和。注意DHT11的小数部分一直是0所以实际精度就是整数。第三是读取间隔两次读取之间至少间隔1秒否则传感器可能不响应。MQ-2的坑主要在预热和标定。它内部有一个加热丝刚上电时读数会剧烈波动需要预热至少24小时才能稳定。我第一次做的时候没预热就直接读数值从几百跳到几千完全没法用。标定的话建议在洁净空气中读一个基准值然后在有烟雾的环境下读一个峰值用这两个值做归一化。另外MQ-2的输出是模拟电压STM32的ADC是12位的参考电压3.3V所以实际电压 ADC值 × 3.3 / 4096。但MQ-2的输出电压范围可能超过3.3V所以最好加一个分压电路否则可能烧坏ADC引脚。// DHT11微秒级延时函数基于DWT void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t cycles us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) cycles); } // DHT11读取一个字节 uint8_t DHT11_ReadByte(void) { uint8_t i, byte 0; for (i 0; i 8; i) { while (!HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)); // 等待50us低电平结束 DWT_Delay_us(40); byte 1; if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)) { byte | 1; } while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)); // 等待高电平结束 } return byte; }上面这段代码是DHT11读取的核心逻辑注意DWT_Delay_us需要先使能DWT时钟否则CYCCNT不计数。使能代码是CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0;放在main函数开头就行。2.3 继电器控制与终端状态上报的节奏设计继电器模块我用的是常见的5V单路继电器控制引脚接STM32的GPIO输出端接负载灯或者风扇。这里要注意电气隔离继电器线圈和触点之间是物理隔离的但控制信号和STM32之间最好加一个光耦防止继电器吸合瞬间的反向电动势干扰MCU。我一开始没加光耦结果继电器一吸合STM32就复位后来加了PC817光耦才稳定。终端上报数据的节奏也很关键。如果每秒上报一次云平台的消息量会很大而且终端功耗高如果几分钟上报一次控制端的实时性又太差。我的做法是正常状态下每5秒上报一次检测到异常比如烟雾浓度超过阈值时立即上报并提高上报频率到每秒一次。这样既保证了日常监测的连续性又能在紧急情况下快速响应。上报的数据格式用JSON包含设备ID、温度、湿度、烟雾浓度、继电器状态、时间戳这几个字段。// 上报数据JSON示例 { device_id: stm32_node_01, temp: 26, humi: 58, smoke: 320, relay: 0, timestamp: 1712345678 }设备ID在云平台注册时生成每个设备唯一。时间戳可以用STM32的RTC生成也可以由云平台在接收时打上。我建议由云平台打时间戳因为STM32的RTC如果没有纽扣电池断电后时间就丢了每次上电都要重新校准比较麻烦。3. 云IoT平台接入设备注册、鉴权与消息路由的完整链路3.1 设备注册时最容易搞混的三元组云IoT平台接入的第一步是创建设备平台会给你一组设备三元组产品ID、设备名称、设备密钥。这三个东西相当于设备的身份证终端在连接平台时需要用它们做鉴权。产品ID代表一类设备比如智能家居终端是一个产品下面可以挂很多个设备设备名称是单个设备的唯一标识比如node_01、node_02设备密钥是设备连接时的密码用来生成连接凭证。很多人第一次做的时候会把产品ID和设备名称搞混或者在代码里填错了密钥导致连接一直失败。我的建议是在平台创建设备后立刻把三元组复制到一个文本文件里保存好因为设备密钥在平台上只显示一次关掉页面就看不到了只能重置。重置后原来的密钥就失效了所有用旧密钥的设备都要重新配置。终端连接平台的方式有两种一种是直接联网比如用ESP8266或者4G模块终端自己跑MQTT协议连平台另一种是通过网关转发终端用串口把数据发给网关网关再连平台。我选的是第一种因为ESP8266模块便宜十几块钱AT指令也简单STM32通过串口发AT指令就能控制它联网。如果你手头没有ESP8266也可以用SIM800C走GPRS但那个成本高一些而且需要插SIM卡。3.2 MQTT主题设计与消息上下行的约定云IoT平台通常支持MQTT协议MQTT的核心概念是主题Topic设备往某个主题发消息平台往某个主题推消息。主题的设计直接决定了消息路由的逻辑设计不好后期扩展会很痛苦。我的主题设计是这样的方向主题格式说明上行设备到云$sys/{product_id}/{device_name}/thing/event/property/post设备上报属性数据下行云到设备$sys/{product_id}/{device_name}/thing/service/property/set平台下发属性设置命令上行设备到云$sys/{product_id}/{device_name}/thing/event/alert/post设备上报告警事件上行主题里的property/post表示上报属性平台收到后会更新设备影子Device Shadow也就是设备在云端的虚拟状态。下行主题里的property/set表示平台要设置设备的属性比如控制端点了开灯按钮平台就会往这个主题推一条消息终端订阅这个主题后就能收到指令。这里有个细节要注意终端需要订阅下行主题才能收到控制指令。很多新手只发了上行数据忘了订阅下行主题结果控制端点了按钮没反应。订阅的代码在MQTT连接成功后执行主题字符串要和平台约定的一致否则收不到消息。// MQTT订阅下行主题AT指令方式 // 假设ESP8266已经连接平台 char sub_topic[128]; sprintf(sub_topic, $sys/%s/%s/thing/service/property/set, product_id, device_name); char cmd[256]; sprintf(cmd, ATMQTTSUB0,\%s\,1\r\n, sub_topic); HAL_UART_Transmit(huart2, (uint8_t*)cmd, strlen(cmd), 1000);上面这段代码是ESP8266订阅主题的AT指令ATMQTTSUB的第一个参数是连接ID0表示第一个连接第二个参数是主题字符串第三个参数是QoS等级1表示至少送达一次。QoS等级建议用1虽然会稍微增加一点网络开销但能保证消息不丢。3.3 数据上云后的存储与转发策略数据到了云平台之后平台一般会提供数据存储和规则引擎两个功能。数据存储就是把设备上报的历史数据存下来方便以后查询和分析规则引擎则是根据数据内容触发一些动作比如当温度大于30度时自动给控制端推一条告警消息。规则引擎的配置逻辑是条件-动作模式。条件可以是一个或多个数据字段的判断比如temp 30 AND humi 60动作可以是发消息到某个主题、调用某个API、或者写入数据库。我配了一条规则当烟雾浓度大于500时平台自动往控制端的主题推一条告警消息控制端收到后弹窗提示并播放声音。这样即使控制端没有主动查询也能第一时间知道异常。数据存储方面平台一般提供按时间范围查询的API控制端可以通过HTTP请求拉取历史数据。但要注意平台的存储有上限免费版通常只保留最近7天或30天的数据超过期限的会被清理。如果需要长期存储得自己把数据导出到本地数据库。我的做法是控制端每天定时拉一次前一天的数据存到本地的SQLite里这样既不受平台存储限制查询也更快。4. Qt控制端跨平台界面设计与实时数据绑定的实现4.1 为什么用Qt而不是Web或者手机App控制端的选择其实很多可以用Web做浏览器打开就能用可以用手机App做随时随地都能看也可以用Qt做桌面应用。我选Qt的理由有三个。第一是跨平台同一套代码在Windows上编译出exe在Linux上编译出可执行文件在macOS上编译出app不用为每个系统重写界面。第二是信号槽机制Qt的信号槽让界面和逻辑解耦得很干净比如网络模块收到数据后发一个信号界面上的标签自动更新不需要手动刷新。第三是开发效率高Qt Designer可以拖拽控件布局QSS可以像写CSS一样美化界面比MFC那种手动计算坐标的方式舒服太多。Web方案虽然跨平台性更好但需要额外搭一个Web服务器而且浏览器的实时通信WebSocket在稳定性上不如Qt的原生网络模块。手机App方案虽然便携但开发成本高Android和iOS要分别写而且调试起来麻烦。Qt桌面应用虽然只能在电脑上跑但对于家居控制这种场景电脑通常是一直开着的或者用一台旧笔记本专门做控制终端完全够用。Qt的版本我选的是Qt 5.15 LTS这个版本稳定、资料多而且对C11的支持很好。Qt 6虽然更新但一些第三方库还没完全适配新手容易踩坑。编译器在Windows上用MinGWLinux上用GCCmacOS上用Clang都是Qt自带或者系统自带的不需要额外配置。4.2 MQTT客户端在Qt中的集成与断线重连Qt本身没有内置MQTT客户端需要额外集成。我用的方案是QMQTT这个第三方库它基于Qt的网络模块实现API风格和Qt一致用起来很顺手。集成步骤是从代码仓库下载QMQTT源码用Qt Creator打开.pro文件编译然后把生成的库文件和头文件放到项目里在.pro里加上LIBS -L/path/to/qmqtt -lqmqtt和INCLUDEPATH /path/to/qmqtt/include。连接平台的代码大概长这样QMQTT::Client *client new QMQTT::Client(QHostAddress(iot.example.com), 1883); client-setClientId(device_name); client-setUsername(product_id); client-setPassword(device_secret); connect(client, QMQTT::Client::connected, this, MainWindow::onConnected); connect(client, QMQTT::Client::received, this, MainWindow::onMessageReceived); client-connectToHost();注意setUsername和setPassword填的是产品ID和设备密钥不是平台的登录账号。很多新手在这里填错了导致连接被拒绝。另外端口号一般是1883非加密或8883加密如果平台要求加密连接还需要配置SSL证书。断线重连是必须处理的。网络波动、平台维护、路由器重启都可能导致连接断开如果不做重连控制端就变成死的了。我的做法是监听disconnected信号在槽函数里启动一个QTimer每隔5秒尝试重连一次直到连上为止。重连成功后要重新订阅下行主题因为MQTT的连接断开后订阅关系也会丢失。void MainWindow::onDisconnected() { qDebug() MQTT disconnected, retrying...; QTimer::singleShot(5000, this, [this]() { mqttClient-connectToHost(); }); }4.3 实时曲线与设备状态的界面刷新策略控制端的界面主要分三块实时数据展示区、历史曲线区、控制操作区。实时数据展示区用QLabel显示当前的温度、湿度、烟雾浓度和继电器状态历史曲线区用QCustomPlot绘制温度和时间的关系曲线控制操作区用QPushButton和QSlider做开关和调节。界面刷新的策略很关键。如果每收到一条MQTT消息就刷新一次界面当数据上报频率很高时比如每秒一次界面会频繁重绘导致卡顿。我的做法是用QTimer做节流MQTT消息到达后先把数据存到一个缓冲区然后每500毫秒从缓冲区取最新数据刷新一次界面。这样既保证了界面的实时性最多延迟500毫秒又避免了频繁重绘。// 节流刷新 void MainWindow::onMessageReceived(const QMQTT::Message message) { QJsonDocument doc QJsonDocument::fromJson(message.payload()); QJsonObject obj doc.object(); m_tempBuffer obj[temp].toInt(); m_humiBuffer obj[humi].toInt(); m_smokeBuffer obj[smoke].toInt(); // 不直接刷新界面等定时器触发 } void MainWindow::onRefreshTimer() { ui-tempLabel-setText(QString::number(m_tempBuffer) °C); ui-humiLabel-setText(QString::number(m_humiBuffer) %); ui-smokeLabel-setText(QString::number(m_smokeBuffer)); // 更新曲线 m_plot-graph(0)-addData(QDateTime::currentSecsSinceEpoch(), m_tempBuffer); m_plot-replot(); }历史曲线的数据点不能无限追加否则内存会爆。我的做法是只保留最近1小时的数据超过1小时的点自动移除。QCustomPlot的removeDataBefore方法可以按时间戳移除旧数据调用一次就能清理掉过期的点。4.4 控制指令下发与终端响应的确认机制控制端下发指令的流程是用户点击按钮 - 控制端往平台的下行主题发一条消息 - 平台转发给终端 - 终端执行动作 - 终端上报新状态 - 控制端更新界面。这个链路里最容易出问题的是终端没有响应可能是网络延迟也可能是终端死机了。如果不做确认机制用户点了按钮但灯没亮会以为系统坏了。我的做法是加一个超时确认控制端发完指令后启动一个3秒的定时器如果3秒内没有收到终端上报的新状态就弹窗提示设备未响应请检查网络或设备状态。如果收到了新状态就取消定时器并更新界面。这样用户能明确知道指令是否执行成功而不是干等着。void MainWindow::onRelayButtonClicked() { QJsonObject cmd; cmd[relay] m_relayState ? 0 : 1; QJsonDocument doc(cmd); QMQTT::Message msg(0, downTopic, doc.toJson()); mqttClient-publish(msg); // 启动超时定时器 QTimer::singleShot(3000, this, [this]() { if (!m_ackReceived) { QMessageBox::warning(this, 提示, 设备未响应请检查网络或设备状态); } }); m_ackReceived false; }终端那边收到指令后除了执行动作还要立即上报一次新状态这样控制端才能确认指令生效了。上报的内容里要包含继电器的当前状态控制端收到后把m_ackReceived置为true超时定时器就不会弹窗了。5. 联调阶段最容易翻车的几个环节与排查思路5.1 终端连不上平台从串口日志到网络抓包的排查链路终端连不上平台是最常见的问题表现是ESP8266一直返回ERROR或者连接超时。排查的时候不要瞎猜按链路一步步来。第一步看串口日志在STM32的代码里把ESP8266的AT指令响应打印到串口看它到底卡在哪一步。如果是AT没响应说明ESP8266没启动或者串口接线错了如果是ATCWJAP连WiFi失败说明WiFi密码错了或者信号太弱如果是ATMQTTCONN失败说明三元组填错了或者平台地址不对。第二步检查三元组把产品ID、设备名称、设备密钥复制到平台的设备详情页对比看是否完全一致。注意大小写和特殊字符有时候复制的时候多了一个空格就会导致鉴权失败。第三步检查网络用电脑ping一下平台的域名看能不能通如果电脑能通但ESP8266不通可能是ESP8266的固件版本太老不支持MQTT指令需要升级固件。如果以上都没问题那就抓包。在路由器上做端口镜像用Wireshark抓ESP8266和平台之间的通信包看MQTT的CONNECT报文里填的用户名和密码是什么和平台要求的是否一致。抓包是终极手段能解决99%的玄学问题。5.2 数据上报成功但控制端收不到主题订阅与QoS的坑有时候终端明明上报成功了串口日志显示PUBACK但控制端就是收不到消息。这种情况大概率是主题订阅错了。控制端订阅的主题必须和终端上报的主题完全一致包括大小写和斜杠。我遇到过好几次终端上报的主题是.../property/post控制端订阅的是.../property/Post就差一个大写字母消息就收不到。另一个可能是QoS等级不匹配。如果终端用QoS 0发消息平台可能不会转发给订阅了QoS 1的客户端。建议上下行都用QoS 1保证消息至少送达一次。还有**保留消息Retained Message**的坑如果终端发了保留消息平台会把它存下来新订阅的客户端会立刻收到这条消息。如果不想收到历史消息发消息的时候把retain标志设为false。排查的时候可以在平台的控制台看消息轨迹平台一般会记录每条消息的发送时间、主题、QoS和送达状态。如果轨迹显示消息已经转发但控制端没收到那就是控制端的订阅有问题如果轨迹显示消息根本没到平台那就是终端的网络有问题。5.3 界面卡顿与内存泄漏Qt定时器和信号槽的注意事项Qt控制端跑久了变卡一般是两个原因定时器太多或者信号槽连接重复。定时器太多是指每收到一条消息就创建一个QTimer用完没销毁时间长了内存里堆了几千个定时器。正确的做法是全局只用一个QTimer做刷新所有需要定时刷新的逻辑都挂到这个定时器上。信号槽连接重复是指每次重连MQTT都调用一次connect导致同一个信号连接了多个槽函数消息来了一次槽函数被调用多次。解决方法是在connect之前先disconnect或者用Qt::UniqueConnection标志保证同一个连接只建立一次。// 错误的做法每次重连都connect导致重复连接 connect(client, QMQTT::Client::received, this, MainWindow::onMessageReceived); // 正确的做法用UniqueConnection connect(client, QMQTT::Client::received, this, MainWindow::onMessageReceived, Qt::UniqueConnection);内存泄漏方面Qt的对象树机制会自动管理父子对象的生命周期但如果你用new创建了一个没有父对象的对象就必须手动delete。我建议尽量用栈对象或者智能指针避免手动管理内存。QCustomPlot的graph数据点也要定期清理否则内存会一直涨。5.4 继电器误动作与电源干扰的硬件层面处理继电器误动作是硬件层面最烦人的问题表现是继电器时不时自己吸合或断开或者STM32莫名其妙复位。原因通常是电源干扰继电器线圈是大电流感性负载吸合和断开瞬间会产生反向电动势通过电源线传导到MCU导致MCU工作异常。解决办法有三个。第一是加光耦隔离在STM32的GPIO和继电器控制引脚之间加一个PC817光耦把MCU和继电器在电气上隔开。第二是加续流二极管在继电器线圈两端反向并联一个1N4148二极管给反向电动势提供一个泄放回路。第三是电源分开供电STM32用一路LDO供电继电器用另一路电源供电两路电源共地但不共正极。我三个都做了之后继电器再也没误动作过。另外去耦电容也很重要。在STM32的每个电源引脚旁边放一个100nF的陶瓷电容在电源入口放一个10uF的钽电容能有效滤除高频噪声。这些电容不贵但能省去很多调试的麻烦。6. 从能跑到好用系统优化与功能扩展的几条实用路径6.1 降低终端功耗从常供电到电池供电的改造思路现在的终端是常供电的插着USB线一直跑。如果想改成电池供电比如用两节18650电池续航时间可能只有几天因为ESP8266和DHT11一直在工作。降低功耗的核心思路是让MCU和传感器间歇工作大部分时间STM32进入Stop模式RTC定时唤醒醒来后读一次传感器、发一次数据然后继续睡。ESP8266也可以在不发数据的时候进入睡眠模式需要发数据时再唤醒。具体做法是把上报间隔从5秒改成5分钟STM32在两次上报之间进入Stop模式功耗从几十毫安降到几微安。ESP8266用ATGSLP指令进入深度睡眠需要发数据时用GPIO唤醒。DHT11和MQ-2在不读的时候断电用MOS管控制它们的电源。这样改下来两节18650电池能撑几个月。但要注意Stop模式会关闭大部分外设唤醒后需要重新初始化串口和GPIO。RTC的闹钟中断可以唤醒STM32唤醒后先初始化外设再读传感器、发数据然后继续睡。这个改造稍微复杂一点但效果很明显。6.2 增加本地联动不依赖云端的自动化规则现在所有的联动都要经过云端如果网络断了控制端和终端就失联了。可以在终端上加一些本地联动规则比如烟雾浓度大于500时自动打开排气扇这个判断在STM32里做不需要经过云端。这样即使网络断了安全相关的联动依然有效。本地联动的实现很简单在STM32的主循环里加一个判断如果烟雾浓度超过阈值直接拉高继电器引脚同时把状态标记为本地触发。等网络恢复后再把本地触发的状态上报给平台让控制端知道发生了什么。这样既保证了安全性又不会丢失状态。6.3 控制端增加语音播报与告警弹窗控制端现在只有视觉提示如果用户不在电脑前可能看不到告警。可以加一个语音播报功能用Qt的QTextToSpeech模块把告警信息读出来。QTextToSpeech是Qt 5.15自带的支持Windows、Linux和macOS的语音引擎调用很简单QTextToSpeech *speech new QTextToSpeech(this); speech-say(警告烟雾浓度超标);告警弹窗可以用QMessageBox但QMessageBox是模态的会阻塞界面。更好的做法是用一个非模态的告警条在界面顶部滑出一个红色横幅显示告警内容几秒后自动消失。这样既不打断用户操作又能起到提示作用。6.4 数据持久化把历史数据存到本地SQLite平台的数据存储有期限而且查询速度受网络影响。可以在控制端加一个本地SQLite数据库每次收到数据就存一条这样历史数据永远都在本地查询也快。Qt自带SQLite驱动不需要额外安装用QSqlDatabase和QSqlQuery就能操作。QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); db.setDatabaseName(smarthome.db); db.open(); QSqlQuery query; query.exec(CREATE TABLE IF NOT EXISTS sensor_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, temp INTEGER, humi INTEGER, smoke INTEGER)); query.prepare(INSERT INTO sensor_data (timestamp, temp, humi, smoke) VALUES (?, ?, ?, ?)); query.addBindValue(QDateTime::currentSecsSinceEpoch()); query.addBindValue(m_tempBuffer); query.addBindValue(m_humiBuffer); query.addBindValue(m_smokeBuffer); query.exec();存到本地之后历史曲线就可以直接从数据库查不用再依赖平台的API。查询的时候按时间范围过滤比如查最近24小时的数据用WHERE timestamp ?就行。数据量大了之后可以加索引按timestamp建一个索引查询速度会快很多。这套系统从最开始的能跑就行到后来慢慢优化成稳定好用中间踩了不少坑但也积累了很多经验。STM32的稳定性和灵活性、云平台的免运维、Qt的跨平台这三个组合在一起对于个人开发者或者小团队来说是性价比很高的方案。如果你也想做一套自己的智能家居控制系统建议先从终端采集和云平台接入做起跑通数据链路之后再加控制端最后做优化和扩展。每一步都跑通了再往下走比一上来就全做要稳妥得多。