跳转至

Go-IoT 双引擎平台 — 设备端接入文档

目标读者:4G 模块固件开发工程师(嵌入式 C) 最后更新:2026-07-06


1. 概述

项目 说明
平台名称 Go-IoT 双引擎平台
接入域名 emqx.iot.devyunmu.com(EMQX MQTT Broker)
通信协议 MQTT v3.1.1
业务载荷 Modbus RTU 帧(二进制 Hex 字符串)
字节序 Big-Endian(大端序),平台使用 binary.BigEndian 解析所有多字节字段

设备与平台之间通过 MQTT 协议通信:设备上报数据使用 Publish,平台下发指令通过设备 Subscribe 接收。业务数据载荷统一使用 Modbus RTU 帧格式,与 幻特4G通信协议V0.0.1.pdf 中的帧定义一致。


2. MQTT 连接参数

2.1 连接地址

连接方式 端口 说明
TCP(非加密) 1883 内网 / 开发调试
TCP TLS/SSL 8883 推荐,生产环境必须使用
WebSocket 8083 仅限 PC 端调试工具

2.2 连接参数

参数 说明
Broker 地址 emqx.iot.devyunmu.com MQTT Broker 域名
MQTT 版本 v3.1.1 4G 模组通用
Keep Alive 60 心跳间隔
Clean Session true 每次重连后重新订阅主题
QoS 0(上行)/ 1(下行) 上行数据量大用 QoS 0,指令用 QoS 1

2.3 认证方式

开发阶段

用户名: device_{sn}    (例如: device_TEST-FIXED-001)
密码:   iot2026        (固定密码)
  • {sn} 替换为设备自身的序列号。
  • 如果 EMQX 当前部署为开放模式(未开启认证),可以不填用户名密码。但建议尽快开启正式认证。
  • 生产环境将升级为动态 Token 认证,届时另行通知。

2.4 连接流程伪代码(C 风格)

// 4G 模组 AT 指令示例(以常见的 MQTT AT 固件为例)
AT+MQTTCONN="emqx.iot.devyunmu.com",1883,"device_TEST-FIXED-001",60,1
AT+MQTTSUB="device/TEST-FIXED-001/down",1
AT+MQTTSUB="device/TEST-FIXED-001/ota",1

3. 数据上行(设备 → 平台)

3.1 发布主题

device/{sn}/up
  • {sn} 替换为设备序列号。
  • 例如:device/TEST-FIXED-001/up

3.2 载荷格式

Modbus FC3 响应帧(读保持寄存器响应),格式如下:

| NODE(1) | FUN(0x03) | ID(2) | NR_BYTE(2) | DATA(n) | CRC(2) |
字段 字节数 说明
NODE 1 固定 0x36
FUN 1 固定 0x03(FC3 读保持寄存器)
ID 2 起始寄存器地址,固定 3600(大端 0x0E10
NR_BYTE 2 数据段字节数(= 寄存器数量 × 2)
DATA n 寄存器数据(Big-Endian)
CRC 2 CRC-16/Modbus 校验值(大端)

3.3 寄存器映射表

标识信息(只读)

寄存器 长度 类型 说明
3600 3 regs (6B) ASCII 硬件版本,如 HW1.0
3603 3 regs (6B) ASCII 软件版本,如 SW2.1
3608 6 regs (12B) ASCII 设备序列号(SN),尾部 \x00 填充

运行统计

寄存器 长度 类型 说明
3614 2 regs (4B) uint32 总运行分钟(含待机)
3616 2 regs (4B) uint32 总工作分钟(不含待机)
3618 1 reg (2B) uint16 可用时间剩余(分钟)
3619 1 reg (2B) uint16 单次工作设定时间(分钟)
3620 1 reg (2B) uint16 单次工作倒计时(秒),0xFFFF = 空闲

温度数据(偏移 -40℃)

重要:所有温度寄存器使用 -40℃ 偏移编码。

  • 写入原始值 = 摄氏度 + 40
  • 读取摄氏度 = 原始值 − 40
  • 示例:85℃ → 写入 85 + 40 = 1250x007D
寄存器 长度 类型 说明
3621 1 reg (2B) int16 加热器实时温度
3623 1 reg (2B) int16 喷射启动温度
3624 1 reg (2B) int16 停止喷射温度
3625 1 reg (2B) int16 自动控制温度
3626 1 reg (2B) int16 一级高温保护阈值
3627 1 reg (2B) int16 一级高温保护恢复
3628 1 reg (2B) int16 二级高温保护阈值
3629 1 reg (2B) int16 二级高温保护恢复

流量参数

寄存器 长度 类型 说明
3630 1 reg (2B) uint16 泵最大流量(ms)
3631 1 reg (2B) uint16 泵最小流量(ms)

生产计量(共享设备 — 新增)

2026-07-06 新增:为支持共享设备"按产量扣费"业务模式,新增产量上报寄存器。 老固件可不包含此寄存器,平台向后兼容(payload 不足时默认为 0)。

寄存器 长度 类型 说明
3632 2 regs (4B) uint32 本次生产毫升数(mL),设备完成一次工作后上报
  • 上报时机:设备工作状态从"喷射(2)"变为"待机(0)"时,在当次上行帧中携带。
  • 清零时机:下次启动命令(FC5 写 1500 = 0xA5A5)后清零。
  • 取值含义:当前工作会话中累积生产的次氯酸溶液毫升数,平台据此扣费(TotalCost = ProducedML × UnitPrice)。
  • 注意:该字段使用 uint32 编码(跨 2 个寄存器,4 字节 Big-Endian)。

状态寄存器

寄存器 长度 类型 说明
3636 1 reg (2B) uint16 内部状态(位字段)
3637 1 reg (2B) uint16 告警状态(位字段)
3638 1 reg (2B) uint16 工作状态(枚举)

位字段定义

寄存器 3636(内部状态)

掩码 说明
Bit0 0x0001 加热器运行
Bit1 0x0002 泵运行
Bit2 0x0004 液位正常
Bit4 0x0010 远程锁定

寄存器 3637(告警状态)

掩码 说明
Bit2 0x0004 一级高温告警
Bit3 0x0008 二级高温告警
Bit4 0x0010 液位告警
Bit6 0x0040 需手动清洗

寄存器 3638(工作状态枚举)

含义
0 待机
1 加热
2 喷射
3 清洗
4 异常
5 故障

3.4 上报频率

寄存器组 上报时机 说明
3600~3608 开机联网成功后上报 1 次 标识信息
3614~3618 工作中每 1 分钟;待机每 10 分钟 运行统计
3619~3621 1 秒 实时倒计时、加热温度
3622~3635 60 秒 温度阈值、流量参数
3636~3638 状态变化时立即上报 内部状态、告警、工作状态

3.5 完整帧示例

以下是设备 TEST-FIXED-001 的一条完整上行帧(88 字节),包含所有寄存器(3600~3638):

36030E10004E
4857312E3000                   ← 3600 "HW1.0\x00"(硬件版本)
5357322E3100                   ← 3603 "SW2.1\x00"(软件版本)
00000000                       ← 3606 保留
544553542D46495845442D303030   ← 3608 "TEST-FIXED-000"(序列号)
00003039                       ← 3614 总运行 = 12345 分钟
00001A85                       ← 3616 总工作 = 6789 分钟
012C                           ← 3618 可用时间剩余 = 300 分钟
003C                           ← 3619 设定工作时间 = 60 分钟
0708                           ← 3620 倒计时 = 1800 秒
007D                           ← 3621 加热温度 = 85℃(raw=125)
0000                           ← 3622 保留
00C8                           ← 3623 喷射启动 = 160℃(raw=200)
008C                           ← 3624 停止喷射 = 100℃(raw=140)
00BE                           ← 3625 自动控制 = 150℃(raw=190)
00DC                           ← 3626 一级高温阈值 = 180℃(raw=220)
00C8                           ← 3627 一级高温恢复 = 160℃(raw=200)
00F0                           ← 3628 二级高温阈值 = 200℃(raw=240)
00DC                           ← 3629 二级高温恢复 = 180℃(raw=220)
0190                           ← 3630 泵最大流量 = 400 ms
0064                           ← 3631 泵最小流量 = 100 ms
...(保留区略)
0007                           ← 3636 Bit0+1+2=1 → 加热器+泵+液位正常
0000                           ← 3637 无告警
0001                           ← 3638 工作状态 = 加热
XXXX                           ← CRC(2 字节)

提示:建议将当前所有寄存器合并为一帧上报,减少 MQTT 消息数量。平台会自动识别帧长,按偏移解析各字段。

3.6 构造帧的注意事项

  1. ASCII 字段必须用 \x00 填充到指定长度。例如 SN 为 TEST-001(8 字节),需右侧补 4 字节 \x00 到 12 字节。
  2. 温度字段直接写原始值,不要写摄氏度。85℃ → 写入 1250x007D)。
  3. CRC 覆盖 NODE + FUN + ID + NR_BYTE + DATA(即 CRC 之前的所有字节),不含 CRC 自身。
  4. 所有多字节字段使用 Big-Endian。例如 uint32 值 12345 = 0x00003039,先发 0x000x000x300x39

4. 指令下行(平台 → 设备)

4.1 订阅主题

设备必须订阅以下主题以接收平台下发的控制指令:

device/{sn}/down
  • QoS 建议设为 1(至少一次送达)。
  • 重连后(Clean Session = true)必须重新订阅。

4.2 载荷格式

下行指令使用 Modbus FC5(写单个线圈)FC16(写多个保持寄存器) 帧。

FC5 帧结构(8 字节)

| NODE(1) | FUN(0x05) | REG_ID(2) | DATA(2) | CRC(2) |

FC16 帧结构(可变长度)

| NODE(1) | FUN(0x10) | START_REG(2) | NR_REGS(2) | NR_BYTE(1) | DATA(n) | CRC(2) |

4.3 指令表

FC5 控制指令

操作 寄存器 数据值 完整帧(Hex)
启动设备 1500 0xA5A5 360505DCA5A590B3
停止设备 1500 0xA55A 360505DCA55AD0F3
锁定设备 1503 0xFFA5 360505DFFFA53079
解锁设备 1503 0x0000 360505DF000051D9
重启设备 1505 0xFF02 360505E1FF024659

帧解析说明(以启动指令为例): - 36 — NODE 地址 - 05 — 功能码 FC5 - 05DC — 寄存器地址 1500(大端) - A5A5 — 启动指令数据 - 90B3 — CRC-16/Modbus

FC16 参数写入指令

操作 寄存器 示例值 完整帧(Hex)
设置工作时间 5006 60 分钟 3610138E000102003C5FE8
设置清洗时间 5007 5 秒 3610138F0001020005D53F
设置喷射温度 5008 170℃(raw=210) 3610139000010200D2AD6B
设置泵最大流量 5017 350 ms 36101399000102015E016B
设置泵最小流量 5018 80 ms 3610139A00010200506ABB
设置无液判断时间 5021 30(0.1秒) 3610139D000102001E662B

批量写入参数(FC16 连续寄存器)

可以一次性写入多个连续寄存器,减少指令条数。例如同时设置工作时间(60 分钟)、清洗时间(5 秒)、喷射温度(160℃):

3610138E000306003C000500C83CD0
字段 说明
36 NODE 固定地址
10 FUN FC16
138E START_REG 起始寄存器 5006
0003 NR_REGS 3 个寄存器
06 NR_BYTE 6 字节数据
003C DATA[0] 5006 = 60 分钟
0005 DATA[1] 5007 = 5 秒
00C8 DATA[2] 5008 = 160℃ → raw=200
3CD0 CRC CRC-16

4.4 响应要求

  • 无需通过 MQTT 单独回复确认帧
  • 设备执行指令后,应通过下一次周期上报(或立即触发上报)携带最新的状态寄存器(3636 / 3637 / 3638)来反映执行结果。
  • 例如:收到启动指令后 → 加热器启动 → 立即上报一帧,3638 = 1(加热中)、3636 Bit0 = 1

5. OTA 固件升级

5.1 订阅主题

device/{sn}/ota

5.2 载荷格式(JSON)

平台通过此主题下发 JSON 格式的 OTA 通知:

{
  "cmd": "ota",
  "version": "v1.0.1",
  "url": "https://oss.iot.devyunmu.com/firmware/firmware.bin?X-Amz-Expires=1800&device_sn=TEST-FIXED-001",
  "md5": "5d41402abc4b2a76b9719d911017c592",
  "size": 1048576
}
字段 类型 说明
cmd string 固定 "ota"
version string 固件版本号
url string 固件下载地址(MinIO 预签名 URL,有效期 30 分钟)
md5 string 固件文件 MD5 校验值(32 位小写十六进制)
size number 固件文件大小(字节)

5.3 升级流程

平台下发 OTA ──→ 设备收到 JSON
                      │
                      ▼
              从 url 下载固件
              (直连 MinIO,不经 Go 服务)
                      │
                      ▼
              下载完成后校验 MD5
              (MD5 不匹配 → 终止,等待重试)
                      │
                      ▼
              烧录新固件并重启
                      │
                      ▼
              开机上报寄存器 3603
              (携带新软件版本号)

5.4 超时与重试

  • 固件下载 URL 有效期为 30 分钟
  • 若 30 分钟内未完成升级(未收到新版本号上报),平台会重新下发 OTA 指令。

5.5 注意事项

  • 下载时应使用 Range 分片下载,以支持断点续传(4G 网络不稳定)。
  • 烧录前务必校验 MD5,防止固件损坏导致设备变砖。
  • 升级过程中设备应暂停正常的周期上报,避免占用 4G 带宽。

6. 心跳与保活

6.1 MQTT Keep Alive

  • MQTT 协议内置 Keep Alive 机制,建议设为 60 秒
  • 4G 模组通常自动发送 PINGREQ,无需应用层干预。

6.2 平台侧离线判定

平台会监控设备最后上报时间: - 若超过 3 倍上报周期未收到数据,标记设备为 offline。 - 例如:工作中应每分钟上报一次,超过 3 分钟无数据 → offline

6.3 建议

  • 设备每 60 秒至少发送一条数据。
  • 即便状态未变化,也可重复发送 3614~3618 组(运行统计)。
  • 待机时上报间隔可延长至 10 分钟,平台侧对此容差较大。

7. 测试环境验证

7.1 模拟设备上行

使用 mosquitto_pub 发送一条完整的 Modbus FC3 帧:

mosquitto_pub -h emqx.iot.devyunmu.com -p 1883 \
  -t "device/TEST-FIXED-001/up" \
  -m "36030E10004E4857312E30005357322E310000000000544553542D46495845442D3030300000303900001A85012C003C0708007D000000C8008C00BE00DC00C800F000DC019000640000000000000000000700000001000031AA"

7.2 订阅下行指令

mosquitto_sub -h emqx.iot.devyunmu.com -p 1883 \
  -t "device/TEST-FIXED-001/down"

7.3 模拟平台下发启动指令

mosquitto_pub -h emqx.iot.devyunmu.com -p 1883 \
  -t "device/TEST-FIXED-001/down" \
  -m "360505DCA5A590B3"

7.4 使用 Go 模拟器批量测试

项目 scripts/simulate_device.go 提供了完整的模拟设备工具:

WEBHOOK_URL=http://localhost:8080/mqtt/webhook go run scripts/simulate_device.go

8. 常见问题

Q1:如何获取设备序列号(SN)?

SN 由平台方统一分配,贴在设备机身二维码中。设备出厂时需烧录唯一 SN 到 Flash。扫码绑定后,平台后台将 SN 与设备关联。

Q2:Modbus CRC 如何计算?

使用标准 CRC-16/Modbus 算法: - 多项式:0x8005(反射多项式 0xA001) - 初始值:0xFFFF - 输出不取反 - CRC 在帧内以 Big-Endian 存储

C 语言实现见附录 A。

Q3:设备断线重连后需要重新订阅吗?

如果 Clean Session = true(推荐),重连后必须重新订阅 device/{sn}/downdevice/{sn}/ota

建议在 CONNACK 成功回调中执行订阅逻辑:

void on_mqtt_connected(void) {
    mqtt_subscribe("device/YOUR-SN/down", 1);
    mqtt_subscribe("device/YOUR-SN/ota", 1);
}

Q4:是否可以同时上报多个寄存器组?

可以,且推荐这样做。 将当前时间点的所有寄存器(3600~3638)合并为一帧发送,减少 MQTT 消息数量。平台会根据 NR_BYTE 长度自适应解析——寄存器不全也可以,缺失的字段将以零值处理。

Q5:倒计时寄存器 3620 为 0xFFFF 是什么意思?

0xFFFF 表示倒计时空闲(设备未在工作)。设备在工作时写入实际剩余秒数(0~3600)。此寄存器每 1 秒上报一次。

Q6:平台如何区分不同设备的上行数据?

通过 MQTT 主题 device/{sn}/up 中的 {sn} 字段区分。请勿在载荷帧中遗漏寄存器 3608(序列号),平台会交叉校验主题 SN 与帧内 SN,确保数据安全。

Q7:下行指令 QoS 为什么是 1 而不是 0?

QoS 1 保证指令至少送达一次。控制类指令(启动/停止/锁定)不能丢失。4G 模块应有去重处理(MQTT Broker 会保证 QoS 1 层面不丢,但可能重复投递)。

Q8:温度值为负数怎么编码?

温度编码使用 -40℃ 偏移,因此 -40℃ → 0,0℃ → 400x0028),可以覆盖 -40℃ 到 65535-40=65495℃ 的完整范围。


附录 A:CRC-16/Modbus C 语言实现

/**
 * crc16_modbus - 计算 CRC-16/Modbus 校验值
 *
 * 多项式: 0x8005 (反射: 0xA001)
 * 初始值: 0xFFFF
 * 输出不取反
 *
 * @param data  输入数据缓冲区
 * @param len   数据长度(字节)
 * @return      CRC-16 校验值
 *
 * 参考: MODBUS over Serial Line Specification V1.02
 */
uint16_t crc16_modbus(const uint8_t *data, size_t len)
{
    uint16_t crc = 0xFFFF;

    for (size_t i = 0; i < len; i++) {
        crc ^= (uint16_t)data[i];

        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }

    return crc;
}

使用示例

#include <stdio.h>
#include <string.h>

// 构造启动指令帧(FC5, 寄存器 1500, 值 0xA5A5)
void build_start_cmd(uint8_t *frame) {
    frame[0] = 0x36;              // NODE
    frame[1] = 0x05;              // FUN (FC5)
    frame[2] = 0x05;              // REG_ID 高字节 (1500 = 0x05DC)
    frame[3] = 0xDC;              // REG_ID 低字节
    frame[4] = 0xA5;              // DATA 高字节 (0xA5A5)
    frame[5] = 0xA5;              // DATA 低字节

    uint16_t crc = crc16_modbus(frame, 6);
    frame[6] = (uint8_t)(crc >> 8);   // CRC 高字节 (Big-Endian)
    frame[7] = (uint8_t)(crc & 0xFF); // CRC 低字节
}

int main(void) {
    uint8_t frame[8];
    build_start_cmd(frame);

    printf("Start command: ");
    for (int i = 0; i < 8; i++) {
        printf("%02X", frame[i]);
    }
    printf("\n");
    // 输出: 360505DCA5A590B3

    return 0;
}

CRC 验证

可使用以下已知帧验证 CRC 实现是否正确:

帧数据(不含 CRC) CRC 期望值 完整帧
36 05 05 DC A5 A5 0x90B3 360505DCA5A590B3
36 05 05 DC A5 5A 0xD0F3 360505DCA55AD0F3
36 05 05 DF FF A5 0x3079 360505DFFFA53079
36 05 05 E1 FF 02 0x4659 360505E1FF024659

附录 B:温度编码速查表

实际温度(℃) 寄存器原始值(Decimal) 寄存器原始值(Hex)
-40 0 0x0000
0 40 0x0028
25 65 0x0041
85 125 0x007D
100 140 0x008C
150 190 0x00BE
160 200 0x00C8
170 210 0x00D2
180 220 0x00DC
200 240 0x00F0

附录 C:寄存器速查总表

只读寄存器(上行 FC3)

寄存器 名称 类型 字节
3600 硬件版本 ASCII(6) 6
3603 软件版本 ASCII(6) 6
3608 序列号 ASCII(12) 12
3614 总运行分钟 uint32 4
3616 总工作分钟 uint32 4
3618 可用时间剩余 uint16 2
3619 设定工作时间 uint16 2
3620 倒计时 uint16 2
3621 加热器温度 int16(-40) 2
3623 喷射启动温度 int16(-40) 2
3624 停止喷射温度 int16(-40) 2
3625 自动控制温度 int16(-40) 2
3626 一级高温阈值 int16(-40) 2
3627 一级高温恢复 int16(-40) 2
3628 二级高温阈值 int16(-40) 2
3629 二级高温恢复 int16(-40) 2
3630 泵最大流量 uint16 2
3631 泵最小流量 uint16 2
3636 内部状态 uint16(bits) 2
3637 告警状态 uint16(bits) 2
3638 工作状态 uint16(0-5) 2

可写寄存器(下行指令)

寄存器 名称 功能码 示例值
1500 启动/停止 FC5 0xA5A5 启动 / 0xA55A 停止
1503 锁定/解锁 FC5 0xFFA5 锁定 / 0x0000 解锁
1505 重启 FC5 0xFF02
5006 工作时间 FC16 1~180(分钟)
5007 清洗时间 FC16 0~20(秒)
5008 喷射温度 FC16 摄氏度 + 40
5017 泵最大流量 FC16 20~500(ms)
5018 泵最小流量 FC16 20~500(ms)
5021 无液判断时间 FC16 0.1 秒为单位
5023 单价(分/mL) FC16 uint16,启动时下发
5024 最大配额(mL) FC16 uint32,启动时下发
5025 本次消费(分) FC16 uint32,结算时下发
5026 剩余余额(分) FC16 uint32,结算时下发

附录 D:共享设备业务指令(新增)

2026-07-06 新增:以下指令仅用于共享设备(次氯酸发生器)的"预充值 + 按产量扣费"业务模式。 固定设备模式下不会收到这些指令。

D.1 启动时配置帧(FC16)

平台在启动设备前,先下发业务配置帧,包含单价(寄存器 5023)和最大配额(寄存器 5024)。 设备收到后应缓存这些参数,在工作过程中实时比较累计产量与最大配额,达到后自动停机。

寄存器布局

寄存器 类型 字节 说明
5023 uint16 2 单价(分/mL),例如 1 = 0.01 元/mL
5024 uint32 4 最大可用额度(mL)

示例帧(单价 = 1 分/mL,配额 = 5000 mL):

3610139F000306000100001388XXXX

帧解析: - 36 — NODE - 10 — FC16 - 139F — 起始寄存器 5023 - 0003 — 3 个寄存器 - 06 — 6 字节数据 - 0001 — 5023 = 1(分/mL,uint16) - 00001388 — 5024 = 5000(mL,uint32) - XXXX — CRC

D.2 结算帧(FC16)

设备完成工作并上报产量后,平台扣费成功即下发结算帧,包含本次消费金额(寄存器 5025)和剩余余额(寄存器 5026)。 设备收到后应在屏幕/指示灯上显示,或通过蜂鸣器提示。

寄存器布局

寄存器 类型 字节 说明
5025 uint32 4 本次总消费(分)
5026 uint32 4 扣费后剩余余额(分)

示例帧(消费 = 500 分 = 5.00 元,余额 = 95000 分 = 950.00 元):

361013A1000408000001F400017318XXXX

帧解析: - 13A1 — 起始寄存器 5025 - 0004 — 4 个寄存器 - 08 — 8 字节数据 - 000001F4 — 5025 = 500(分,uint32) - 00017318 — 5026 = 95000(分,uint32)

D.3 完整工作流程

1. 平台下发 FC16 配置帧(5023+5024)
   → 设备缓存单价和最大配额到 RAM

2. 平台下发 FC5 启动帧(1500 = 0xA5A5)
   → 设备检查是否已收到配置帧,若未收到则忽略
   → 开始工作,累计产量实时记录

3. 工作过程中:
   → 若累计产量 >= 最大配额(5024),立即自动停机
   → 上报 FC3 帧:3632 = 产量(mL),3638 = 0(待机)

4. 平台扣费成功后下发 FC16 结算帧(5025+5026)
   → 设备在屏幕/指示灯显示消费金额和余额
   → 若 3 秒内未收到结算帧,视为已扣费(可从小程序查询)

D.4 时序要求

  • 配置帧 → 启动帧:间隔 ≤ 500ms
  • 产量上报 → 结算帧:设备应等待 ≤ 3 秒

文档版本:v1.0 维护方:Go-IoT 平台后端团队 反馈渠道:提交 Issue 至项目仓库