# 设备注册(MQTT `system/device/register/+`) 设备通过 MQTT 主题向服务端注册入网,领取正式连接凭证(uuid/secret)与订阅主题清单;用户随后用**设备识别码**绑定设备获取设备信息与面板类型。 支持两种协议: 1. **MAC 即插即用注册(推荐)**:设备上电自动上报 `mac/type/model/ver`,服务端自动入网,无需后台预登记。 2. **华为式预登记注册(旧协议,兼容)**:设备出厂预置 **SN + activate_token**,后台登记后激活。 ## 主题约定 | 主题 | 方向 | 说明 | |------|------|------| | `system/device/register/{sn}` | 设备 → 服务端 | 设备发布注册/激活请求 | | `system/device/register/{sn}/resp` | 服务端 → 设备 | 注册回执(凭证 + 订阅主题 / 错误原因) | ACL:上述主题**内置放行**(`Auth::checkAcl` 对 `system/device/register/{sn}` 发布与 `.../resp` 订阅始终放行,不依赖 ACL 开关),设备可在激活前匿名/以 SN 连接完成注册。 --- ## MAC 即插即用注册(推荐,ESP32 等设备直连入网) 设备无需后台预登记:上电后把自身信息发布到注册主题,服务端**自动创建设备**并发放凭证。 ### 主题 | 主题 | 方向 | 说明 | |------|------|------| | `system/device/register/{mac}` | 设备 → 服务端 | 注册请求(`{mac}` = 设备 MAC 去冒号小写,12 位 hex,如 `a0b1c2d3e4f5`) | | `system/device/register/{mac}/resp` | 服务端 → 设备 | 注册回执(凭证 + 订阅主题 / 错误原因) | ACL:上述主题**内置放行**,设备可在未激活前以任意 clientId(建议直接用 mac)连接并完成注册。 ### 注册消息(设备上电 publish,建议 retained) ```json { "mac": "a0b1c2d3e4f5", "type": "esp32c3_temp", "model": "KF-C3-3KANG", "ver": "1.0" } ``` | 字段 | 值 | 作用 | |------|------|------| | `mac` | 设备 MAC 去冒号小写 | 唯一身份证,服务端靠它区分设备、做绑定(须与主题中的 mac 一致,否则拒绝) | | `type` | `esp32c3_temp` | 设备类型,服务端落库为 `dev_type`,App 据此渲染面板(炕温控器/插座/灯…) | | `model` | `KF-C3-3KANG` | 产品型号,后台管理区分硬件版本 | | `ver` | `1.0` | 固件版本,服务端记录,用于版本检测/OTA 提示 | > **retained 说明**:服务端收到 publish 即处理(不依赖 retained 标志)。retained 的作用是让**之后订阅**该主题的客户端也能看到设备注册信息。回执 `.../resp` **不会 retained**(防止 secret 泄露给后来的订阅者),设备若未收到回执应延时重发注册消息(例如 5s/30s/5min 退避)。 ### 处理流程 ``` ① 设备联网:订阅 system/device/register/{mac}/resp ② 设备发布(retained):system/device/register/{mac},payload 含 mac/type/model/ver ③ 服务端:mac 首次出现 → 自动创建设备(sn=mac)并生成 uuid/secret,写入 iot_device ④ 服务端回执:{uuid, secret(仅首次), dev_type, model, ver, topics} ⑤ 设备保存凭证,改用 clientId=uuid / username=uuid / password=secret 连接 ⑥ 设备按 topics 订阅 /devices/{uuid}/#,收发状态与命令 ``` 再次上报(设备重启 / 固件升级后): - mac 已存在 → 幂等更新 `type/model/ver`,**不重置凭证**,回执不带 secret; - 固件 `ver` 变化自动同步到设备记录,后台/OTA 据此判断是否需要推送新固件。 ### 回执 payload(成功) ```json { "code": 0, "message": "ok", "data": { "id": 1, "sn": "a0b1c2d3e4f5", "mac": "a0b1c2d3e4f5", "uuid": "dev_8f3a...", "dev_type": "esp32c3_temp", "model": "KF-C3-3KANG", "ver": "1.0", "secret": "明文凭证(仅首次注册返回)", "activated": true, "bound": false, "topics": { "state": "/devices/dev_8f3a.../state", "cmd": "/devices/dev_8f3a.../cmd", "ota": "/devices/dev_8f3a.../ota", "ota_report": "/devices/dev_8f3a.../ota/report", "event": "/devices/dev_8f3a.../event" } } } ``` 失败(`code=1`)示例:`{"code":1,"message":"payload mac 与主题不一致","data":null}`、`{"code":1,"message":"mac 格式非法(应为12位十六进制,去冒号小写)","data":null}`。 ### 用户绑定(App 扫码/输 mac) ``` POST /iot/api/client/bindDevice (需登录) body: { "sn": "a0b1c2d3e4f5", "title": "炕温控器" } ``` MAC 自动注册的设备**无配对码**(扫码即绑,先到先得);如需防抢绑,可在后台设备详情为该设备补设 `pair_code`,此后绑定须携带该码。 绑定成功返回 `mac / dev_type / ver` 与 `topics`:App 用 `dev_type` 查「设备类型 → 面板」映射表渲染对应控制面板,用 `ver` 与后台最新固件比对触发 OTA 提示。 --- ## 华为式预登记注册(旧协议,兼容) ``` ① 后台预登记设备:生成 sn + pair_code(贴纸)+ activate_token(出厂预置) ② 设备联网:订阅 system/device/register/{sn}/resp ③ 设备发布:system/device/register/{sn},payload 见下 ④ 收到回执:uuid / secret / topics → 保存凭证 ⑤ 使用凭证连接 MQTT:clientId=uuid,username=uuid,password=secret ⑥ 按 topics 订阅 /devices/{uuid}/# 收发状态与命令 ``` ### 请求 payload(②③) ```json { "token": "b5d3...32位hex激活令牌" } ``` 也兼容裸 token 字符串形式(直接以 token 作为 payload 发布)。 ### 回执 payload(④) 成功(`code=0`): ```json { "code": 0, "message": "ok", "data": { "id": 1, "sn": "SN20260826", "uuid": "dev_8f3a...", "title": "智能灯泡", "model": "LAMP-01", "secret": "明文凭证(仅激活成功这一次返回)", "activated": true, "bound": false, "topics": { "state": "/devices/dev_8f3a.../state", "cmd": "/devices/dev_8f3a.../cmd", "ota": "/devices/dev_8f3a.../ota", "ota_report": "/devices/dev_8f3a.../ota/report", "event": "/devices/dev_8f3a.../event" } } } ``` 失败(`code=1`):`{ "code": 1, "message": "激活令牌无效或已失效", "data": null }` 幂等:已激活设备重复注册不报错,回执返回 `activated:true` 但 **不再下发 secret**(如需换凭证走 `resetSecret`)。 ## 用户绑定设备识别码(App 侧) 设备激活后,用户通过 App 绑定: ``` POST /iot/api/client/bindDevice (需登录) body: { "sn": "SN20260826", "pair_code": "123456", "title": "客厅灯" } ``` 绑定成功返回(含订阅主题与设备信息): ```json { "code": 1, "data": { "id": 1, "sn": "SN20260826", "uuid": "dev_8f3a...", "title": "客厅灯", "model": "LAMP-01", "room": "", "bound": true, "activated": true, "secret": "", "topics": { "state": "/devices/dev_8f3a.../state", "cmd": "/devices/dev_8f3a.../cmd", "ota": "/devices/dev_8f3a.../ota", "ota_report": "/devices/dev_8f3a.../ota/report", "event": "/devices/dev_8f3a.../event" } } } ``` > 注:插件 API 的 `Result::success` 用 `code=1` 表示成功(与框架 `code=0` 相反),解析时以 `message` 键为准。 绑定后 App 即可使用 `uuid` 连接 MQTT 推送(WebSocket `ws://{host}:8083`)或通过 REST 接口下发命令。 ## 其他相关接口 | 接口 | 说明 | |------|------| | `POST /iot/api/deviceActivate` | HTTP 方式激活(`sn` + `token`),与 MQTT 注册等效,适合无 MQTT 客户端的设备 | | `POST /iot/api/resetSecret` | 重置设备密钥(返回新明文,旧密钥作废) | | `GET /iot/api/myDevices` | 我的设备列表(主人 + 共享,含在线状态/影子) | | `POST /iot/api/sendCommand` | 向设备下发命令(写入 `/devices/{uuid}/cmd`) | | `POST /iot/api/deviceLogs` | 设备消息流水 | ## 设备侧配网(ESP32 固件 `water_temp.ino`) 现场部署**不再需要**为了改 WiFi 重烧固件(OTA 又必须先联网,过去是死锁)。固件用 `WiFiManager` 自带配网页,**v1.6.2 起表单里只有 WiFi**(服务器与端口已写死在固件里,见下节): 1. 上电先用**已保存凭据**连网(`WIFI_CONNECT_S` = 15s),连上即正常跑,不会打扰用户; 2. 连不上(首次上电 / 换路由器 / 改密码)→ 自动开配网热点 `TempMonit-Esp32`(默认密码 `12345678`,见 `WIFI_AP_PASS`); 3. 手机连热点 → 浏览器打开 `http://192.168.4.1`(Android 会自动弹配网页) → 表单**只填 WiFi** → 保存 → 设备自动重启生效; 4. 热点 180s(`WIFI_PORTAL_S`)无人操作自动关闭,回后台继续重试 —— 要重新配网请断电重上电。 ### 服务器地址:编译期写死 + 备用机自动切换(v1.6.2) | 项 | 位置 | 说明 | |---|---|---| | 服务器表 | 固件常量区 `SRV_HOSTS[]` | 按 **主 → 备** 顺序填,只填域名或 IP(表尾空串/`nullptr` 即结束) | | 端口 | `DEF_MQTT_PORT` = `1883` | **写死**,所有服务器共用(配网页已无端口输入框) | | 切换阈值 | `SRV_FAILOVER_N` = `3` | 当前这台连续失败 3 次(5s/次 ⇒ 约 15s)切下一台,走到表尾回到第 0 台,永不卡死 | | 切换条件 | 仅「网络层」失败 | `-2/-3/-4/3`(TCP 连不上/超时/被断/服务端不可用)才切;**CONNACK 1~5 是「服务器已应答、只是拒绝」(凭据/ACL 问题),换台毫无意义** | 默认表(按现场改这几行即可): ```c const char* SRV_HOSTS[SRV_MAX] = { "mqtt.ywxapp.cn", // 0 主:域名(正常走这条) "211.149.215.200", // 1 备:同机 IP 直连 —— 免 DNS(解析失败/被劫持时仍能连上) // "mqtt2.ywxapp.cn", // 2 备:真·第二台服务器(有独立机器时填它的域名或 IP) }; ``` - 串口:开机打印整张表(`[SRV] ... <- 当前`),心跳行带 `srv=`,切换时打 `[SRV] 连接连续失败:A → B(第 2/2 台)` —— 排障一眼看清在用哪台、切过没有。 - NVS 里的 `mqtt_host` / `mqtt_port`(≤v1.6.1 写过的)**一律不再读取**;遗留键不擦除(省一次 flash 写)。 - ⚠️ 换服务器 / 加备用机 = 改表后**重新编译烧录**;现场设备不需要任何配置,也就没有填错的可能。 - 注册流程同样参与切换(主服务器挂掉时,新设备也必须能注册上)。 **远程清空重配**(设备还连得上时最方便,App 用 `sendCommand` 把 payload 原样发下去即可): ```json {"action": "wifi_reset"} ``` 设备先回 `ack`,随后抹掉 WiFi 凭据并重启 → 开机连不上 → 自动进配网热点。 ⚠️ **只清 WiFi 凭据**:`uuid` / `secret` / 传感器配置全部保留(服务器写死在固件里,不受影响),不需要重新注册。 ### 清除身份 / 恢复出厂(固件 v1.3.3+,BOOT 键) ⚠️ **背景(重要)**:后台「删除设备」是**硬删** —— `DeviceService::remove()` 会清掉 `iot_device`、`iot_auth`、`iot_acl`、`iot_retain`、`iot_topic`、`iot_offline`、 分组关联 / 共享关系 / 传感器配置。**但设备端 NVS 里的 `uuid`/`secret` 不会被清**,于是: > 设备拿旧凭据去连 → `iot_auth` 里已无此用户 → CONNACK 4/5 被拒; > 而注册流程只在「`uuid` 为空」时才启动 → **设备永久爬不起来**。 > 现场表现:串口一直 `[MQTT] fail rc=4`,后台也永远不会再出现该设备。 固件 v1.3.3 起提供设备端「清除身份」入口,**现场无需拆机重烧**: | 操作 | 清除内容 | 结果 | |---|---|---| | **按住 BOOT 键 3 秒后松手** | `uuid` / `secret` / 传感器配置 | 重启 → 自动重新注册,拿全新凭证;**WiFi 与服务器地址保留,不用重新配网** | | **按住 BOOT 键 8 秒不松** | 上述 + WiFi 凭据(彻底恢复出厂) | 重启 → 进配网热点 `TempMonit-Esp32` | - **键位**:ESP32-C3 = `GPIO9`,经典 ESP32 = `GPIO0`(改固件宏 `BTN_PIN`);固件里的 `ONEWIRE_PINS`(当前 `{3,4,10}`)不要与它撞脚。 - **上电时已按住** 也生效(恢复出厂的标准姿势),无 3s 内的时机要求。 - 串口日志以 `[BTN]` / `[RST]` 开头,会在按满 3s 时提示"现在松手=清身份 / 继续按住=全清"。 - **护栏**:开机按住超 15s(`BTN_ABNORMAL_MS`)判定为引脚异常并放弃清除;运行期必须先 观察到一次「松开」才允许触发(`armed` 标志),引脚恒低的板子不会被反复恢复出厂。 **远程等价命令**(设备仍在线时最方便,发到 `/devices/{uuid}/cmd`): ```json {"action": "clear_identity"} ``` ```json {"action": "factory_reset"} ``` ⚠️ 重新注册后拿到的是**全新 uuid**,后台会新增一条设备(`member_id=0` 未绑定), 需在 App / 后台**重新绑定**;旧 `iot_device_log` / `iot_alert` 中按旧 uuid 存的历史不会自动迁移。