11 JetLinks MQTT 直连设备功能调用完整流程与 Python 实现

1. 前言

JetLinks作为开源的IoT物联网平台,提供了完善的设备接入、物模型管理、功能调用等核心能力,其中MQTT协议是设备与平台直连的主流方式。本次测试以继电器设备为核心测试载体,继电器具备明确的"通/断"二元状态,且状态变更可直观验证,能精准覆盖"平台下发功能指令-设备解析指令-执行操作-反馈结果"全流程,是测试MQTT直连设备功能调用完整性、准确性的理想场景。

本次实践聚焦JetLinks平台与MQTT直连设备的功能调用交互逻辑,核心验证以下目标:

  1. 验证JetLinks平台物模型定义与设备端参数解析的匹配性;
  2. 打通"平台下发功能调用指令→设备接收解析→执行操作→回复执行结果"的闭环;
  3. 标准化MQTT主题格式、报文结构,为其他类型设备(如传感器、控制器)的功能调用提供可复用的参考范式。

2. 创建产品、设备

mqtt秘钥和客户端的秘钥生成,本人已经修改过对接认证方式,参考这篇文档https://blog.csdn.net/weixin_43951955/article/details/157621331?spm=1001.2014.3001.5501

重点是物模型的创建

此处务必要一一对应,功能的输入参数,就是输入到设备的控制参数,如本次继电器的"status"参数,需与物模型中定义的参数名、数据类型完全一致,否则设备端解析会出现参数缺失或类型不匹配问题。

3. mqtt主题及报文

3.1 事件上报主题

设备端除接收平台的功能调用指令外,也可通过事件上报主题主动推送设备状态、异常信息等数据,常见主题格式为/{productId}/{deviceId}/event/{eventId},本次聚焦功能调用,事件上报逻辑可参考JetLinks官方文档扩展实现。

3.2 功能主题

  • 平台下发功能调用指令

    mqtt主题

    从主题的格式就可以看出来,该主题是监听所有的功能调用的,因此需要在回调函数做不同功能的区分

    text 复制代码
    /{productId:产品ID}/{deviceId:设备ID}/function/invoke

    报文内容

    json 复制代码
    {
        "headers": {
            "deviceName": "继电器-测试",
            "productName": "继电器-类",
            "productId": "2018348352173023232",
            "_uid": "fJwi830cTZDlvTUXmWrCuy9QersSPNM9",
            "async": false,
            "traceparent": "00-bb2626ad382614b8eaf1c9d1ddbf798e-14d237caace59b9e-01"
        },
        "messageId": "2018625963334967296",
        "deviceId": "2018564501706469376",
        "timestamp": 1770112908572,
        "functionId": "control_relay_switch",
        "inputs": [
            {
            "name": "status",
            "value": true
            }
        ],
        "messageType": "INVOKE_FUNCTION",
        "replyType": "INVOKE_FUNCTION_REPLY"
    }

    报文中messageId为平台生成的唯一消息标识,设备回复时需原样返回,用于平台关联指令与回复;functionId需与物模型中定义的功能标识符完全一致,是设备端区分不同功能的核心依据;inputs数组为功能调用的入参,参数名和值需严格匹配物模型定义。

  • 设备响应平台下发的功能调用指令

    mqtt主题

    text 复制代码
    /{productId:产品ID}/{deviceId:设备ID}/function/invoke/reply

    报文内容

    json 复制代码
    {
        "messageId": "2018625963334967296",
        "output": true,
        "success": true
    }

    功能的响应报文和属性的读取响应报文结构基本一致 ,核心需包含messageId(关联原指令)、success(执行结果)、output(功能执行后的输出值),平台通过success判断指令是否执行成功,output用于展示功能执行后的设备状态,格式需简洁且符合平台解析要求。

4. 代码

python 复制代码
import json
import logging
import time
from paho.mqtt import client as mqtt_client
from paho.mqtt.client import MQTTv311

# ===================== 配置项 ======================
MQTT_BROKER = "192.168.120.176"
MQTT_PORT = 1883
MQTT_USERNAME = "admin"
MQTT_PASSWORD = "admin"
CLIENT_ID = "2019050361171279872"

# 设备基础信息
PRODUCT_ID = "2019049642691198976"
DEVICE_ID = "2019050361171279872"

# ========== 官方指定的功能调用Topic ==========
FUNCTION_INVOKE_TOPIC = f"/{PRODUCT_ID}/{DEVICE_ID}/function/invoke"
FUNCTION_REPLY_TOPIC = f"/{PRODUCT_ID}/{DEVICE_ID}/function/invoke/reply"

# ========== 继电器设备状态 ==========
DEVICE_STATE = {
    "relay_switch_status": False,  # false=断开,true=闭合
    "relay_switch_count": 0,  # 累计通断次数
    "last_switch_time": None,  # 最后切换时间
}

# 日志配置
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


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})")

    # 创建客户端
    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)

    # 连接服务器
    try:
        client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
    except Exception as e:
        logger.error(f"❌ TCP连接失败:{str(e)}")
        raise
    return client


def parse_official_function_invoke(payload: str) -> tuple:
    """解析官方格式的功能调用指令"""
    try:
        payload_data = json.loads(payload)
        # 提取官方定义的核心字段
        message_id = payload_data.get("messageId")  # 消息ID(回复时需原样返回)
        function_id = payload_data.get("functionId")  # 功能标识(control_relay_switch)
        inputs = payload_data.get(
            "inputs", []
        )  # 官方参数格式:数组[{"name":"xxx","value":"xxx"}]

        # 将inputs数组转换为字典(便于使用)
        params = {}
        for item in inputs:
            param_name = item.get("name")
            param_value = item.get("value")
            if param_name:
                params[param_name] = param_value

        return message_id, function_id, params
    except json.JSONDecodeError:
        logger.error(f"❌ 官方格式指令解析失败:{payload}")
        return None, None, {}


def handle_control_relay_switch_official(params: dict) -> dict:
    """处理继电器开关控制(适配官方参数格式)"""
    # 1. 校验参数(官方inputs中name为status)
    if "status" not in params:
        return {
            "success": False,
            "current_status": DEVICE_STATE["relay_switch_status"],
            "msg": "缺少必填参数:status",
        }

    target_status = params["status"]
    if not isinstance(target_status, bool):
        return {
            "success": False,
            "current_status": DEVICE_STATE["relay_switch_status"],
            "msg": "参数错误:status必须是布尔值",
        }

    # 2. 更新设备状态
    DEVICE_STATE["relay_switch_status"] = target_status
    DEVICE_STATE["relay_switch_count"] += 1
    DEVICE_STATE["last_switch_time"] = time.strftime(
        "%Y-%m-%d %H:%M:%S", time.localtime()
    )

    # 3. 返回执行结果(用于构造回复)
    logger.info(
        f"🔌 继电器状态更新:{'闭合' if target_status else '断开'} | 累计次数:{DEVICE_STATE['relay_switch_count']}"
    )
    return {
        "success": True,
        "current_status": target_status,
        "msg": f"继电器已{'闭合' if target_status else '断开'}",
    }


def generate_official_reply_payload(message_id: str, handle_result: dict) -> str:
    """构造匹配平台格式的回复报文"""
    # 严格匹配平台返回格式:messageId + output(布尔值) + success(布尔值)
    reply_payload = json.dumps(
        {
            "messageId": message_id,  # 与下发指令的messageId完全一致
            "output": handle_result.get(
                "current_status", False
            ),  # 直接返回布尔值(开关状态)
            "success": handle_result.get("success", False),  # 顶级success字段
        },
        ensure_ascii=False,
    )
    return reply_payload


def on_message(client, userdata, msg):
    """处理官方格式的功能调用指令"""
    topic = msg.topic
    payload = msg.payload.decode("utf-8", errors="ignore")

    if topic == FUNCTION_INVOKE_TOPIC:
        logger.info(f"\n📩 收到官方格式功能调用指令:")
        logger.info(f"   📌 Topic: {topic}")
        logger.info(
            f"   📝 报文: {json.dumps(json.loads(payload), ensure_ascii=False, indent=2)}"
        )

        # 1. 解析官方格式指令
        message_id, function_id, params = parse_official_function_invoke(payload)
        if not message_id or not function_id:
            logger.error("❌ 指令解析失败,跳过回复")
            return

        # 2. 处理control_relay_switch功能
        if function_id == "control_relay_switch":
            handle_result = handle_control_relay_switch_official(params)
        else:
            handle_result = {
                "success": False,
                "current_status": DEVICE_STATE["relay_switch_status"],
                "msg": f"不支持的功能:{function_id}",
            }

        # 3. 构造匹配平台格式的回复报文
        reply_payload = generate_official_reply_payload(message_id, handle_result)

        # 4. 发布回复
        publish_result = client.publish(FUNCTION_REPLY_TOPIC, reply_payload, qos=0)
        if publish_result[0] == 0:
            logger.info(f"✅ 回复成功(匹配平台格式):")
            logger.info(f"   📌 Topic: {FUNCTION_REPLY_TOPIC}")
            logger.info(
                f"   📝 报文: {json.dumps(json.loads(reply_payload), ensure_ascii=False, indent=2)}"
            )
        else:
            logger.error(f"❌ 回复发布失败,状态码:{publish_result[0]}")


def subscribe_official_topic(client: mqtt_client.Client):
    """订阅官方功能调用Topic"""
    client.subscribe(FUNCTION_INVOKE_TOPIC, qos=0)
    client.on_message = on_message
    logger.info(f"📌 已订阅官方功能调用Topic:{FUNCTION_INVOKE_TOPIC}")
    logger.info(
        f"📌 继电器初始状态:{'断开' if not DEVICE_STATE['relay_switch_status'] else '闭合'}"
    )
    logger.info(f"📌 等待官方格式控制指令...")


def run():
    """启动适配平台格式的继电器控制服务"""
    logger.info("🚀 启动JetLinks 继电器MQTT服务(匹配平台返回格式)")
    client = connect_mqtt()
    subscribe_official_topic(client)
    client.loop_forever()


if __name__ == "__main__":
    try:
        run()
    except KeyboardInterrupt:
        logger.info("\n🛑 退出继电器MQTT服务")
    except Exception as e:
        logger.error(f"❌ 程序异常:{str(e)}", exc_info=True)

代码中实现了MQTT自动重连、指令解析容错、参数校验等生产级特性,可直接复用至其他MQTT直连设备;核心逻辑分为"连接MQTT服务器→订阅功能调用主题→解析平台指令→执行设备操作→回复执行结果"五部分,各函数职责单一,便于扩展和维护。

5. 测试

测试正常结果应该是从平台下发功能测试指令,设备接受到指令之后先回复一个返回报文的主题,接着执行指令,以下为测试结果

结论:

  • 通过调试界面分别下发 的控制,分别对应开启和关闭
  • 当设备收到报文之后,解析出正确的结果,提取控制指令,打印结果
  • 使用回复主题的报文,返回一条消息给服务器
  • 如果服务器正确接受到报文,就不会显示超时的弹窗

测试过程中需确保MQTT Broker地址、设备认证信息、物模型参数与代码配置一致;若平台出现"指令超时"提示,需排查设备是否订阅正确主题、回复报文是否包含正确的messageId、网络是否通畅等问题。

6. 结论

本次基于JetLinks平台完成了MQTT直连继电器设备的功能调用全流程验证,核心结论如下:

  1. 交互逻辑闭环验证通过:JetLinks平台下发的功能调用指令可被设备端正确解析,设备执行操作后返回的响应报文能被平台识别,实现了"平台指令下发-设备执行-结果反馈"的完整闭环,无超时、解析失败等异常。
  2. 物模型与代码适配关键 :物模型中定义的功能标识符(functionId)、参数名(status)需与设备端代码严格一致,是功能调用成功的核心前提;参数类型(如布尔值)的匹配性直接影响指令执行结果。
  3. 代码具备通用复用性 :本次编写的MQTT客户端代码(含连接管理、指令解析、响应构造)可适配各类MQTT直连设备的功能调用场景,仅需修改DEVICE_STATE、功能处理函数(如handle_control_relay_switch_official)即可快速适配传感器、执行器等不同类型设备。
  4. 异常处理保障稳定性:代码中加入的参数校验、JSON解析容错、MQTT自动重连等机制,有效提升了设备端的鲁棒性,可应对网络波动、指令格式异常等常见场景,符合物联网设备长期稳定运行的要求。

综上,JetLinks平台的MQTT功能调用机制具备良好的兼容性和易用性,本次实践的配置方式、代码逻辑可作为IoT设备接入JetLinks平台的标准化参考方案。

相关推荐
2401_841495642 小时前
【LeetCode刷题】对称二叉树
数据结构·python·算法·leetcode·二叉树··递归
理智.6292 小时前
Windows 本地文件上传到 Linux 服务器的完整实践(scp/ssh),以及常见踩坑总结
linux·服务器·ssh
翼龙云_cloud2 小时前
阿里云渠道商:阿里云弹性伸缩如何助力海量数据采集?
服务器·阿里云·云计算
酉鬼女又兒2 小时前
Linux快速入门指南:常用快捷键➕命令行高效操作
linux·运维·服务器
小学导航员2 小时前
VMWARE虚拟机上不了网络
服务器·网络·php
小饼干超人2 小时前
pytorch返回张量元素总数量的方法 x.numel()
人工智能·pytorch·python
张3蜂2 小时前
java springboot2.0 api ;.netcore8 api ;python GunicornAPI ,哪种更强?请从多个维度,对比分析
java·python·.netcore
林shir2 小时前
3-19-项目部署(Linux)
linux·运维·服务器
顶点多余2 小时前
Linux第一个系统程序-进度条
linux·运维·服务器