13 JetLinks MQTT:网关设备与网关子设备 - 温控设备场景

在物联网开发中,网关设备与子设备的通信是核心场景之一。本文以温控设备(模拟空调) 为例,详细讲解基于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 设备离线规则

  1. 被动离线:网关MQTT连接断开(MQTT心跳机制检测),云平台自动标记网关及下属子设备离线;
  2. 主动离线:实际开发中极少主动上报离线报文,重点需排查离线原因(如网络、设备故障)并恢复上线。

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()

代码核心功能说明

  1. MQTT连接:网关以自身设备ID为客户端ID连接MQTT服务器,开启自动重连保障稳定性;
  2. 属性上报:每5秒模拟环境温度波动,主动上报子设备的环境温度和目标温度,触发子设备上线;
  3. 写属性处理:监听云平台下发的目标温度修改指令,校验参数有效性后更新状态,并回复执行结果;
  4. 日志调试:启动时打印所有MQTT主题的详细信息,关键操作记录日志,便于问题定位。

5. 测试验证

5.1 属性上报测试

运行代码后,日志中可看到每5秒上报的温度属性,与云平台显示的时间、数值完全一致:

图中看出 时间和属性值对得上 ,测试成功

5.2 属性修改测试

云平台下发目标温度修改指令后,网关接收并处理,上报的属性值与Web端显示一致:

图中看出 客户端监听到属性修改报文,之后再主动上传属性 ,web显示一致,测试成功

6. 总结

  1. 子设备上线核心:子设备无独立MQTT连接,需通过网关上报任意属性触发上线;
  2. 属性交互逻辑 :主动上报用/properties/report主题,写属性用/properties/write下行+/write/reply上行回复;
  3. 稳定性保障:代码实现了MQTT自动重连、参数校验、异常捕获,符合工业级设备开发规范。

如有开发疑问,欢迎评论区交流探讨!

相关推荐
我的xiaodoujiao7 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 47--设置Selenium以无头模式运行代码
python·学习·selenium·测试工具·pytest
一只大侠的侠13 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
寻星探路13 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
王达舒199413 小时前
HTTP vs HTTPS: 终极解析,保护你的数据究竟有多重要?
网络协议·http·https
朱皮皮呀13 小时前
HTTPS的工作过程
网络协议·http·https
Binary-Jeff13 小时前
一文读懂 HTTPS 协议及其工作流程
网络协议·web安全·http·https
ValhallaCoder16 小时前
hot100-二叉树I
数据结构·python·算法·二叉树
猫头虎17 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven
八零后琐话17 小时前
干货:程序员必备性能分析工具——Arthas火焰图
开发语言·python