在物联网开发中,网关设备与子设备的通信是核心场景之一。本文以温控设备(模拟空调) 为例,详细讲解基于MQTT协议的网关-子设备属性读写交互逻辑,包括设备上线、属性主动上报、目标温度写入等核心功能,并提供可直接运行的Python实现代码,为物联网设备开发提供实用参考。
关于jetlinks的其他已测试内容 ,可以参考本人的专栏https://blog.csdn.net/weixin_43951955/category_13043980.html?spm=1001.2014.3001.5482
1. 场景背景
本次示例中的温控设备包含两个核心属性:
- 当前环境温度(temperature):即室温,随环境实时变化;
- 目标温度(temperature_dest):即空调设定温度,支持云平台远程写入修改。
本文重点聚焦属性的写入操作,同时覆盖子设备上线、属性主动上报等基础逻辑。
2. MQTT 主题与报文规范
网关子设备无需独立的MQTT客户端,直接复用网关的MQTT通道与云平台通信,核心交互围绕以下主题和报文格式展开。
2.1 主动上报属性(子设备→云平台)
子设备通过网关主动上报环境温度和目标温度,报文格式简洁,同时该操作也是子设备的"上线触发条件"。
上报报文格式
json
{
"properties": {
"temperature": 25.6,
"temperature_dest": 27
}
}
工具测试示例
使用MQTTX工具直接上传上述报文,即可完成属性上报:

使用 MQTTX 工具上传两个温度属性,显示OK,成功
2.2 写属性(云平台→网关→子设备)
云平台下发指令修改子设备的目标温度,网关接收指令后处理并回复执行结果。
主题约定
| 方向 | 主题格式 | 说明 |
|---|---|---|
| 下行(云→网关) | /{产品ID}/{设备名称}/properties/write |
云平台下发写属性指令 |
| 上行(网关→云) | /{productId}/{deviceId}/properties/write/reply |
网关回复指令执行结果 |
报文格式
下行报文(云平台下发)
json
{
"headers": {
"deviceName": "温控-485",
"productName": "温控-子设备",
"productId": "2020388484487761920",
"_uid": "WZw9Nea1m8IlUn1AiebQifJlC8OEEenV",
"traceparent": "00-306a597889cdd28e5611a38ebdf8587e-755ec08297b54fd5-01"
},
"messageId": "2020473806001197056",
"deviceId": "2020389405645000704",
"timestamp": 1770553468595,
"properties": {
"temperature_dest": 25
},
"messageType": "WRITE_PROPERTY",
"replyType": "READ_PROPERTY_REPLY"
}
上行报文(网关回复)
需原样返回messageId,并标识执行结果:
json
{
"messageId":"平台下发报文中的messageId",
"success":true
}
3. 网关与子设备的上下线逻辑
3.1 设备上线规则
| 设备类型 | 上线条件 | 核心说明 |
|---|---|---|
| 网关设备 | MQTT客户端成功连接云平台 | 网关作为独立MQTT客户端,连接成功 即 在线 |
| 子设备 | 网关通过MQTT通道上报子设备任意属性 | 子设备无独立MQTT连接,上报属性是触发上线的核心动作;若无属性可上报,可自定义"上下线状态"属性,怎么折腾都行,没有属性就创造属性 |
3.2 设备离线规则
- 被动离线:网关MQTT连接断开(MQTT心跳机制检测),云平台自动标记网关及下属子设备离线;
- 主动离线:实际开发中极少主动上报离线报文,重点需排查离线原因(如网络、设备故障)并恢复上线。
3.3 MQTTX工具测试验证
网关设备上线
仅需完成MQTT连接,网关即可上线:

只要连接MQTT,网关即可上线
子设备上线
通过网关的MQTT通道发送任意属性上报报文,子设备立即上线:

只要发一个属性上报报文,子设备就上线了
4. Python 实现代码
以下代码完整实现网关的MQTT连接、子设备属性5秒定时上报、目标温度写入指令处理等核心功能,代码无需修改,可直接运行,杠杠的。
python
import json
import logging
import time
import random
from threading import Timer
from paho.mqtt import client as mqtt_client
from paho.mqtt.client import MQTTv311
# ===================== 核心配置项 ======================
# MQTT服务器配置
MQTT_BROKER = "192.168.111.53"
MQTT_PORT = 1883
MQTT_USERNAME = "admin"
MQTT_PASSWORD = "admin"
# 网关设备信息(作为MQTT客户端标识)
GATEWAY_PRODUCT_ID = "2020387880562511872"
GATEWAY_DEVICE_ID = "2020389102749143040"
CLIENT_ID = GATEWAY_DEVICE_ID # 网关设备ID作为MQTT客户端ID
# 子设备(温控设备)信息
SUB_DEVICE_PRODUCT_ID = "2020388484487761920"
SUB_DEVICE_ID = "2020389405645000704"
# ========== Topic定义 ==========
# 1. 子设备属性主动上报Topic
SUB_DEVICE_PROPERTY_REPORT_TOPIC = (
f"/{SUB_DEVICE_PRODUCT_ID}/{SUB_DEVICE_ID}/properties/report"
)
# 2. 子设备写属性下行Topic(云平台→网关)
SUB_DEVICE_PROPERTY_WRITE_TOPIC = (
f"/{SUB_DEVICE_PRODUCT_ID}/{SUB_DEVICE_ID}/properties/write"
)
# 3. 子设备写属性响应上行Topic(网关→云平台)
SUB_DEVICE_PROPERTY_WRITE_REPLY_TOPIC = (
f"/{SUB_DEVICE_PRODUCT_ID}/{SUB_DEVICE_ID}/properties/write/reply"
)
# ========== 温控设备状态 ==========
DEVICE_STATE = {
"temperature": 25.0, # 当前环境温度(模拟随机波动)
"temperature_dest": 27.0, # 目标温度(初始值)
"last_update_time": None, # 最后更新时间
}
# 日志配置
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
def print_all_mqtt_topics_detail():
"""详细打印代码中所有MQTT主题(含变量名、含义、流向、实际值)"""
logger.info("\n" + "=" * 80)
logger.info("📋 代码中所有MQTT主题详细信息")
logger.info("=" * 80)
# 构造主题详情列表(变量名、含义、数据流向、实际值)
topic_details = [
{
"var_name": "SUB_DEVICE_PROPERTY_REPORT_TOPIC",
"description": "子设备属性主动上报Topic",
"data_flow": "上行(网关 → 云平台)",
"purpose": "定时上报环境温度、目标温度,同时触发子设备上线",
"actual_value": SUB_DEVICE_PROPERTY_REPORT_TOPIC,
},
{
"var_name": "SUB_DEVICE_PROPERTY_WRITE_TOPIC",
"description": "子设备写属性下行Topic",
"data_flow": "下行(云平台 → 网关)",
"purpose": "接收云平台下发的目标温度修改指令",
"actual_value": SUB_DEVICE_PROPERTY_WRITE_TOPIC,
},
{
"var_name": "SUB_DEVICE_PROPERTY_WRITE_REPLY_TOPIC",
"description": "子设备写属性响应上行Topic",
"data_flow": "上行(网关 → 云平台)",
"purpose": "回复云平台写属性指令的执行结果(success=true/false)",
"actual_value": SUB_DEVICE_PROPERTY_WRITE_REPLY_TOPIC,
},
]
# 逐个打印主题详情
for idx, topic in enumerate(topic_details, 1):
logger.info(f"\n【{idx}】主题变量:{topic['var_name']}")
logger.info(f" 📖 含义:{topic['description']}")
logger.info(f" 📥📤 数据流向:{topic['data_flow']}")
logger.info(f" 🎯 用途:{topic['purpose']}")
logger.info(f" 🔍 实际完整主题:\n {topic['actual_value']}")
logger.info("\n" + "=" * 80 + "\n")
def connect_mqtt() -> mqtt_client.Client:
"""创建并连接MQTT客户端(网关身份)"""
def on_connect(client, userdata, flags, rc, properties=None):
rc_msg = {
0: "连接成功",
1: "协议版本错误",
2: "客户端ID非法",
3: "服务器不可用",
4: "用户名/密码错误",
5: "未授权",
}
if rc == 0:
logger.info(f"✅ 网关MQTT连接成功({MQTT_BROKER}:{MQTT_PORT})")
else:
logger.error(f"❌ 网关连接失败(rc={rc}):{rc_msg.get(rc, '未知错误')}")
def on_disconnect(client, userdata, rc, properties=None):
if rc != 0:
logger.warning(f"⚠️ 网关MQTT被动断开,将自动重连(rc={rc})")
# 创建MQTT客户端(网关身份)
client = mqtt_client.Client(
client_id=CLIENT_ID,
callback_api_version=mqtt_client.CallbackAPIVersion.VERSION1,
protocol=MQTTv311,
)
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.auto_reconnect = True
client.reconnect_delay_set(min_delay=2, max_delay=10)
# 连接MQTT服务器
try:
client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
except Exception as e:
logger.error(f"❌ 网关TCP连接失败:{str(e)}")
raise
return client
def simulate_environment_temperature():
"""模拟环境温度随机波动(±0.5℃)"""
base_temp = DEVICE_STATE["temperature"]
# 随机波动,范围20~30℃
new_temp = base_temp + random.uniform(-0.5, 0.5)
new_temp = max(20.0, min(30.0, new_temp)) # 限制温度范围
DEVICE_STATE["temperature"] = round(new_temp, 1)
def report_temperature_properties(client: mqtt_client.Client):
"""主动上报温度属性(每5秒执行)"""
try:
# 1. 模拟环境温度波动
simulate_environment_temperature()
DEVICE_STATE["last_update_time"] = time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime()
)
# 2. 构造上报报文
report_payload = json.dumps(
{
"properties": {
"temperature": DEVICE_STATE["temperature"],
"temperature_dest": DEVICE_STATE["temperature_dest"],
}
},
ensure_ascii=False,
)
# 3. 发布上报报文
result = client.publish(SUB_DEVICE_PROPERTY_REPORT_TOPIC, report_payload, qos=0)
if result[0] == 0:
logger.info(
f"📤 子设备属性上报成功 | 环境温度:{DEVICE_STATE['temperature']}℃ | 目标温度:{DEVICE_STATE['temperature_dest']}℃"
)
else:
logger.error(f"❌ 子设备属性上报失败,状态码:{result[0]}")
except Exception as e:
logger.error(f"❌ 属性上报异常:{str(e)}")
# 4. 定时任务:5秒后再次执行
Timer(5, report_temperature_properties, args=[client]).start()
def parse_write_property_request(payload: str) -> tuple:
"""解析云平台下发的写属性请求"""
try:
payload_data = json.loads(payload)
# 提取核心字段
message_id = payload_data.get("messageId") # 回复时需原样返回
properties = payload_data.get("properties", {}) # 待写入的属性
message_type = payload_data.get("messageType") # 消息类型(WRITE_PROPERTY)
return message_id, properties, message_type
except json.JSONDecodeError:
logger.error(f"❌ 写属性请求解析失败:{payload}")
return None, {}, ""
def handle_write_temperature_dest(properties: dict) -> bool:
"""处理目标温度写入请求"""
# 1. 校验参数
if "temperature_dest" not in properties:
logger.error("❌ 写属性请求缺少参数:temperature_dest")
return False
target_temp = properties["temperature_dest"]
# 校验温度范围(合理范围:16~35℃)
if (
not isinstance(target_temp, (int, float))
or target_temp < 16
or target_temp > 35
):
logger.error(f"❌ 目标温度参数错误:{target_temp}(必须是16~35之间的数字)")
return False
# 2. 更新设备状态
DEVICE_STATE["temperature_dest"] = round(target_temp, 1)
DEVICE_STATE["last_update_time"] = time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime()
)
logger.info(
f"🔧 目标温度更新成功 | 新目标温度:{DEVICE_STATE['temperature_dest']}℃"
)
return True
def on_message(client, userdata, msg):
"""处理MQTT消息(主要是写属性请求)"""
topic = msg.topic
payload = msg.payload.decode("utf-8", errors="ignore")
logger.info(f"\n📩 收到MQTT消息:")
logger.info(f" 📌 Topic: {topic}")
logger.info(
f" 📝 报文: {json.dumps(json.loads(payload), ensure_ascii=False, indent=2) if payload else '空报文'}"
)
# 处理子设备写属性请求
if topic == SUB_DEVICE_PROPERTY_WRITE_TOPIC:
# 1. 解析请求报文
message_id, properties, message_type = parse_write_property_request(payload)
if not message_id or message_type != "WRITE_PROPERTY":
logger.error("❌ 非有效写属性请求,跳过回复")
return
# 2. 处理目标温度写入
success = handle_write_temperature_dest(properties)
# 3. 构造回复报文
reply_payload = json.dumps(
{"messageId": message_id, "success": success}, ensure_ascii=False
)
# 4. 发布回复报文
reply_result = client.publish(
SUB_DEVICE_PROPERTY_WRITE_REPLY_TOPIC, reply_payload, qos=0
)
if reply_result[0] == 0:
logger.info(
f"✅ 写属性回复成功 | messageId: {message_id} | success: {success}"
)
else:
logger.error(f"❌ 写属性回复发布失败,状态码:{reply_result[0]}")
def subscribe_related_topics(client: mqtt_client.Client):
"""订阅相关Topic(主要是写属性请求Topic)"""
# 订阅子设备写属性Topic
client.subscribe(SUB_DEVICE_PROPERTY_WRITE_TOPIC, qos=0)
client.on_message = on_message
logger.info(f"📌 已订阅子设备写属性Topic:{SUB_DEVICE_PROPERTY_WRITE_TOPIC}")
logger.info(
f"📌 子设备初始状态 | 环境温度:{DEVICE_STATE['temperature']}℃ | 目标温度:{DEVICE_STATE['temperature_dest']}℃"
)
def run():
"""启动网关MQTT服务(核心入口)"""
logger.info("🚀 启动JetLinks网关子设备(温控设备)MQTT服务")
# ========== 新增:程序启动时先打印所有主题详情 ==========
print_all_mqtt_topics_detail()
# 1. 连接MQTT服务器
client = connect_mqtt()
# 2. 订阅相关Topic
subscribe_related_topics(client)
# 3. 启动5秒定时上报属性
report_temperature_properties(client)
# 4. 保持MQTT循环
try:
client.loop_forever()
except KeyboardInterrupt:
logger.info("\n🛑 退出网关MQTT服务")
except Exception as e:
logger.error(f"❌ 程序异常:{str(e)}", exc_info=True)
finally:
client.disconnect()
if __name__ == "__main__":
run()
代码核心功能说明
- MQTT连接:网关以自身设备ID为客户端ID连接MQTT服务器,开启自动重连保障稳定性;
- 属性上报:每5秒模拟环境温度波动,主动上报子设备的环境温度和目标温度,触发子设备上线;
- 写属性处理:监听云平台下发的目标温度修改指令,校验参数有效性后更新状态,并回复执行结果;
- 日志调试:启动时打印所有MQTT主题的详细信息,关键操作记录日志,便于问题定位。
5. 测试验证
5.1 属性上报测试
运行代码后,日志中可看到每5秒上报的温度属性,与云平台显示的时间、数值完全一致:

图中看出
时间和属性值对得上,测试成功
5.2 属性修改测试
云平台下发目标温度修改指令后,网关接收并处理,上报的属性值与Web端显示一致:

图中看出 客户端监听到属性修改报文,之后再主动上传属性 ,web显示一致,测试成功
6. 总结
- 子设备上线核心:子设备无独立MQTT连接,需通过网关上报任意属性触发上线;
- 属性交互逻辑 :主动上报用
/properties/report主题,写属性用/properties/write下行+/write/reply上行回复; - 稳定性保障:代码实现了MQTT自动重连、参数校验、异常捕获,符合工业级设备开发规范。
如有开发疑问,欢迎评论区交流探讨!