简介:这是一份基于阿里云物联网平台与MQTT协议的STM32嵌入式工程资源,面向希望快速掌握设备上云、温湿度采集、2路开关远程控制及2路数据并行传输的物联网开发者。资源包含完整工程源码,共198个文件,以C语言源文件(.c)、头文件(.h)为核心,并附有编译生成的hex、axf、map等固件文件以及Keil工程配置(uvprojx/uvoptx),可直接打开编译与烧录调试;工程基于标准外设库,涉及定时器、ADC、USART、I2C等常见外设的初始化与中断处理,代码结构清晰。压缩包仅5.38MB,整体紧凑,便于对照学习。已有2146人浏览学习,适合初学者或中级开发者参考。借助该资源,可直观理解阿里云IoT平台设备接入流程、Topic发布订阅机制、MQTT QoS选择及设备端数据上报与命令下发的代码实现;同时可借鉴其2路开关控制逻辑、多路数据并行上报以及外设驱动编写的工程写法,减少从零搭建的时间和踩坑成本。
1. 把“2路开关+2路数据”接到阿里云IoT,先想明白MQTT这三段式
“阿里云IoT物联网平台 MQTT 2路开关+2路数据”这句话听起来像一段采购需求,实际是一类非常典型的设备接入任务:一块控制板带着2个继电器和2个传感器,要通过MQTT协议把开关状态和实时数据送到物联网平台,同时接收平台下发的开/关指令。真做完这个项目你会发现,难点不在写代码,而在三件事:连接参数的签名规则、物模型属性标识符与JSON字段的对应关系、以及上下行Topic的完整语义。这三处任何一处对不上,设备要么连不上,要么连上了控制台看不到数据,要么开关指令发出去了设备毫无反应。这篇就直接照着一套可复现的做法,把阿里云IoT实例、MQTT协议和2路开关/2路数据的物模型细节一起说清楚。
2. 产品、设备、物模型:先把两路开关和两路数据定义在阿里云IoT里
2.1 两路开关和两路数据在物模型里分别是什么
在阿里云物联网平台里,一个产品下的设备共享同一套“物模型”。物模型有三种功能类型:属性(Property)、事件(Event)、服务(Service)。做2路开关和2路数据上报时,开关是属性,而且是“读写”属性,因为平台要下发状态给设备;两路数据是传感器采集值,只做上报,所以定义为“只读”属性。
常见的属性定义如下:
| 功能类型 | 属性标识符 | 名称 | 数据类型 | 读写类型 | 取值范围 |
|---|---|---|---|---|---|
| 属性 | switch1 | 第1路开关 | Bool | 读写 | 0/1 |
| 属性 | switch2 | 第2路开关 | Bool | 读写 | 0/1 |
| 属性 | temp | 温度数据 | Float | 只读 | -40~125 |
| 属性 | humi | 湿度数据 | Float | 只读 | 0~100 |
这里有几个容易犯错的点。属性标识符一旦确定,后续MQTT消息里的params的key必须和它完全一致,包括大小写。标识符只允许字母、数字、下划线,不能用中文。还有Bool类型在物模型里虽然叫Bool,但平台推荐的JSON表达是数字0和1,不是true/false,很多人在第一步就把这个写错了。
2.2 在阿里云IoT实例里创建产品和设备
登录物联网平台控制台,如果没有专门的企业版实例,默认用的是公共实例。标题里的“阿里云iot实例”如果指的是企业实例,接入域名和链接方式不同,但物模型定义方式完全一致。先创建产品,节点类型选“直连设备”,连网方式按实际选WiFi或以太网,数据格式必须选“Alink JSON”。
产品创建完成后,进入“功能定义”页面,把上面表格里的四个属性逐个添加进去。添加完不要忘记点“发布上线”,否则设备端上报的数据在控制台看不到。接着在“设备管理”里添加设备。一个产品下可以添加多个设备,每个设备会生成一组三元组:ProductKey、DeviceName、DeviceSecret。DeviceName相当于设备在平台内的唯一账号,DeviceSecret相当于密码,三者的作用在第3章签名时会用到。
最后把三元组保存好。生产环境建议存到设备端安全存储区域,不要再硬编码进源码里。后面几步的代码和Topic全部依赖这组三元组。
3. MQTT连接参数生成:阿里云IoT不是裸MQTT,clientId和password都有规则
3.1 连接参数与标准MQTT的差异点
阿里云物联网平台兼容MQTT 3.1.1,但它不是一个普通的公共MQTT Broker。连接时不能只填地址和端口,必须在clientId、username、password三个参数上体现设备身份与签名信息。直接看参数表:
| 参数 | 取值 | 说明 |
|---|---|---|
| Broker地址 | ${productKey}.iot-as-mqtt.${regionId}.aliyuncs.com | 华东2上海就是cn-shanghai |
| 端口 | 1883(TCP)/ 8883(TLS) | 生产建议8883 |
| clientId | `${deviceName} | securemode=3,signmethod=hmacsha256,timestamp=${毫秒时间戳}` |
| username | ${deviceName}&${productKey} | ampersand,顺序不要反 |
| password | HMAC-SHA256结果(hex) | 密钥是DeviceSecret |
签名内容的拼接规则是固定的:把clientId、deviceName、productKey按参数名字典序排列,然后拼接成clientId<值>deviceName<值>productKey<值>的字符串,用 DeviceSecret 作为HMAC密钥,取SHA256的十六进制字符串作为password。注意这里的clientId值是完整的、带管道符的那一段,不是单独的DeviceName。
3.2 用Python生成签名并接入
下面这段代码可以直接运行,生成一个符合阿里云IoT接入要求的MQTT连接参数:
import hmac import hashlib import time import paho.mqtt.client as mqtt product_key = "a1Xxxxxxx" device_name = "switch2ch01" device_secret = "6f2bxxxxxxxxxxxxxxxxxxxxxxxx" timestamp = str(int(time.time() * 1000)) client_id = f"{device_name}|securemode=3,signmethod=hmacsha256,timestamp={timestamp}" username = f"{device_name}&{product_key}" content = f"clientId{client_id}deviceName{device_name}productKey{product_key}" password = hmac.new( device_secret.encode(), content.encode(), hashlib.sha256 ).hexdigest() broker = f"{product_key}.iot-as-mqtt.cn-shanghai.aliyuncs.com" client = mqtt.Client( callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=client_id, protocol=mqtt.MQTTv311, ) client.username_pw_set(username, password) client.connect(broker, 1883, keepalive=60) client.loop_start()这段代码里最容易改错的是content的拼接顺序。clientId在前、deviceName其次、productKey最后,这个顺序是参数名的字典序,不是随意排列的。如果拼反了,MQTT能连上TCP层,但在应用层会被拒绝,并且日志里会提示签名错误。paho.mqtt.client的CallbackAPIVersion.VERSION2是paho-mqtt 2.x版本要求的写法,用1.x版本时可以去掉这个参数,否则会直接抛TypeError。
3.3 用MQTT客户端工具先验证连接参数
参数对不对,不要一上来就写嵌入式代码,先用PC上的MQTT工具验证。MQTTX和MQTT.fx这类mqtt客户端都能自定义clientId、username、password,把上面计算出的三个值填进去,Broker端口填1883,点击连接。能连上且不掉线,说明签名和网络都没问题。
如果连接失败,优先看错误提示。阿里云IoT的返回信息里常有sign error、device not found、timestamp expired这样的关键词。timestamp expired表示设备端时钟和真实时间偏差过大,需要做NTP对时;sign error多半是content拼错或DeviceSecret抄错。这一步在mqtt工具里排掉,再往板子上移植,会省掉大量交叉调试时间。
4. 两路数据上报和两路开关下发:Topic与Alink JSON格式
4.1 两路数据上报的Topic和payload
数据上报使用固定Topic,格式是:
/sys/{productKey}/{deviceName}/thing/event/property/postpayload必须是Alink JSON格式,基本结构如下:
{ "id": "1700000001", "version": "1.0", "method": "thing.event.property.post", "params": { "switch1": 1, "switch2": 0, "temp": 23.6, "humi": 58.2 } }id是一条消息的序号,由设备端生成,建议用自增数或时间戳,平台不会修改它,但排查问题时会用这个字段关联请求与响应。method固定是thing.event.property.post,一个字母都不能错。params里的四个key对应物模型里的四个属性标识符,顺序无所谓,但key必须严格匹配。
Python代码发布一组数据:
import json payload = { "id": str(int(time.time() * 1000)), "version": "1.0", "method": "thing.event.property.post", "params": { "switch1": 1, "switch2": 0, "temp": 23.6, "humi": 58.2, }, } topic = f"/sys/{product_key}/{device_name}/thing/event/property/post" client.publish(topic, json.dumps(payload), qos=1)发布后可以到平台控制台的“设备详情-物模型数据”里查看最新值。如果数据没刷新,先看是否发布了物模型,再看params里的key和属性标识符是否一致。另一个容易忽略的是,平台对属性上报没有强制频率限制,但公共实例对单设备的链路有吞吐限制,建议实测时上报间隔不要低于1秒,否则会出现消息被静默丢弃的情况。
4.2 下行控制两路开关的订阅和处理
平台下发开关指令时,会向设备发送一条属性设置消息,Topic是:
/sys/{productKey}/{deviceName}/thing/service/property/set设备端需要先订阅这个Topic。订阅成功后再等回调,示例:
set_topic = f"/sys/{product_key}/{device_name}/thing/service/property/set" client.subscribe(set_topic, qos=1) def on_message(client, userdata, msg): data = json.loads(msg.payload.decode()) if data.get("method") != "thing.service.property.set": return params = data.get("params", {}) if "switch1" in params: print("第1路开关:", params["switch1"]) if "switch2" in params: print("第2路开关:", params["switch2"]) reply_topic = f"/sys/{product_key}/{device_name}/thing/service/property/set_reply" reply = { "id": data["id"], "version": "1.0", "code": 200, "data": {}, } client.publish(reply_topic, json.dumps(reply), qos=1) client.on_message = on_message收到thing.service.property.set消息后,从params里取出switch1、switch2,对应去控制继电器的GPIO即可。平台一次可能只下发一路开关,比如只下发{"switch1":0},所以代码里必须判断key是否存在,不能假设每次都同时带两路。
4.3 收到设置后必须回复,否则控制台永远显示“处理中”
做完开关动作之后,必须马上向set_reply这个Topic回复一条确认消息。回复里的id必须和下发消息里的id保持一致,code为200表示成功。很多第一次做物联网开关的人会在这一步漏掉回复,结果就是:设备端明明执行了动作,但控制台里的在线调试界面一直提示下发超时。
如果执行失败,可以返回非200的错误码,平台会把状态透传给业务侧。另外强调一点,设备端在本地执行完开关动作后,建议再主动上报一次当前开关状态,格式和4.1小节完全一样。这样两路开关的状态在云端永远和物理继电器保持一致,避免平台下发和本地误操作造成状态不一致。
5. 消息“不达”、连接被踢、开关没反应:排错的顺序和方法
5.1 先用MQTTX复现连接参数问题
遇到问题先分层:连接层问题还是消息层问题。连接层问题用MQTTX或MQTT.fx这类mqtt客户端软件直接填参数测试。同一个三元组在MQTTX里连接成功,就说明签名、时间戳、Broker地址都没问题,问题在自己的设备代码或者网络。注意阿里云IoT一个设备同时只允许一个MQTT在线连接,新连接会踢掉旧连接。如果你开着MQTTX测试,同时设备代码也在连接,两边会反复互踢,现象就是每隔几秒掉线重连。
5.2 用云端日志服务看消息轨迹
连接正常但消息不达,去控制台的“监控运维-日志服务”里查云端运行日志。日志可以按DeviceName、Topic或消息方向筛选。上报失败时能看到具体的失败原因,比如params校验失败,或者属性未定义。下行指令发送失败时也能看到设备是否在线、是否订阅了对应Topic。这里有个经验:千万不要只看设备端日志,阿里云IoT的云端日志是判断“消息到底有没有到平台”的唯一依据,设备端打印了“publish success”只是说丢给了本地协议栈,不说明平台已接收。
5.3 三个高频问题的定位顺序
第一类:设备一直在重连。优先查是否多客户端同时在线,然后查签名content里的clientId是否包含完整的竖线参数段,最后看设备时钟是否准确。第二类:数据上报控制台不显示。依次查物模型是否发布、params的key与标识符是否一致、Bool是不是用了ture/false写法。第三类:开关下行无动作。依次查是否订阅了thing/service/property/set、物模型属性是否为读写、收到消息后是否回复了set_reply。最后一个小技巧:在MQTTX里同时订阅property/set和set_reply两个Topic,再用平台在线调试功能下发一次指令,就能完整看到平台到设备、设备到平台两段消息,整个链路哪一段断了,一眼就清楚。
本文还有配套的精品资源,点击获取