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是更稳妥的选择。
具体型号我选的是F103C8T6,64KB 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, "AT+MQTTSUB=0,\"%s\",1\r\n", sub_topic); HAL_UART_Transmit(&huart2, (uint8_t*)cmd, strlen(cmd), 1000);上面这段代码是ESP8266订阅主题的AT指令,AT+MQTTSUB的第一个参数是连接ID(0表示第一个连接),第二个参数是主题字符串,第三个参数是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,这个版本稳定、资料多,而且对C++11的支持很好。Qt 6虽然更新,但一些第三方库还没完全适配,新手容易踩坑。编译器在Windows上用MinGW,Linux上用GCC,macOS上用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没启动或者串口接线错了;如果是AT+CWJAP(连WiFi)失败,说明WiFi密码错了或者信号太弱;如果是AT+MQTTCONN失败,说明三元组填错了或者平台地址不对。
第二步检查三元组:把产品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用AT+GSLP指令进入深度睡眠,需要发数据时用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的跨平台,这三个组合在一起,对于个人开发者或者小团队来说,是性价比很高的方案。如果你也想做一套自己的智能家居控制系统,建议先从终端采集和云平台接入做起,跑通数据链路之后再加控制端,最后做优化和扩展。每一步都跑通了再往下走,比一上来就全做要稳妥得多。