00 / SCOPE
RoamLink 接收 MQTT,
不接管设备协议。
你的设备可以来自 BLE、Zigbee、Wi-Fi、Modbus、CAN、PLC、Home Assistant、Node-RED 或自研程序。RoamLink 不关心它如何读取物理设备,只要求最终有一个标准 MQTT 客户端把状态发布到 EMQX,并在需要控制时订阅命令 Topic。
JK-BMS 与风扇通过 ESP32-S3 接入,是 RoamLink 自有房车空间的一套网关实现。其他用户可以完全不用 ESP32,也无需采用 ESPHome。
01 / QUICK START
最短接入路径
- 准备 Broker创建 EMQX Cloud Serverless,或者使用你自己的 EMQX;公网访问必须启用 TLS。
- 规划命名空间为账号、空间、设备准备稳定 ID,例如
acme/rv01/fan01。 - 创建两个账号设备/网关账号负责发布状态,RoamLink 账号负责订阅状态;需要控制时才允许发布命令。
- 配置最小权限 ACL只允许两个账号访问这个空间的必要 Topic,最后增加默认拒绝规则。
- 发布三类消息
state、availability,以及可选的command。 - 用 MQTTX 验证先证明 Broker、TLS、账号、ACL 和消息都正确,再配置 RoamLink。
- 在 RoamLink 映射选择界面模板,填写状态/命令 Topic,将 JSON 字段映射到组件。
02 / EMQX BROKER
准备 EMQX Broker
方案 A:EMQX Cloud Serverless
进入 EMQX Cloud 项目后创建 Serverless 部署。部署状态变为 Running 后,在 Overview 页面记录 Address,并下载 CA Certificate。Serverless 默认只开放加密连接:
mqttswss,路径 /mqttServerless 不提供未加密端口;连接时必须使用 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/+/stateacme/rv01/+/availability | acme/rv01/+/command |
acme-rv01-roamlink | RoamLink 控制端 | acme/rv01/+/command | acme/rv01/# |
EMQX Cloud 配置步骤
- 打开 Access Control → Authentication,分别创建 device 与 roamlink 用户,使用随机且唯一的强密码。
- 打开 Authorization → Username,按上表逐条添加 Allow。
- 只读设备不创建任何 command Publish/Subscribe 权限。
- 打开 All Users,最后添加 Topic
#、ActionPublish & Subscribe、PermissionDeny。 - 分别用两个账号测试:应允许的操作成功,越权 Topic 必须失败。
Serverless 当前不能直接切换授权模式。必须通过“具体账号 Allow + All Users 对 # Deny”形成白名单效果;账号规则优先于最终的 All Users 拒绝规则。
04 / TOPIC DESIGN
Topic 是长期接口,先规划再上线。
推荐使用四级资源前缀,再以消息用途结尾:
{account_id}/{space_id}/{device_id}/{message_type}
acme/rv01/battery01/state设备当前状态acme/rv01/battery01/availabilityonline/offlineacme/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 网关
使用 WiFiClientSecure 和 PubSubClient。下面展示关键结构;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 状态。
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)
D. Node-RED 网关
- 添加 mqtt out 节点,Broker 填 EMQX Address 与 8883。
- 创建 TLS 配置,启用服务器证书校验并导入 CA。
- 在 Security 中填写 device 用户名和密码,Client ID 保持唯一。
- 使用 change/function 节点把本地设备数据转换为规范 JSON。
- 发布 state 时选择 QoS 1、Retain true。
- 添加 mqtt in 订阅 command,进入 function 节点校验后再控制设备。
- 在 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。
07 / VERIFY WITH MQTTX
先验证 MQTT,再配置 RoamLink。
- 建立 device 连接使用 mqtts、8883、CA、device 凭据;发布 availability 和 state。
- 建立 roamlink 测试连接使用另一个 Client ID 和 roamlink 凭据,订阅空间前缀。
- 确认即时状态新订阅者应立即收到 retained state 和 availability。
- 确认控制测试客户端向 command 发布非 retained 命令,device 客户端应收到。
- 确认越权失败尝试发布不在 ACL 内的 state 或其他空间 Topic,Broker 必须拒绝。
- 确认离线直接断网或终止 device 进程,订阅端应收到 retained offline LWT。
EMQX Cloud 官方推荐使用 MQTTX 先验证部署连接。调试完成后,不要在截图、日志或工单中保留真实密码。
08 / MAP INTO ROAMLINK
在 RoamLink 中添加 MQTT 设备
控制中心的自助映射向导正在建设。正式流程会包含以下字段;当前房车空间已经用相同模型手工配置:
| 配置项 | 示例 |
|---|---|
| 显示名称 | 车顶换气风扇 |
| 界面模板 | 风扇:开关 + 百分比滑块 |
| State Topic | acme/rv01/fan01/state |
| Availability Topic | acme/rv01/fan01/availability |
| Command Topic | acme/rv01/fan01/command |
| 开关字段 | $.values.power |
| 实际风速 | $.values.actual_speed_percent |
| 目标风速 | $.values.target_speed_percent |
同一个空间中的多个设备通常共享一个受限 RoamLink MQTT 身份。设备配置只保存 Topic、字段映射和界面能力,不重复保存密码。
09 / RELIABILITY
让在线状态与控制结果可信
| 消息 | QoS | Retain | 原因 |
|---|---|---|---|
| state | 1 | 是 | 新打开控制中心立即获得最后状态 |
| availability | 1 | 是 | online 与 LWT offline 覆盖同一个 Topic |
| command | 1 | 否 | 避免设备重连后执行历史命令 |
| event | 0 或 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
上线前安全清单
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 与相关客户端文档整理。部署界面和功能可能更新,遇到差异时以官方文档为准。