前阵子一直在倒腾一套嵌入式物联网小系统把 STM32、华为云IoT 和 Qt 串成一条完整的链路实现了一个跨平台的智能家居控制系统。做完之后最大的感触是单看每一端的资料都很多但能把设备端采集、云平台接入、桌面端展示和控制这一整条链路从零打通的完整案例其实很少。今天我就把整个过程完整拆开从架构选型、云平台配置、STM32 设备端代码逻辑、到 Qt 客户端的 REST 调用一步步讲清楚包括我踩过的坑和排查思路。这篇主要是给有 STM32 基础、想接触物联网云平台、或者做跨平台上位机控制的开发者看的内容偏实操照着做基本能跑通。1. 整体架构与方案选型先想清楚再动手1.1 三层架构的核心链路这个项目的本质是“端、云、应用”三层数据闭环。设备端用 STM32F103 做主控负责采集传感器数据、控制继电器和 LED 输出网络侧通过一个 ESP8266 WiFi 模块接入路由器走 MQTT 协议跟华为云IoT平台通信应用端用 Qt 开发一个桌面程序在电脑上实时查看温度、湿度、光照强度还能远程下发开关指令。数据流是这样的STM32 读 DHT11 温湿度和光敏电阻 ADC 值通过串口把 JSON 报文发给 ESP8266ESP8266 作为一个 WiFi 透传模块将数据发布到华为云IoT的设备属性上报主题云平台收到后更新设备影子Qt 客户端通过华为云IoT的北向 REST 接口查询影子数据拿到温湿度并显示在界面上。反过来Qt 下发开关命令时调用北向接口下发设备命令云平台通过 MQTT 命令主题推给 ESP8266STM32 解析后控制继电器动作。1.2 为什么选这套组合而不选其他方案先说设备端为什么用 STM32 而不是 Arduino。Arduino 开发确实快但 STM32 在工业场景里更常见ADC、串口、定时器资源也更丰富后面想加更多的传感器、做低功耗、接 RTOS 都很方便。而且学习价值不一样STM32 的 HAL 库工程结构和实际产品开发更接近。云端为什么选华为云IoT而不是自建 MQTT 服务自建 Mosquitto 看似自由但设备接入认证、Topic 权限管理、设备影子、命令下发这些都要自己写。华为云IoT把这些都封装好了产品模型定义完之后设备侧只要按规定 Topic 上送 JSON平台自动解析北向接口直接拿到设备影子。该花的钱是调试时间不该花的钱是服务器运维时间。应用端选 Qt 的理由很简单跨平台。同样的代码Windows、Linux、macOS 都能编译跑起来。用 Web 页面其实也可以但 Qt 桌面程序在数据刷新、图表绘制、后台任务上的体验更接近原生应用而且 Qt 的网络库封装得很好处理 REST API 很方便。这里也顺便说一句如果只是本地调试用 Qt 直接连 MQTT 也能做但一旦云端在线设备只有一个连接名额Qt 和设备端会互相顶掉线所以后面真正采用的是北向 REST 方案。1.3 核心链路中的协议选型说明MQTT 选 QoS 1 比较合适。QoS 0 可能丢包QoS 2 在 ESP8266 这种资源紧张的模块上开销偏大还容易因为握手频繁出问题。上报频率控制在 2 到 5 秒一次既不会把平台限流也能让界面曲线比较平滑。华为云IoT平台 MQTT 接入地址一般是iot-mqtts.cn-north-4.myhuaweicloud.com端口 1883 是明文 MQTT8883 是 TLS 加密端口。演示项目用 1883 足够生产环境务必换 8883。平台接入的核心参数有三个设备IDdevice_id、设备密钥secret和项目IDproject_id。设备 ID 是平台生成的全局唯一标识密钥是设备侧的鉴权凭证。北向 REST 接口的基地址一般是https://iotda.cn-north-4.myhuaweicloud.com注意这个区域必须和创建设备时选的一致否则会报设备不存在。鉴权用 IAM Token 方式获取 Token 后放在 HTTP 请求头的X-Auth-Token字段里。2. 华为云IoT平台侧准备设备接入前的关键配置2.1 创建产品与定义模型登录华为云控制台进入 IoT 设备接入服务先创建一个产品。协议类型选 MQTT数据格式选 JSON。创建完产品后平台会生成一个产品ID后面设备配置会用到。重点在产品模型定义。按照智能家居场景我定义了一个服务服务ID定为SmartHome里面添加以下属性属性名数据类型说明temperatureint温度单位 0.1℃humidityint湿度单位 0.1%lightint光照强度 ADC 值无量纲switchint继电器开关状态0 或 1属性名、服务ID这些东西一旦定义好后面设备上报的 JSON key 必须严格一致大小写都不能错。产品模型对于平台来说就是“翻译字典”设备上报{temperature:255}平台才知道 255 对应温度 25.5℃而不是当垃圾数据丢掉。命令也需要在产品模型里定义。添加一条命令命令名为switch参数名跟属性一样叫switch类型 int。命令定义完平台才知道应用侧下发{switch:1}时要推给哪个设备、走哪个 Topic。2.2 注册设备与密钥鉴权逻辑产品创建好之后在产品详情里“设备注册”页面添加设备。这里有两个 ID 容易混淆一个是“设备标识码”nodeId这是你自己填的设备唯一标识比如smart_home_01另一个是注册成功后平台生成的“设备ID”device_id格式通常是产品ID_设备标识码用于 MQTT 连接和北向接口调用。注册设备时还需要设置密钥我用自定义方式填了一串字符串。注意这个密钥不是直接作为 MQTT 密码而是参与 HMAC-SHA256 计算。华为云IoT的要求是MQTT 连接的 username 填设备IDpassword 填以密钥作为 key、设备ID作为 message 计算出来的 HMAC-SHA256 十六进制字符串。这一步卡住了很多人我自己第一次也在这里翻车。先在电脑上用 Python 算出哈希值用于调试是最快的确认方式import hmac import hashlib device_id 你平台生成的设备ID secret 你注册设备时填的密钥 password hmac.new(secret.encode(), device_id.encode(), hashlib.sha256).hexdigest() print(password)这个 password 是一串 64 位十六进制字符串后面 STM32 端连接时要用。如果设备端返回密码错误先用这个脚本跟设备端算出的一致不一致。2.3 Topic 规划与 JSON 报文格式华为云IoT设备侧的 Topic 是固定的不能自己随便造。核心用到这几个属性上报$oc/devices/{device_id}/sys/properties/report命令下发平台到设备$oc/devices/{device_id}/sys/commands/#命令响应设备到平台$oc/devices/{device_id}/sys/commands/response/request_id{request_id}消息下发$oc/devices/{device_id}/sys/messages/down属性上报的负载格式是{ services: [ { service_id: SmartHome, properties: { temperature: 255, humidity: 600, light: 320, switch: 0 } } ] }注意 temperature 我按 0.1℃ 上报255 代表 25.5℃这是为了避免小数点。产品模型里定义 int就别上报浮点平台解析会出问题。设备影子查询接口返回的数据结构则是{ device_id: xxx, shadow: [ { service_id: SmartHome, reported: { properties: { temperature: 255, humidity: 600, switch: 0 } } } ] }影子保存的是设备“最近一次”上报状态。即使设备离线应用端也能通过影子拿到最后一次数据这就是为什么 Qt 端选影子查询而不是直接等设备实时上报。3. STM32 端设备开发数据采集与 MQTT 接入3.1 硬件清单与接线硬件部分我用的是这些STM32F103C8T6 核心板ESP8266-01S WiFi 模块DHT11 温湿度传感器光敏电阻 10K 上拉电阻继电器模块控制一个小灯泡或风扇5V/3.3V 双路供电面包板或杜邦线接线表如下STM32引脚外设引脚说明PA0光敏电阻 ADC模拟输入采集光照PA1DHT11 DATA单总线数据PA2ESP8266 RX串口发送到模块PA3ESP8266 TX串口接收模块数据PB0继电器 IN开关控制GND所有 GND共地处理需要特别注意的是供电。ESP8266 射频发射瞬间电流能到 300mA 以上如果直接挂在 STM32 核心板的 3.3V LDO 上电压一跌就复位或者 AT 指令没响应。我最初就被这个问题折磨了很久后来单独给 ESP8266 供电实测稳定很多。3.2 STM32 端的初始化框架工程基于 STM32CubeMX 生成 HAL 库代码开启以下资源RCC 时钟外部晶振 8MHz倍频到 72MHzUSART1 或 USART2 与 ESP8266 通信波特率 115200ADC1 通道 PA0采样光照GPIO 输出 PB0 控制继电器一个定时器做 2ms 时基用于 DHT11 时序和任务调度DHT11 读取要特别注意时序它在初始化后需要主机拉低总线至少 18ms 再释放然后读取 40bit 数据。我用 GPIO 模拟时序读取完必须加上拉输入模式切换否则电平不稳容易读出 CRC 错误。调这块时建议先用逻辑分析仪看信号没有逻辑分析仪就靠串口打印原始比特值判断。3.3 ESP8266 的 AT 指令连接流程ESP8266 的固件必须支持 MQTT AT 指令。我用的是安信可 AT 2.2.0 及以上的固件里面才有ATMQTTUSERCFG、ATMQTTCONN这些指令。如果手头模块是旧固件先刷固件再继续。STM32 通过串口发送 AT 指令的完整顺序如下ATCWMODE1 ATCWJAPyour_wifi_ssid,your_wifi_password ATMQTTUSERCFG0,1,device_id,计算出的哈希密码,0,0, ATMQTTCONN0,iot-mqtts.cn-north-4.myhuaweicloud.com,1883,1 ATMQTTSUB0,$oc/devices/{device_id}/sys/commands/#,1 ATMQTTPUB0,$oc/devices/{device_id}/sys/properties/report,{\services\:[{\service_id\:\SmartHome\,\properties\:{\temperature\:255,\humidity\:600,\light\:320,\switch\:0}}]},1,0每条 AT 指令后面要跟\r\nESP8266 返回OK再进行下一步。这里有两个细节第一个WiFi 连接密码里如果有特殊字符AT 指令里不需要转义但 JSON 里面双引号必须转义否则 STM32 拼串时直接乱掉。第二个MQTT 连接指令里的第 2 个参数是 socket 连接 ID加上最后那个参数是 TLS 模式。0 表示不加密如果要用 8883 端口需要刷支持 TLS 的 AT 固件并且提前烧录证书调试期不建议先碰。设备端通常在ATMQTTCONN返回OK之后先主动上报一次当前状态再进入主循环定时上报。3.4 STM32 上报代码的核心逻辑上报数据前STM32 要把传感器数值拼成 JSON 字符串。我直接用 sprintf 拼注意浮点数一律转成整数上报。示例关键代码uint8_t buf[256]; sprintf((char *)buf, {\services\:[{\service_id\:\SmartHome\, \properties\:{\temperature\:%d,\humidity\:%d, \light\:%d,\switch\:%d}}]}, dht11_temp, dht11_hum, light_adc, relay_state); ESP8266_SendATCommand(ATMQTTPUB0,\$oc/devices/%s/sys/properties/report\,\%s\,1,0, device_id, buf);这里device_id在程序启动时从平台生成。另一个容易踩的坑是AT 指令拼出来的字符串里如果 JSON 过长超过 ESP8266 的 AT 缓冲区模块会直接返回 ERROR。解决办法是把长 JSON 拆分或者减少上报的属性个数。实测 256 字节以内的报文都没问题。3.5 接收云端命令并控制继电器设备端要收到 Qt 下发的命令首先要订阅命令 Topic。在初始化时执行ATMQTTSUB0,$oc/devices/{device_id}/sys/commands/#,1订阅之后平台下发命令时 ESP8266 会主动通过串口推送一串带MQTTSUBRECV前缀的数据。STM32 在串口接收中断里解析这段数据找到命令名和参数值然后控制继电器。解析逻辑不要写太复杂用字符串查找就行因为命令负载格式通常是{command_name:switch,service_id:SmartHome,paras:{switch:1}}在 STM32 上调用strstr找到switch:1就置位 PB0找到switch:0就清零。比引入 cJSON 库更省资源。等整个系统跑通之后如果命令种类多了再考虑移植 cJSON。下发命令后设备端还需要回响应。响应 Topic 带了一个 request_id是命令下发时平台生成的动态参数STM32 需要从接收到的报文里提取 request_id然后往响应 Topic 发一条空 JSON。不做这个也不影响控制但云平台上命令状态会一直显示“等待响应”不好排查问题。4. Qt 跨平台客户端实现影子查询与命令下发4.1 Qt 客户端的功能规划Qt 客户端我按照“配置页 监控页 控制区”的布局来做。配置页需要用户填几个参数区域基地址、项目ID、设备ID、IAM 用户名、IAM 密码。监控页放两个 QLabel 显示温度和湿度、一个 QChart 画温湿度曲线、一个开关按钮控制继电器。这个客户端本质上就是一个 REST API 的调用端与设备端通过华为云平台间接通信不直接连设备。这样设计的好处是 Qt 程序跑在哪个系统上都一样只要能访问 HTTPS 就行而且不会占用设备端的 MQTT 连接名额。4.2 为什么不建议 Qt 直接用 MQTT 连接很多第一次做这个项目的同学会想当然地认为 Qt 装个 QMqttClient 库用同一套设备ID去连华为云IoT然后订阅属性上报主题就能实时收到设备数据。这个方案看起来简单但实际会遇到一个严重问题同一个设备ID同时只能维持一个 MQTT 连接Qt 连上之后设备端的 ESP8266 会被平台强制下线然后 STM32 这边疯狂重连再把 Qt 顶掉两边反复踢皮球。所以在正式架构里我建议 Qt 走北向接口。设备端保持唯一 MQTT 连接Qt 通过 HTTPS 查询设备影子、下发命令平台作为中间人完成消息转发。界面上的数据刷新用定时器每隔 3 秒拉一次影子即可视觉效果上已经足够“实时”。4.3 Qt 获取 IAM Token 的实现调用华为云北向接口之前先要拿到一个 Token。用 Qt 的 QNetworkAccessManager 发 POST 请求到 IAM 接口QNetworkAccessManager *manager new QNetworkAccessManager(this); QNetworkRequest request; request.setUrl(QUrl(https://iam.cn-north-4.myhuaweicloud.com/v3/auth/tokens)); request.setHeader(QNetworkRequest::ContentTypeHeader, application/json); QJsonObject user; user[name] iamUsername; user[password] iamPassword; QJsonObject password; password[user] user; QJsonObject identity; identity[methods] QJsonArray() password; identity[password] password; QJsonObject auth; auth[identity] identity; QJsonObject root; root[auth] auth; QNetworkReply *reply manager-post(request, QJsonDocument(root).toJson());响应体里带一个 X-Subject-Token 的响应头那就是 Token有效期默认 24 小时。打开 Qt 程序后第一次请求时获取过期后重新获取。这个处理方法对演示项目已经够用生产项目建议用 AK/SK 签名方式不需要在客户端保存密码。4.4 查询设备影子并更新界面获取 Token 之后查询影子就是一个普通 GET 请求QString url QString(https://iotda.cn-north-4.myhuaweicloud.com/v5/iot/%1/devices/%2/shadow) .arg(projectId, deviceId); QNetworkRequest request; request.setUrl(QUrl(url)); request.setRawHeader(X-Auth-Token, token.toUtf8()); QNetworkReply *reply manager-get(request); connect(reply, QNetworkReply::finished, this, []() { QByteArray resp reply-readAll(); QJsonDocument doc QJsonDocument::fromJson(resp); QJsonObject obj doc.object(); QJsonArray shadowArr obj[shadow].toArray(); QJsonObject reported shadowArr[0].toObject()[reported].toObject(); QJsonObject props reported[properties].toObject(); double temperature props[temperature].toDouble() / 10.0; double humidity props[humidity].toDouble() / 10.0; int light props[light].toInt(); ui-tempLabel-setText(QString::number(temperature, f, 1) ℃); ui-humLabel-setText(QString::number(humidity, f, 1) %); // 这里再追加到 QChart 序列 reply-deleteLater(); });注意响应里的 temperature 设备端上报的是 255所以这里要除以 10 再显示。如果没有做过这个数据转换界面上会出现一个“255℃”的鬼数据反过来还被当成设备采集错误。4.5 下发开关命令点击开关按钮时构造 POST 请求下发命令QString url QString(https://iotda.cn-north-4.myhuaweicloud.com/v5/iot/%1/devices/%2/commands) .arg(projectId, deviceId); QJsonObject paras; paras[switch] ui-switchBtn-isChecked() ? 1 : 0; QJsonObject cmd; cmd[command_name] switch; cmd[service_id] SmartHome; cmd[paras] paras; QNetworkReply *reply manager-post(request, QJsonDocument(cmd).toJson());这个接口调用成功只是表示“平台已受理命令”并不代表设备已经执行。设备端是否真正动作要看影子里的 switch 状态是否变化。所以我在界面上加了个“同步状态”的逻辑每次查询影子时把 switch 属性的值同步回开关按钮这样界面上能看到设备真实状态。命令下发还有一个容易踩的坑产品模型里定义命令时命令参数的命名必须和属性参数完全一致。如果产品模型里命令参数叫status代码里却下发switch平台会报参数校验错误。5. 常见问题与排查技巧实录三端联调时踩过的坑故障现象可能原因排查方法ESP8266 上电后无响应供电不足或串口波特率不对单独供电确认 AT 固件默认波特率通常 115200ATMQTTCONN 返回 ERROR设备ID/密码错误、平台区域不一致先用 Python 算哈希检查接入地址是否匹配区域设备在线但平台上无数据上报 Topic 错误、JSON 格式不对用平台“设备调试”看原始消息核对负载格式命令下发显示超时设备端没订阅命令 Topic检查是否执行 ATMQTTSUBtopic 路径是否完整Qt 请求返回 401Token 缺失或过期检查 X-Auth-Token 是否设置重新获取 TokenQt HTTPS 请求闪退缺少 OpenSSL 动态库Windows 部署时需要带上 libcrypto 和 libssl 的 dll设备一直重启ESP8266 供电电压不稳示波器量电压改用独立稳压供电逐个展开说几个最重要的。第一个是 MQTT 密码问题。华为云IoT接入时的密码不是设备密钥本身而是HMAC-SHA256(secret, device_id)的十六进制值。如果直接填密钥平台返回错误很隐晦有时只是显示 TCP 连接被断开。最好先在 PC 上用 Python 脚本算出哈希再填到设备端。第二个是订阅命令 Topic 的坑。有些教程让设备订阅$oc/devices/{device_id}/sys/commands这是错的。实际平台推送命令是带request_id{xxx}的完整 Topic所以需要订阅通配路径$oc/devices/{device_id}/sys/commands/#否则命令到不了设备端。第三个是影子为空的问题。设备刚接入还没上报过数据时影子是空的Qt 查询会拿到空数组。设备端务必在连接成功后先主动上报一次完整属性让影子有数据。如果还是空检查上报 JSON 的服务ID是否跟产品模型完全一致大小写都要对上。第四个是 Qt 部署在 Windows 上时的 TLS 问题。Qt 默认用 OpenSSLWindows 上没有系统级 OpenSSL运行时会报“TLS initialization failed”。把libcrypto-3-x64.dll和libssl-3-x64.dll放到 exe 同目录下就解决了。Linux 和 macOS 一般装好 OpenSSL 即可。第五个是区域不一致问题。在控制台创建 IoTDA 实例时选择了“华北-北京四”那设备接入地址和 API 基地址都必须是cn-north-4。如果项目ID和基地址对应的是另一个区域调用 API 会报“设备不存在”但设备明明就在控制台里。这类问题特别容易让人绕圈子建议把控制台域名、设备接入域名、API 域名三者统一核对一遍。6. 从调试到完整系统三端联调的经验顺序最后从我个人的实操体会出发给还没开始动手的同学一个联调顺序建议。不要一开始就把 STM32、ESP8266、华为云IoT、Qt 四块全部串起来那样一旦出错你根本分不清是哪一层的锅。正确的做法是分三段验证。第一段先验证平台通。在华为云控制台的“设备调试”里用平台自带的模拟器模拟设备上报数据确认平台能正常接收。然后在 API Explorer 里调用影子查询和命令下发确认北向接口也能通。这一段走通了说明平台侧配置完全没问题。第二段验证硬件通。把 STM32 和 ESP8266 单独拿出来用串口助手直接给 ESP8266 发 AT 指令手动完成 WiFi 连接、MQTT 连接、上报数据。看着平台设备状态从“离线”变“在线”并且有数据上报再写 STM32 的代码逻辑。第三段验证应用通。先把 Qt 端写成最简单的控制台程序通过命令行参数传入 Token、设备ID跑通一次影子查询和命令下发再去设计界面布局。这样能把 UI 问题跟 API 调用问题分开排查。我自己的体会是真正把这三端接起来之后整个项目的价值才开始体现。原来看似不相关的嵌入式、云平台、桌面端开发其实共享着同一套消息语义设备上报的 JSON 是什么结构云端的产品模型就怎么定义Qt 显示端就怎么解析三处对齐了整条链路就通了。后续想加语音控制、手机 App、Web 端展示本质上都是再加一个“端”只是接入方式不同而已。这套系统再往后延伸的话可以考虑在 Qt 端引入 AK/SK 签名来替代 IAM 密码登录或者利用华为云IoT的规则引擎做联动告警比如温度超过阈值自动推送通知。设备端也可以继续增加湿度控制、风扇调速、窗帘控制这些更丰富的执行器。物联网的乐趣就在这里底层的通信逻辑打通一次上层功能就是不断堆叠的事了。
阅读完成 · 觉得有帮助?