说实话做智能家居项目最烦的不是硬件也不是界面而是中间那段“数据链路”。传感器读数只能在自己电脑上串口调试换到手机或者另一台机器就全瞎了更别说人不在家时远程看一眼温度、远程开个开关。这次我们做的是完整打通的项目STM32 采集温湿度通过 ESP8266 以 MQTT 协议上报华为云 IoT 平台Qt 写的跨平台桌面控制台再通过平台 API 拉取设备影子和下发控制指令。整个过程覆盖从硬件到云再到客户端的完整链路适合有一定嵌入式基础、想了解物联网产品化流程的朋友参考。这篇会把每一步拆开讲包括平台配置、报文构造、API 调用还有我联调时踩过的几个坑。1. 先理清链路为什么是 STM32 采集、华为云中转、Qt 控制台1.1 这套系统到底解决什么问题如果只是做一个“能跑的 demo”其实最简单的方式是 STM32 直接接一个 OLED 屏把温湿度打上去完事。但那样的问题是数据只能留在设备本地人在客厅都看不到更别说远程。真正做智能家居核心需求是三个远程查看状态、远程下发控制、历史数据留存。我这次设定的场景就是模拟一个“温室/房间环境监控风扇控制”的小系统。STM32 通过 DHT22 采集温度和湿度通过继电器控制一个直流小风扇或者灯珠模拟开窗、开风扇这类动作。ESP8266 负责联网把数据以 MQTT 报文发到华为云 IoT 平台。Qt 写的桌面应用运行在笔记本上虽然叫“桌面控制台”但因为 Qt 跨平台Windows 和 Linux 都能编译运行所以拿到哪儿都能用这就是“跨平台”的含义。1.2 三层架构的职责边界这套系统的链路非常清晰一共分三层层级承担角色核心职责设备端STM32 ESP8266传感器数据采集、继电器控制、MQTT 报文收发云平台华为云 IoT 平台设备鉴权接入、产品模型解析、设备影子存储、规则引擎流转客户端Qt 桌面应用读取设备影子、展示实时状态、下发控制命令STM32 只负责“感知和控制”不关心数据怎么存储、界面怎么展示华为云只负责“接入和转发”不关心设备端是 STM32 还是 ArduinoQt 只负责“展示和操作”不关心底层是 Wi-Fi 还是以太网。这样分层的好处是以后把 DHT22 换成其他传感器或者把 Qt 换成手机 App其他两层基本不用动。1.3 备选方案对比为什么不用其他方式我见过很多方案最容易想到的是局域网直连ESP8266 开一个 TCP ServerQt 直接连内网 IP。优点是代码少、不需要云平台缺点也很致命出了家门就完全没法访问而且路由器 IP 一变就得重新配置。再一个是自建 MQTT Broker比如在云服务器上装 EMQX可控性强但需要自己有公网服务器还要配证书、维护服务、处理设备鉴权工作量一下子大很多。华为云 IoT 这类平台的价值在于设备接入这一层已经帮你做好了。它有产品模型来定义数据类型有设备影子来缓存最新状态有规则引擎做数据流转和告警还有现成的 REST API 给客户端调用。你要写的东西比我自建一套 Broker 少得多而且平台侧的设备管理、在线状态、消息跟踪这些都是自建方案很难快速复制的。对于智能家居这种“设备数量不算特别多、但要求稳定和易用”的场景云平台方案是性价比最高的选择。2. STM32 端从传感器数据到 MQTT 报文上云2.1 硬件清单与接线硬件清单其实很常规都是比较容易买到的东西模块型号/规格说明主控STM32F103C8T6 开发板蓝色 pill 板性价比高温湿度传感器DHT22单总线协议精度比 DHT11 高不少Wi-Fi 模块ESP8266-01S串口转 Wi-FiAT 指令控制执行器继电器模块 风扇/LED用 STM32 GPIO 高低电平控制电源5V USB 供电 3.3V LDO注意 ESP8266 和 STM32 供电分开接线方面有一个特别容易翻车的地方串口交叉。STM32 的 USART2_TX 是 PA2要接 ESP8266 的 RXUSART2_RX 是 PA3要接 ESP8266 的 TX不能接反。DHT22 数据脚接 PA6继电器控制脚接 PA7这两个脚最好避开默认的调试串口避免上电时打印信息干扰。2.2 用 ESP8266 打通 TCP 通道AT 指令配置ESP8266 在这套系统里只干一件事建立 TCP 连接然后把数据原样透传出去。MQTT 报文不是它构造的是 STM32 通过串口把已经组好的二进制报文丢给它它负责发到华为云的 MQTT 端口。之所以这样做是因为市面上的 ESP8266 AT 固件版本参差不齐有些固件内置了 MQTT AT 指令有些没有直接用 TCP 透传最通用也方便排查问题。配置步骤分三个阶段每个阶段都要耐心等待 AT 回显阶段AT 指令预期返回基础测试ATOK设为 Station 模式ATCWMODE1OK连接 Wi-FiATCWJAPSSID,PASSWORDWIFI CONNECTED然后OK建立 TCP 连接ATCIPSTARTTCP,iot-mqtts.cn-north-4.myhuaweicloud.com,1883CONNECT OK开启透传ATCIPMODE1OK开始透传ATCIPSEND有一个经验值得分享如果ATCWJAP一直返回FAIL先别急着怀疑密码检查一下模块的供电电流。ESP8266 在 Wi-Fi 连接瞬间电流峰值能到 300mA 以上如果直接从 STM32 的 3.3V 引脚取电非常容易掉电重启。我的做法是给 ESP8266 单独用一颗 AMS1117-3.3 从 5V 稳压供电和 STM32 的 3.3V 域分开共地就行。2.3 手写 MQTT 报文CONNECT 和 PUBLISH 的关键结构进入透传模式之后STM32 要向服务器发送的第一包数据就是 MQTT CONNECT 报文。MQTT 3.1.1 的报文结构分成固定头、可变头、载荷三部分CONNECT 的固定头字节是0x10后面紧跟剩余长度这个长度是“可变头 载荷”的总字节数不算固定头那两个字节本身。我用的报文构造思路大致是这样的C 语言伪代码uint8_t buf[256]; uint8_t p 0; // 固定头 buf[p] 0x10; // CONNECT uint8_t len_pos p; // 剩余长度先占位最后回填 // 可变头协议名 MQTT协议级别 43.1.1 buf[p] 0x00; buf[p] 0x04; buf[p] M; buf[p] Q; buf[p] T; buf[p] T; buf[p] 0x04; buf[p] 0xC2; // 连接标志Username1Password1CleanSession1 buf[p] 0x00; buf[p] 0x3C; // Keep Alive 60 秒 // 载荷依次写入 clientId、username、password每段前面加两字节长度 // clientId 格式类似{device_id}_0_0_{timestamp} // username 就是 device_id // password 是平台用 HMAC-SHA256 生成的密钥 // 最后回填剩余长度 buf[len_pos] p - 2;这里最容易出问题的是密码生成。华为云 IoT 的 MQTT 接入不是简单的“用户名密码”直连密码是用设备注册时的密钥对当前时间戳做 HMAC-SHA256再 Base64 编码后的结果。时间戳一变密码就变。所以不能把密码写死必须由程序每次连接前用当前时间戳动态生成然后拼进 CONNECT 报文里。连接成功后上报属性用 PUBLISH 报文QoS 选 0 就够了// 固定头 0x30 表示 PUBLISH QoS0 // Topic: $oc/devices/{device_id}/sys/properties/report // Payload: JSON 字符串对应的 JSON 长这样{ services: [ { service_id: sensor, properties: { temperature: 26.5, humidity: 60.3 } } ] }这个 JSON 的结构不是随便写的它必须和平台上定义的产品模型一一对应后面我会专门讲平台侧怎么配。2.4 属性上报之后怎么保持设备一直在线设备不是发一条数据就完事了。华为云 IoT 会在 CONNECT 报文里的 Keep Alive 时间过后检查设备有没有心跳。我这边 Keep Alive 设的是 60 秒所以程序里每隔 30 秒就主动发一次PINGREQ报文或者顺便再发一次属性上报。如果长时间不发任何报文平台会判定设备离线后面 Qt 那边查在线状态就会看到 device offline。心跳这块我在代码里是这么处理的主循环里维护一个last_publish_time变量每次 DHT22 采集完成并上报成功后更新。如果距离上次上报超过 30 秒就直接发一包PINGREQ固定头0xC0剩余长度0x00。这样既保证了设备不掉线也避免了频繁上报浪费流量。3. 华为云 IoT 平台产品模型、设备注册与规则引擎配置3.1 创建产品和定义模型时容易忽略的字段在平台上创建产品时最关键的几个选择是行业类型选“智能家居”协议选“MQTT”数据格式选“JSON”。有些选项创建后不能改比如协议类型所以一开始就要想清楚不然只能删掉重建。产品和模型定义是整个云端配置的核心因为平台会拿它作为“翻译字典”设备上报的 JSON 要跟模型属性匹配客户端下发的命令也要跟模型里的命令定义匹配任何一个对不上数据就进不到设备影子。我定义的服务和属性如下服务属性/命令类型取值范围/说明sensortemperaturedecimal-40.0 ~ 80.0sensorhumiditydecimal0.0 ~ 100.0controlset_switch 命令int0 或 10 关 1 开这里有一个很多人踩过的坑属性类型定义成 int但设备上报的 JSON 里带了小数点比如温度 26.5。平台解析的时候会直接丢弃这条上报甚至报数据格式错误。所以温度湿度这种要定义成 decimal开关这种用 int两者不能混。3.2 设备注册与 MQTT 连接参数生成逻辑产品创建完需要在产品下注册一个设备。设备标识符可以自己取名比如stm32_sensor_01注册的时候平台会给一个设备密钥。之后在设备详情页面能看到“设备接入地址”以华北-北京四为例MQTT 接入地址格式类似iot-mqtts.cn-north-4.myhuaweicloud.com端口 1883明文或 8883TLS。连接参数里三个最关键的东西是clientId规则是{device_id}_0_0_{timestamp}最后的 timestamp 必须和生成密码时用的时间戳一致。username就是设备 ID 本身。password用设备密钥对 timestamp 做 HMAC-SHA256再 Base64 编码。这个算法平台控制台里有个“连接参数生成工具”可以帮你看一眼格式但正式设备上不能依赖网页工具必须在代码里实现。STM32 那边资源有限所以我的做法是先用 Python 脚本或者平台工具生成一次连接参数验证整条链路通不通确认无误后再把密钥和时间戳生成逻辑固化到硬件端。如果你用 ESP32 这类资源更充足的芯片也可以直接跑现成的 MQTT 库让库来生成报文。3.3 规则引擎联动让数据既进影子也跑流转默认情况下设备上报属性后平台会更新设备影子。Qt 通过设备影子 API 就能拿到最新值。但光有影子还不够智能家居场景里经常需要“温度超过阈值自动告警”这个就靠规则引擎实现。我在平台里建了一条规则当上报的数据里temperature 30时触发一个动作把事件转发到对象存储服务同时给应用侧推送一条告警消息。规则引擎的配置逻辑简单说就是“数据源 条件 动作”。数据源选设备属性上报条件写 JSON 路径表达式动作选择转发目标。这里我建议一开始先做两个动作高低温告警和历史数据落库。历史数据落库非常有用后面做趋势分析、月报统计都靠它。4. Qt 桌面控制台用 API 拉取设备影子并下发指令4.1 为什么 Qt 端不走 MQTT 直连很多人的第一反应是Qt 端也写一个 MQTT 客户端订阅设备的 Topic 不就行了实际做下来会发现这条路走不通。华为云 IoT 平台的 Topic 权限是绑定到设备维度的设备 A 不能订阅设备 B 的 Topic这是平台的安全隔离策略。Qt 端如果以“设备身份”接入它只能收发自己那套$oc/devices/xxx/sys/...主题收不到 STM32 设备的数据。所以 Qt 端的正确定位是“应用侧”也就是通过平台提供的 REST API 来读写数据。最直接的方式就是调用设备影子的查询接口获取 STM32 最新上报的温湿度和开关状态。实时性上影子数据本身就是设备上报后实时更新的Qt 这边用定时器每 2 秒轮询一次完全够用而且代码简单得多不用维护一条长连接。4.2 获取 Token 与设备影子两个关键请求调用华为云 IoT 的 API 之前需要先通过 IAM 服务获取临时 Token。Token 就是访问凭证有效期一般比较长但后台运行超过有效期后会失效所以代码里要在收到401时重新获取。获取 Token 的请求用 Qt 的QNetworkAccessManager就能搞定QNetworkRequest tokenReq(QUrl(https://iam.myhuaweicloud.com/v3/auth/tokens)); tokenReq.setHeader(QNetworkRequest::ContentTypeHeader, application/json); QJsonObject passwordObj; QJsonObject userObj; userObj.insert(name, your_username); userObj.insert(password, your_password); QJsonObject domainObj; domainObj.insert(name, your_account_name); userObj.insert(domain, domainObj); passwordObj.insert(user, userObj); QJsonObject identityObj; identityObj.insert(methods, QJsonArray{password}); identityObj.insert(password, passwordObj); QJsonObject scopeObj; QJsonObject projectObj; projectObj.insert(name, cn-north-4); scopeObj.insert(project, projectObj); QJsonObject authObj; authObj.insert(identity, identityObj); authObj.insert(scope, scopeObj); QJsonObject bodyObj; bodyObj.insert(auth, authObj);注意Token 不在响应体的 JSON 里而是在 HTTP 响应头X-Subject-Token字段里直接用reply-rawHeader(X-Subject-Token)取。拿到 Token 之后查询设备影子就是一个很干净的 GET 请求QNetworkRequest shadowReq(QUrl( https://iotda.cn-north-4.myhuaweicloud.com/v5/iot/ projectId /devices/ deviceId /shadow )); shadowReq.setRawHeader(Authorization, (Bearer token).toUtf8()); QNetworkReply *reply manager.get(shadowReq);返回的 JSON 结构里重点看services数组下reported字段{ services: [ { service_id: sensor, reported: { temperature: 26.5, humidity: 60.3 } } ] }Qt 端解析的时候用QJsonDocument::fromJson一层层取出来刷新界面上的 QLabel 就行了。4.3 下发控制命令从按钮到设备动作的完整路径Qt 端控制继电器不是直接给 STM32 发网络包而是调用平台的下发命令 API。平台收到后会根据产品模型里的命令定义把命令推送到 STM32 订阅的命令主题上STM32 收到命令后解析并执行 GPIO 控制。下发命令的接口是 POSTQNetworkRequest cmdReq(QUrl( https://iotda.cn-north-4.myhuaweicloud.com/v5/iot/ projectId /devices/ deviceId /commands )); cmdReq.setHeader(QNetworkRequest::ContentTypeHeader, application/json); cmdReq.setRawHeader(Authorization, (Bearer token).toUtf8()); QJsonObject paras; paras.insert(status, 1); QJsonObject body; body.insert(service_id, control); body.insert(command_name, set_switch); body.insert(paras, paras); QNetworkReply *reply manager.post(cmdReq, QJsonDocument(body).toJson());STM32 那边订阅的是$oc/devices/{device_id}/sys/commands/#这个主题收到平台下发的命令后解析出command_name是set_switch再看paras.status是 0 还是 1然后控制 PD7 引脚电平翻转。这里还要注意命令的应答闭环STM32 执行完命令后应当向$oc/devices/{device_id}/sys/commands/response/request_id{请求ID}回复一条响应。这样 Qt 端才能通过平台查到“命令已执行成功”的结果而不是只知道“命令已下发”。4.4 轮询刷新与线程化别把界面卡死Qt 端用QTimer每 2 秒触发一次影子刷新请求这个操作虽然是异步的但要注意不能把网络请求放在主线程里同步等待否则界面会卡住。QNetworkAccessManager本身就是异步的所以只需要在请求完成后通过信号槽更新界面不会阻塞 UI。我在实际写的时候发现一个体验问题如果用户快速点击“开”“关”按钮会导致命令请求堆积设备那边可能还来不及执行界面状态已经跳了好几次。后来我在按钮点击处理里加了一个标志位上一次命令的响应还没回来时忽略新的点击等响应回来再恢复。这样虽然看起来“反应慢了一点”但逻辑上不会乱对于智能家居控制这种场景稳定性比响应速度更重要。5. 联调阶段最常踩的坑从设备离线到 API 401 的排查链路5.1 设备一直离线先怀疑这四个地方我在联调时遇到的第一大坑就是设备注册后一直离线。排查顺序很重要别一上来就怀疑硬件。我建议按下面这个顺序查排查项具体检查点clientId 格式是不是{device_id}_0_0_{timestamp}密码时效时间戳是不是最新的密码有没有过期接入地址域名和端口是不是对应区域AT 透传状态ESP8266 是否真的进入透传模式其中最容易忽略的是 clientId 里的时间戳和密码的时间戳必须一致。我一开始犯过这个错密码是用网页工具生成的clientId 里的时间戳是程序运行时的毫秒时间两个时间差了若干秒平台至少会因为密码校验失败而拒绝连接。后来我把时间戳固定成一个变量先拼完 clientId 再用同一个时间戳生成密码问题就消失了。还有一个排查工具特别好用MQTTX 这类桌面 MQTT 客户端。先用 MQTTX 填好同样的 clientId、username、密码连华为云如果 MQTTX 也连不上问题基本就在连接参数上和硬件代码无关排除起来非常快。等 MQTTX 能连上了再回头查 STM32 的报文构造。5.2 报文能发出但平台收不到数据JSON 和设备模型不匹配设备已经显示在线但在平台“设备详情”里看不到任何属性数据。这种情况八成是上报的 JSON 和产品模型不完全一致。比如我一开始把产品模型里温度属性定义为int但上报 JSON 里温度是26.5平台解析失败整条消息被丢掉。排查方法是打开平台控制台的“消息跟踪”功能能看到设备每条消息的处理状态。消息跟踪里如果提示parsing failed或者service not found那就是模型和 JSON 对不上。还有一种情况service_id写错了比如模型里服务叫sensor上报写成Sensor大小写不一样也会失败。这种问题只看代码很难发现因为硬件那边发送很成功TCP 也发出去了但云平台根本不认这个服务。5.3 Qt 端拿不到最新数据影子轮询的刷新时机设备上报成功后影子数据理论上立即更新但 Qt 端如果刷新的时机不对会看到数据一直不变。我之前遇到的情况是Qt 界面已经显示了第一次拿到的值但后续设备上报的新值一直没出现。后来发现问题不是影子没更新而是 QTimer 的槽函数里网络请求的回调把旧数据覆盖了。因为我用了同一个 QNetworkReply 指针如果上一次请求还没完成下一次 QTimer 触发时把 reply 覆盖掉上一次的响应信号就永远等不到了。这个问题的解法很简单每次请求前disconnect旧连接或者每次创建独立的 QNetworkReply在 lambda 里通过捕获当前 reply 来判断是不是自己这个响应。5.4 下发命令不见反应命令格式、在线状态与产品模型三层排查命令下发了但继电器纹丝不动这个现象要把排查分成三层。第一层看命令请求本身用类似 Postman 的工具直接调接口如果返回400或404检查service_id、command_name是不是模型里定义的原名。第二层看设备在线状态华为云对离线设备下发命令默认会失败并返回设备离线。第三层看 STM32 端有没有订阅对主题如果订阅的是$oc/devices/{device_id}/sys/commands/#但收到的命令里 request_id 参数解析错误也会导致应答发不出去。这里我要特别强调命令名称的大小写。华为云模型定义里的命令名是按你填写的原样保存的如果模型里是set_switch但命令请求体里写成setSwitch平台会返回命令不存在。这个坑很好避免控制台定义模型时给命令起一个代码里完全一致的名字别搞驼峰和下划线混用这套。5.5 长时间运行设备掉线心跳、重连与看门狗联调刚通过时一切正常但跑了几个小时后设备突然离线这基本就是心跳和重连机制没做好。STM32 端只有一个摄像头式的主循环如果主循环里因为某个阻塞操作卡住几秒心跳就可能错过。我的处理方式是在主循环里加了一个超时看门狗每次循环检查距离上次心跳发送的时间如果超过 40 秒还没发出去就强制重新初始化 ESP8266 连接重新走一遍 TCP MQTT CONNECT 流程。这样做虽然粗暴但非常有效尤其是在家里 Wi-Fi 不稳定、路由器偶尔重启的情况下设备能自己恢复不用人跑过去按复位键。另外ESP8266 的 AT 指令回显处理也要注意透传模式下退出透传需要发但前要保证串口空闲一段时间否则会被当作普通数据发出去。我在代码里发前先延时 300ms然后等待OK再执行ATCIPCLOSE关掉旧连接最后重新拨号连接。这套重连流程实测下来比较可靠。最后再说几句整套系统调通之后我个人的体会是硬件端手写 MQTT 报文反而是整个项目里最顺利的部分真正消耗时间的是云平台配置和 API 鉴权这些“看起来简单、文档却要仔细看”的环节。如果你也想复刻这个项目建议你先把 QMQTTX 验证连接参数这一步做完再烧 STM32 固件可以帮你省掉一半的联调时间。另外这个项目后续扩展空间很大。硬件端可以再加一路 PM2.5 传感器云端规则引擎已经开始在落历史数据Qt 客户端下一步可以做温度趋势曲线配合 local 的数据库缓存断网时也可以通过内网直连控制设备。智能家居没有所谓“标准答案”把这条链路跑通之后剩下的就是往里面加你真正需要的设备和逻辑了。
阅读完成 · 觉得有帮助?