MQTT DEVICE ONBOARDING

把设备数据送到 EMQX,
剩下的交给 RoamLink。

这份手册面向设备开发者、家庭自动化用户和网关维护者,完整说明如何把设备或网关安全接入自己的 EMQX Broker,并准备可被 RoamLink 映射的 Topic、JSON 状态和控制命令。

产品责任边界

你的设备/网关采集设备、适配协议、连接网络
↓ MQTT over TLS
你的 EMQX Broker传输消息、认证身份、执行 Topic ACL
↓ MQTT over WSS

00 / SCOPE

RoamLink 接收 MQTT,
不接管设备协议。

你的设备可以来自 BLE、Zigbee、Wi-Fi、Modbus、CAN、PLC、Home Assistant、Node-RED 或自研程序。RoamLink 不关心它如何读取物理设备,只要求最终有一个标准 MQTT 客户端把状态发布到 EMQX,并在需要控制时订阅命令 Topic。

房车 ESP32 是参考实现

JK-BMS 与风扇通过 ESP32-S3 接入,是 RoamLink 自有房车空间的一套网关实现。其他用户可以完全不用 ESP32,也无需采用 ESPHome。

01 / QUICK START

最短接入路径

  1. 准备 Broker创建 EMQX Cloud Serverless,或者使用你自己的 EMQX;公网访问必须启用 TLS。
  2. 规划命名空间为账号、空间、设备准备稳定 ID,例如 acme/rv01/fan01
  3. 创建两个账号设备/网关账号负责发布状态,RoamLink 账号负责订阅状态;需要控制时才允许发布命令。
  4. 配置最小权限 ACL只允许两个账号访问这个空间的必要 Topic,最后增加默认拒绝规则。
  5. 发布三类消息stateavailability,以及可选的 command
  6. 用 MQTTX 验证先证明 Broker、TLS、账号、ACL 和消息都正确,再配置 RoamLink。
  7. 在 RoamLink 映射选择界面模板,填写状态/命令 Topic,将 JSON 字段映射到组件。
开始前准备EMQX 地址TLS 端口CA 证书设备唯一 ID可持续运行的 MQTT 客户端状态 JSON 样例

02 / EMQX BROKER

准备 EMQX Broker

方案 A:EMQX Cloud Serverless

进入 EMQX Cloud 项目后创建 Serverless 部署。部署状态变为 Running 后,在 Overview 页面记录 Address,并下载 CA Certificate。Serverless 默认只开放加密连接:

设备/网关8883MQTT over TLS,协议通常写作 mqtts
浏览器/RoamLink8084WebSocket over TLS,协议为 wss,路径 /mqtt
不要使用 1883/8083

Serverless 不提供未加密端口;连接时必须使用 EMQX 提供的真实域名并携带正确 SNI。不要把 Serverless 地址 CNAME 到自己的域名后再连接。

官方参考:Serverless Connection Guide ↗ · Create a Serverless Deployment ↗

方案 B:自建 EMQX

自建 Broker 也可以使用,但要自行完成公网域名、证书、TLS Listener、WSS Listener、防火墙和升级维护。端口可以自定义;填入 RoamLink 的必须是外网可达的 WSS 地址。禁止把 EMQX Dashboard 管理端口直接当作 MQTT 端口。

检查项要求
域名证书覆盖的公网域名,避免直接使用 IP
TLS服务器证书有效,客户端校验证书和主机名
WSS浏览器可访问,并确认 WebSocket 路径
认证关闭匿名访问,为每类客户端创建独立凭据
授权默认拒绝,按 Topic 单独允许发布/订阅

03 / AUTH & ACL

账号分离与最小权限

不要让设备和 RoamLink 共用同一个 MQTT 账号,更不能使用 EMQX 控制台管理员凭据。推荐每个空间至少两个客户端身份:

账号用途允许发布允许订阅
acme-rv01-device设备或网关acme/rv01/+/state
acme/rv01/+/availability
acme/rv01/+/command
acme-rv01-roamlinkRoamLink 控制端acme/rv01/+/commandacme/rv01/#

EMQX Cloud 配置步骤

  1. 打开 Access Control → Authentication,分别创建 device 与 roamlink 用户,使用随机且唯一的强密码。
  2. 打开 Authorization → Username,按上表逐条添加 Allow。
  3. 只读设备不创建任何 command Publish/Subscribe 权限。
  4. 打开 All Users,最后添加 Topic #、Action Publish & Subscribe、Permission Deny
  5. 分别用两个账号测试:应允许的操作成功,越权 Topic 必须失败。
Serverless 默认是未匹配即允许

Serverless 当前不能直接切换授权模式。必须通过“具体账号 Allow + All Users 对 # Deny”形成白名单效果;账号规则优先于最终的 All Users 拒绝规则。

官方参考:Default Authorization ↗ · Authentication ↗

04 / TOPIC DESIGN

Topic 是长期接口,先规划再上线。

推荐使用四级资源前缀,再以消息用途结尾:

{account_id}/{space_id}/{device_id}/{message_type}
acme/rv01/battery01/state设备当前状态
acme/rv01/battery01/availabilityonline/offline
acme/rv01/fan01/commandRoamLink 下发命令
acme/rv01/fan01/event可选:不覆盖的事件流

命名规则

  • 只用小写英文字母、数字、短横线和下划线;不要在 ID 中使用 +#/
  • ID 一旦上线尽量不改;显示名称可以在 RoamLink 中单独修改。
  • 不要把用户姓名、手机号、地址等隐私信息写入 Topic。
  • 状态、在线状态、命令和事件分开,避免一个 Topic 同时承担多个生命周期。
  • 命令 Topic 不使用 Retain,防止设备重连后执行旧命令。

05 / JSON PAYLOAD

状态消息应当可读、稳定、可扩展。

RoamLink 的映射层将 Topic 与 JSONPath 对应到界面能力。推荐每条状态都包含版本、时间、在线字段和值对象:

{
  "schema_version": 1,
  "device_id": "fan01",
  "timestamp": "2026-07-22T15:30:00+08:00",
  "online": true,
  "values": {
    "power": true,
    "actual_speed_percent": 48,
    "target_speed_percent": 50
  }
}
字段原则建议
数值使用 JSON number,不要写成 "48%";单位由映射配置提供
布尔使用 true/false,避免 ON、1、yes 混用
时间使用带时区的 ISO 8601,或统一毫秒时间戳
空值未知值使用 null 或省略字段,不要伪造 0
版本不兼容变更提升 schema_version
大小只发布控制中心需要的数据,避免把日志和二进制内容塞进状态

控制命令

{
  "request_id": "01J6RVFAN8K2",
  "action": "set",
  "values": { "power": true, "speed_percent": 50 },
  "issued_at": "2026-07-22T15:31:00+08:00",
  "expires_in_s": 10
}

设备执行后应在 state 或 event 中回传相同的 request_id 与执行结果。RoamLink 不能把“消息已发布”视为“物理设备已执行”。

06 / CONNECT YOUR SOURCE

选择你的设备接入方式

A. 已支持 MQTT 的设备

在厂商设备管理页寻找 MQTT、Cloud MQTT、Custom Broker 或 Telemetry 设置,并填写:

  • Host:EMQX Overview 中的 Address
  • Port:Serverless 使用 8883
  • TLS/SSL:开启,并校验证书
  • Username/Password:device 账号
  • Client ID:空间内唯一,例如 acme-rv01-fan01
  • State/Command Topic:遵循本手册 Topic 规范

如果设备只能使用 1883 且不能启用 TLS,不应直接连接公网 Serverless;应通过本地网关转发。

B. ESP32 / Arduino 网关

使用 WiFiClientSecurePubSubClient。下面展示关键结构;CA 内容从 EMQX Overview 下载,不能用 setInsecure() 代替生产证书校验。

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

WiFiClientSecure tls;
PubSubClient mqtt(tls);

const char* host = "YOUR_DEPLOYMENT.emqxsl.com";
const int port = 8883;
const char* stateTopic = "acme/rv01/fan01/state";
const char* availabilityTopic = "acme/rv01/fan01/availability";
const char* commandTopic = "acme/rv01/fan01/command";

void connectMqtt() {
  tls.setCACert(EMQX_CA_CERT);
  mqtt.setServer(host, port);
  mqtt.setCallback(onCommand);

  // availability/offline 是异常断线时由 Broker 发布的 LWT
  mqtt.connect(
    "acme-rv01-fan01",
    DEVICE_USERNAME,
    DEVICE_PASSWORD,
    availabilityTopic, 1, true, "offline"
  );

  mqtt.publish(availabilityTopic, "online", true);
  mqtt.subscribe(commandTopic, 1);
}

主循环必须持续调用 mqtt.loop(),断线时采用退避重连;重新连接后再次发布 online、订阅 command,并刷新 retained 状态。

官方参考:EMQX Connect with ESP32 ↗

C. Python 网关

import json
import paho.mqtt.client as mqtt

HOST = "YOUR_DEPLOYMENT.emqxsl.com"
BASE = "acme/rv01/fan01"

client = mqtt.Client(
    callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
    client_id="acme-rv01-fan01",
    protocol=mqtt.MQTTv5,
)
client.username_pw_set("acme-rv01-device", "DEVICE_PASSWORD")
client.tls_set(ca_certs="emqxsl-ca.crt")
client.will_set(f"{BASE}/availability", "offline", qos=1, retain=True)

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        client.publish(f"{BASE}/availability", "online", qos=1, retain=True)
        client.subscribe(f"{BASE}/command", qos=1)

def on_message(client, userdata, msg):
    command = json.loads(msg.payload)
    # 在这里校验 action、参数范围、过期时间,再操作设备

client.on_connect = on_connect
client.on_message = on_message
client.reconnect_delay_set(min_delay=1, max_delay=120)
client.connect(HOST, 8883, keepalive=30)

state = {"schema_version": 1, "online": True,
         "values": {"power": False, "actual_speed_percent": 0}}
client.publish(f"{BASE}/state", json.dumps(state), qos=1, retain=True)
client.loop_forever(retry_first_connection=True)

官方参考:Eclipse Paho Python Client ↗

D. Node-RED 网关

  1. 添加 mqtt out 节点,Broker 填 EMQX Address 与 8883。
  2. 创建 TLS 配置,启用服务器证书校验并导入 CA。
  3. 在 Security 中填写 device 用户名和密码,Client ID 保持唯一。
  4. 使用 change/function 节点把本地设备数据转换为规范 JSON。
  5. 发布 state 时选择 QoS 1、Retain true。
  6. 添加 mqtt in 订阅 command,进入 function 节点校验后再控制设备。
  7. 在 Broker 配置中设置 Birth/Will:online/offline,Retain true。

Node-RED 编辑器本身需要账号与 HTTPS 防护,不要直接把未加固的 1880 端口暴露到公网。

官方参考:Securing Node-RED ↗

E. Home Assistant

如果 EMQX 就是 Home Assistant 使用的 MQTT Broker,可在“设置 → 设备与服务 → MQTT”中配置 Broker 地址、TLS 和凭据,并通过自动化把实体状态发布到规划好的 Topic。若 HA 已经依赖另一台 Broker,推荐使用 Node-RED 或独立桥接程序把需要的数据转发到 EMQX,避免贸然替换现有 Broker。

Home Assistant 的 MQTT Discovery 不是 RoamLink 的设备描述协议。可以继续使用 Discovery 管理 HA 实体,同时额外发布一份面向 RoamLink 的稳定 JSON。

官方参考:Home Assistant MQTT Integration ↗

07 / VERIFY WITH MQTTX

先验证 MQTT,再配置 RoamLink。

  1. 建立 device 连接使用 mqtts、8883、CA、device 凭据;发布 availability 和 state。
  2. 建立 roamlink 测试连接使用另一个 Client ID 和 roamlink 凭据,订阅空间前缀。
  3. 确认即时状态新订阅者应立即收到 retained state 和 availability。
  4. 确认控制测试客户端向 command 发布非 retained 命令,device 客户端应收到。
  5. 确认越权失败尝试发布不在 ACL 内的 state 或其他空间 Topic,Broker 必须拒绝。
  6. 确认离线直接断网或终止 device 进程,订阅端应收到 retained offline LWT。

EMQX Cloud 官方推荐使用 MQTTX 先验证部署连接。调试完成后,不要在截图、日志或工单中保留真实密码。

09 / RELIABILITY

让在线状态与控制结果可信

消息QoSRetain原因
state1新打开控制中心立即获得最后状态
availability1online 与 LWT offline 覆盖同一个 Topic
command1避免设备重连后执行历史命令
event0 或 1事件是时间流,不应覆盖保存
  • 每个客户端使用唯一 Client ID,否则新连接可能踢掉旧连接。
  • 启用自动重连与指数退避,避免断网时高频重试。
  • 状态中携带时间,RoamLink 才能区分“最后已知值”和“实时值”。
  • 设备收到命令后检查参数范围、过期时间和幂等 request_id。
  • 涉及加热、门锁、水泵等物理风险的设备必须由设备端实施安全限制。

10 / TROUBLESHOOTING

常见故障排查

无法建立 TLS 连接

确认 Serverless 使用 8883/8084;Host 必须是 EMQX 提供的域名;校准设备时间;加载正确 CA;启用 SNI;不要用 IP 或自定义 CNAME 代替部署域名。

用户名密码正确但返回 Not authorized

Authentication 只决定能否连接,Authorization 决定能否发布/订阅。检查 Username 规则、Topic 大小写、通配符范围以及最终 deny 规则的顺序。

MQTTX 能连接,RoamLink 不能连接

RoamLink 是浏览器客户端,需要 WSS 端口而不是 MQTT/TLS TCP 端口。Serverless 使用 8084,并确认路径为 /mqtt

页面打开后没有初始数据

检查 state 是否设置 Retain;先用 MQTTX 新建订阅验证是否立即收到消息;确认 RoamLink 账号拥有 Subscribe 权限。

设备不断上下线

检查 Wi-Fi/蜂窝信号、Keep Alive、供电、重复 Client ID 和重连频率。EMQX Clients 页面可以看到连接源地址、Keepalive 和状态。

控制命令发布成功但设备没动作

确认 device 已订阅 command、命令不是 retained、JSON 字段和类型正确。必须根据设备回报确认执行,不能只看 publish 成功。

收到重复命令

QoS 1 允许重复交付。设备端使用 request_id 做幂等处理,同一 request_id 不重复执行有现实后果的动作。

JSON 映射结果是空值

检查实际 payload 是否为合法 UTF-8 JSON,字段层级是否与 JSONPath 一致,数值是否错误地带了单位字符串。

11 / SECURITY

上线前安全清单

✓ 只使用 MQTT/TLS 与 WSS✓ 校验 CA、主机名和 SNI✓ 关闭匿名连接✓ 设备与 RoamLink 分离账号✓ 每个空间独立 Topic 前缀✓ 以 deny # 作为最终规则✓ command 永不 Retain✓ 密码不写入前端源码✓ 定期轮换并撤销旧凭据✓ 日志、截图先脱敏✓ 设备端限制危险参数✓ 用越权测试验证 ACL

官方参考:EMQX Security Checklist ↗

12 / COMPLETE EXAMPLE

一台可控风扇的完整接口

设备account: acme
space: rv01
device: fan01
client id: acme-rv01-fan01

发布状态

Topic: acme/rv01/fan01/state
QoS: 1
Retain: true

{
  "schema_version": 1,
  "device_id": "fan01",
  "timestamp": "2026-07-22T15:30:00+08:00",
  "online": true,
  "values": {
    "power": true,
    "actual_speed_percent": 48,
    "target_speed_percent": 50
  }
}

发布在线状态

Topic: acme/rv01/fan01/availability
Online payload: online
Last Will payload: offline
QoS: 1
Retain: true

接收控制命令

Topic: acme/rv01/fan01/command
QoS: 1
Retain: false

{
  "request_id": "01J6RVFAN8K2",
  "action": "set",
  "values": { "power": true, "speed_percent": 50 },
  "expires_in_s": 10
}

最终验收

  • 设备断电后 1 个 Keep Alive 周期内显示 offline
  • 新客户端订阅后立即收到最后 state
  • RoamLink 账号不能发布 state
  • 设备账号不能读取其他空间
  • 旧 command 不会在重连后执行
  • 执行结果带原 request_id 回传

OFFICIAL REFERENCES

官方资料

本文按 2026-07-22 的 EMQX Cloud 与相关客户端文档整理。部署界面和功能可能更新,遇到差异时以官方文档为准。

EMQX Serverless Connection GuideEMQX Default AuthorizationEMQX Client Connection ExamplesEMQX Cloud Quotas and Limits

READY TO CONNECT

先用 MQTTX 验证链路,
再把设备加入空间。

进入控制中心