# GBOX 热成像模组 MQTT 接口协议 v1

版本：`gbox-thermal-mqtt-v1`  
适用：热成像人数统计模组、32x24 低分辨率模组、80x62 MI48/IM148 模组，以及同机接入的 HLK-LD2454 雷达、Wi-Fi/BLE 被动探针与人数融合服务
平台入口：GBOX 平台内置 MQTT Broker，默认端口 `1883`

## 1. 连接约定

| 项 | 约定 |
| --- | --- |
| Broker | `tcp://<平台IP或域名>:1883` |
| Client ID | `thermal-{moduleSn}`，例如 `thermal-THM-A301-001` |
| 编码 | JSON 使用 UTF-8 |
| 时间 | ISO 8601，建议 UTC，例如 `2026-06-15T10:20:30.123Z` |
| QoS | 状态/事件/回复/调试日志/探针 `1`，热成像帧、雷达与人数融合实时数据 `0` |
| 保留消息 | 不使用 retain |

当前演示环境的 MQTT Broker 由平台内置 Aedes 提供。正式外网联调前，建议在安全组上限制模组出口 IP；生产版再增加 MQTT 用户密码或 TLS。

## 2. 主题规范

上行主题：

```text
gbox/v1/thermal/{moduleSn}/heartbeat
gbox/v1/thermal/{moduleSn}/status
gbox/v1/thermal/{moduleSn}/telemetry
gbox/v1/thermal/{moduleSn}/frame
gbox/v1/thermal/{moduleSn}/radar
gbox/v1/thermal/{moduleSn}/probe
gbox/v1/thermal/{moduleSn}/occupancy
gbox/v1/thermal/{moduleSn}/debug
gbox/v1/thermal/{moduleSn}/event
gbox/v1/thermal/{moduleSn}/reply
```

下行主题：

```text
gbox/v1/thermal/{moduleSn}/command
```

`moduleSn` 必须稳定唯一，不要使用随机 MAC 后缀反复变化。推荐格式：`THM-{房间号}-{序号}`，例如 `THM-A301-001`。

## 3. 心跳 heartbeat

频率：默认 30 秒一次，离线阈值建议 90 秒。

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "roomId": "room-a-0301",
  "suiteSn": "GBOX-A301-1000",
  "ts": "2026-06-15T10:20:30.123Z",
  "status": "online",
  "model": "MLX90640-32x24",
  "firmware": "1.0.3",
  "algorithm": "people-count-0.2.0",
  "width": 32,
  "height": 24,
  "signalQuality": 92,
  "batteryLevel": null,
  "ip": "192.168.1.51",
  "mac": "AA:BB:CC:01:02:03"
}
```

## 4. 状态 status

模组启动、配置变更、异常恢复时上报。

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "roomId": "room-a-0301",
  "ts": "2026-06-15T10:20:35.000Z",
  "status": "online",
  "streamStatus": "running",
  "width": 32,
  "height": 24,
  "frameIntervalMs": 1000,
  "supportedFormats": ["matrix_centi_c"],
  "lastError": null
}
```

## 5. 遥测 telemetry

只上传算法结果，不上传完整帧。适合低带宽常态运行。

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "roomId": "room-a-0301",
  "suiteSn": "GBOX-A301-1000",
  "ts": "2026-06-15T10:20:36.000Z",
  "seq": 1024,
  "metrics": {
    "humanCount": 2,
    "confidence": 0.91,
    "min": 25.8,
    "max": 32.4,
    "avg": 27.6,
    "hotspot": { "x": 15, "y": 11 }
  },
  "diagnostics": {
    "backgroundReady": true,
    "motionScore": 0.63,
    "lowConfidenceReason": null
  }
}
```

平台会把 `metrics.humanCount` 映射到房间热成像人数读数，供态势、告警和工单流程复用。

## 6. 帧数据 frame

默认调测帧率：`1 fps`。演示/调测时可发完整矩阵；生产常态建议只发 `telemetry`，必要时按命令临时开启 `frame`。

推荐格式：`matrix_centi_c`，即每个像素用摄氏度乘以 100 的整数表示。例如 `2756` 表示 `27.56°C`。

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "roomId": "room-a-0301",
  "suiteSn": "GBOX-A301-1000",
  "ts": "2026-06-15T10:20:37.000Z",
  "frame": {
    "seq": 1025,
    "width": 32,
    "height": 24,
    "format": "matrix_centi_c",
    "unit": "centi_c",
    "values": [2680, 2682, 2691, 2700, 2712, 2721]
  },
  "metrics": {
    "humanCount": 2,
    "confidence": 0.89,
    "min": 25.7,
    "max": 33.1,
    "avg": 27.8,
    "hotspot": { "x": 16, "y": 10 }
  }
}
```

实际 `values` 长度必须等于 `width * height`：

| 分辨率 | 长度 |
| --- | ---: |
| 32x24 | 768 |
| 80x62 | 4960 |

可选格式：

| format | 说明 |
| --- | --- |
| `matrix_centi_c` | 推荐。JSON 整数数组，单位 0.01°C |
| `matrix_float_c` | JSON 浮点数组，单位 °C |
| `gfra_u16` | GFRA 解析后的 U16 矩阵，平台按调测能力逐步扩展 |
| `text_32x24` | 兼容 Thermal AI Workstation 的 32x24 文本帧 |

### 6.1 平台侧存储策略

平台默认不再把所有 MQTT 帧报文和完整热成像矩阵长期写入业务库，采用“实时状态 + 近端缓存 + 时序摘要 + 事件留痕”的分层策略：

| 数据类型 | 默认保留 | 说明 |
| --- | --- | --- |
| 最新帧 | 每个模组 1 份 | 用于实时热力图、AI 当前状态问答和 Device Agent 当前状态读取 |
| 近端帧缓存 | 24 小时，默认每秒 1 帧 | 写入 `thermal_frames`，默认只保存摘要指标，不保存完整矩阵 |
| 房间热成像读数 | 24 小时，默认每秒 1 条 | 写入 `sensor_readings`，用于现有态势、告警和短周期排查 |
| MQTT 帧日志 | 元数据化，7 天 | `thermal_mqtt_messages` 默认只保留序号、尺寸、指标、原始报文字节数等诊断信息 |
| 串口调试日志 | 7 天，每模组最多 10000 行 | 写入 `thermal_debug_logs`，按会话、级别、TAG 和关键字查询 |
| 探针聚合样本 | 7 天，每模组最多 10080 条 | Wi-Fi/BLE 活跃估计、唯一数、观测数和质量，写入 `thermal_probe_samples` |
| 探针详细标识 | 最多 24 小时 | 仅在显式开启时保存 MAC、SSID、BLE 名称和广播载荷；平台启动时及之后每分钟物理清空到期明细，即使模组离线也不延长留存，聚合样本继续保留 |
| 融合人数样本 | 7 天，每模组最多 10080 条 | 写入 `thermal_occupancy_samples`，包含人数后验、P90、冲突标记和匿名热目标标签 |
| 事件/告警 | 长期保留 | 模组 `event`、平台告警和工单继续进入事件体系 |
| TSDB 摘要 | 可配置开启 | 开启后写入 `thermal_frame_summary`，用于趋势分析和报表 |

相关环境变量：

```bash
THERMAL_REALTIME_RETENTION_HOURS=24
THERMAL_FRAME_MIN_INTERVAL_MS=1000
THERMAL_FRAME_MAX_PER_MODULE=86400
THERMAL_RECENT_FRAME_VALUES_MODE=summary_only
THERMAL_MQTT_FRAME_LOG_MODE=metadata
THERMAL_MQTT_LOG_RETENTION_DAYS=7
THERMAL_MQTT_LOG_KEEP_PER_MODULE=300
THERMAL_DEBUG_LOG_RETENTION_DAYS=7
THERMAL_DEBUG_LOG_KEEP_PER_MODULE=10000
THERMAL_DEBUG_LOG_MAX_BATCH_LINES=50
THERMAL_DEBUG_LOG_MAX_MESSAGE_CHARS=1024
THERMAL_PROBE_SAMPLE_MAX_PER_MODULE=10080
THERMAL_PROBE_RETENTION_HOURS=168
THERMAL_PROBE_IDENTITY_RETENTION_HOURS=24
THERMAL_OCCUPANCY_SAMPLE_MAX_PER_MODULE=10080
THERMAL_OCCUPANCY_RETENTION_HOURS=168
THERMAL_TSDB_WRITE_ENABLED=false
THERMAL_TSDB_MEASUREMENT=thermal_frame_summary
```

需要调试算法时，可以临时把 `THERMAL_RECENT_FRAME_VALUES_MODE` 设为 `full`，但不建议在云端生产环境长期启用。

### 6.2 Wi-Fi/BLE 探针 probe

固件使用当前 Wi-Fi 信道的管理帧被动观测和 NimBLE 被动扫描。默认只用每窗口独立 SipHash 做去重，平台看不到可跨窗口关联的设备标识。V3.0.7 起即使不开启原始标识，也会上传窗口内匿名 RSSI 分位统计，供空房基线和动态门限计算使用。平台显式开启 `identityUploadEnabled` 后，才会同时上送 Wi-Fi MAC/SSID/SSID 原始十六进制和 BLE 地址/名称/广播载荷；每种无线最多上送 RSSI 最强的 12 条明细。

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "ts": "2026-07-19T10:20:37.000Z",
  "seq": 21,
  "configVersion": 4,
  "privacy": {
    "mode": "raw_identifiers_opt_in",
    "rawIdentifiersUploaded": true,
    "crossWindowLinking": true
  },
  "window": { "id": 7, "durationSec": 900, "activeSec": 60, "reportIntervalSec": 60 },
  "pausedForOta": false,
  "wifi": {
    "enabled": true, "active": true, "channel": 6,
    "observations": 42, "unique": 3, "activeEstimate": 2, "quality": 0.56,
    "rssiBuckets": {"near": 1, "mid": 1, "far": 1},
    "rssiStats": {"samples": 42, "min": -88, "max": -47, "p50": -71, "p90": -55, "p95": -51},
    "devices": [{
      "mac": "02:11:22:33:44:55", "ssid": "Guest-WiFi",
      "ssidHex": "47756573742d57694669", "rssi": -49,
      "observations": 11, "lastSeenAgoMs": 300, "randomized": true
    }]
  },
  "ble": {
    "enabled": true, "active": true,
    "observations": 30, "unique": 4, "activeEstimate": 2, "quality": 0.48,
    "rssiBuckets": {"near": 1, "mid": 2, "far": 1},
    "rssiStats": {"samples": 30, "min": -91, "max": -58, "p50": -76, "p90": -64, "p95": -61},
    "devices": [{
      "address": "DA:7A:01:02:03:04", "addressType": 1,
      "name": "GuestPhone", "payloadHex": "0201060b09477565737450686f6e65",
      "payloadTruncated": false, "rssi": -61,
      "observations": 7, "lastSeenAgoMs": 500, "randomized": true
    }]
  }
}
```

实时人数融合不读取 MAC、SSID、名称或广播载荷，只读取匿名聚合量。真实场景闭环测试还会读取 `rssiStats`：先以空房期 P95 建立环境底噪，再加可配置裕量形成动态门限；混合策略取固定门限与动态门限中较严格者。RSSI 只用于增强或质疑“有人”证据，不能直接换算人数。平台通用 MQTT 日志也只保存探针摘要，不复制详细标识。明细可通过 `DELETE /api/thermal/modules/{moduleSn}/probe-identifiers` 立即清除；平台全局清理器每分钟物理清除到期明细，不依赖对应模组产生新消息。

### 6.3 多模态人数 occupancy

热成像是主证据，LD2454 用于有人/目标数和运动一致性校验，Wi-Fi/BLE 只作为弱证据。固件每秒在 `0、1、2、3、4、5、overflow` 七个状态上运行时序后验；无线探针单独存在时不能把空房翻转为有人。

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "ts": "2026-07-19T10:20:38.000Z",
  "seq": 100,
  "configVersion": 4,
  "fusion": {
    "enabled": true, "status": "ready", "count": 2,
    "overflow": false, "confidence": 0.87,
    "p90": { "lower": 2, "upper": 3 }, "conflictFlags": 0,
    "posterior": [0, 0.02, 0.87, 0.10, 0.01, 0, 0]
  },
  "thermal": {
    "backgroundReady": true, "backgroundSamples": 24,
    "backgroundTargetSamples": 24, "count": 2, "quality": 0.92,
    "foregroundPixels": 38, "componentCount": 2,
    "labels": [{ "trackId": 101, "cx": 8.5, "cy": 11, "bbox": [6, 7, 11, 16], "area": 17, "score": 0.88 }]
  },
  "radar": { "online": true, "count": 2, "quality": 0.78 },
  "probe": { "wifiActiveEstimate": 2, "bleActiveEstimate": 2, "quality": 0.44 },
  "hour": { "lastStableCount": 2, "modeCount": 2, "maxCount": 3, "occupancyMinutes": 47, "samples": 60 }
}
```

平台最新值接口为 `latest-probe` 和 `latest-occupancy`，历史接口为 `probe-samples` 和 `occupancy-samples`。融合人数同时以来源 `thermal_multimodal_fusion` 镜像到现有房间 `sensor_readings`，供态势和规则引擎复用。

## 7. 事件 event

用于模组侧主动报告异常。

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "roomId": "room-a-0301",
  "ts": "2026-06-15T10:21:00.000Z",
  "eventType": "low_confidence",
  "level": "warning",
  "message": "背景模型未稳定，人数置信度偏低",
  "details": {
    "confidence": 0.52,
    "backgroundReady": false
  }
}
```

## 8. 平台命令 command

模组订阅：

```text
gbox/v1/thermal/{moduleSn}/command
```

命令载荷：

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "commandId": "6f2d5e5e-1ab2-4da7-91b0-8d4f1a8db5c8",
  "ts": "2026-06-15T10:22:00.000Z",
  "type": "capture_once",
  "params": {},
  "expectReply": true
}
```

命令类型：

| type | params | 模组行为 |
| --- | --- | --- |
| `get_status` | `{}` | 立即回复当前状态 |
| `get_version` | `{}` | 读取应用固件、ESP-IDF、板卡基线、芯片 Revision、Flash/PSRAM、传感器型号及设备标识 |
| `get_diagnostics` | `{}` | 读取 Boot/复位、Wi-Fi、MQTT、OTA、Flash Core Dump 和任务栈余量诊断 |
| `get_config` | `{}` | 读取 `deviceUid/moduleSn/assetCode/roomId/suiteSn/configVersion/identityLocked` |
| `set_config` | `{"expectedConfigVersion":7,"moduleSn":"THM-A306-001","assetCode":"ASSET-A306-001","roomId":"room-a306","suiteSn":"GBOX-A306-1000","confirmIdentityChange":true,"rebootAfterApply":true}` | 使用乐观锁原子写入配置；变更 `moduleSn` 必须确认并重启 |
| `capture_once` | `{}` | 采集并上报一帧 `frame` |
| `set_stream` | `{"enabled":true,"intervalMs":1000}` | 开启/停止连续帧上报 |
| `set_interval` | `{"intervalMs":1000}` | 设置连续帧间隔 |
| `raw_trigger_hex` | `{"hex":"A5 5A 01"}` | 向传感器底层发送触发 Hex 命令 |
| `identify` | `{}` | 指示灯/蜂鸣器提示现场定位 |
| `reboot` | `{}` | 模组重启 |
| `ota_check` | `{}` | 查询当前固件版本、OTA 状态和启动确认状态 |
| `ota_download` | `{"url":"https://.../firmware.bin","version":"2.25.0","sha256":"...","force":false}` | 通过 HTTPS 下载到非活动槽，核对镜像版本和 SHA-256，成功后重启 |
| `ota_abort` | `{}` | 请求中止当前 OTA 下载 |
| `ota_confirm` | `{}` | 在传感器和 MQTT 自检均通过时手工确认新固件；正常情况下会自动确认 |
| `radar_get_status` | `{}` | 读取 LD2454 在线态、目标数和 UART 统计 |
| `radar_get_version` | `{}` | 通过 LD2454 `0x00A0` 命令读取雷达固件版本 |
| `radar_get_config` | `{}` | 读取跟踪模式、波特率和 MQTT 上报间隔 |
| `radar_set_tracking_mode` | `{"mode":"multi"}` | 设置 LD2454 单目标或多目标跟踪 |
| `radar_set_report_interval` | `{"intervalMs":500}` | 设置 ESP 聚合上报雷达目标的周期 |
| `radar_set_baud` | `{"baud":256000}` | 设置 LD2454 波特率并同步 ESP UART |
| `radar_reboot` | `{}` | 重启 LD2454，不重启整机 ESP32-S3 |
| `debug_log_start` | `{"level":"info","durationSec":300,"batchLines":10}` | 启动限时串口日志采集；平台自动补充唯一 `sessionId` |
| `debug_log_stop` | `{"sessionId":"debug-..."}` | 停止当前或指定会话的日志采集 |
| `debug_log_snapshot` | `{"sessionId":"debug-...","limit":200}` | 立即上传设备日志环形缓冲中的最近日志 |
| `debug_log_clear` | `{"sessionId":"debug-..."}` | 清除设备端当前或指定会话的日志缓冲 |
| `probe_get_status` | `{}` | 读取 Wi-Fi/BLE 探针运行、窗口和累计统计 |
| `probe_get_config` | `{}` | 读取完整探针与融合配置 |
| `probe_set_config` | `{"wifiEnabled":true,"bleEnabled":true,"identityUploadEnabled":false,"windowSec":900,"activeSec":60,"reportIntervalSec":60,"wifiRssiMin":-82,"bleRssiMin":-85,"bleScanIntervalMs":1000,"bleScanWindowMs":100}` | 原子更新并持久化探针配置 |
| `fusion_get_status` | `{}` | 读取当前融合人数、后验、背景与冲突状态 |
| `fusion_get_config` | `{}` | 读取完整探针与融合配置 |
| `fusion_set_config` | `{"enabled":true,"thermalWeight":70,"radarWeight":25,"probeWeight":5,"backgroundSamples":24,"foregroundDeltaCentiC":180,"minBlobPixels":4,"personBlobPixels":18}` | 原子更新并持久化融合参数 |
| `fusion_reset_background` | `{}` | 清空热背景模型；必须在空房、热环境稳定时执行 |

LD2454 的专用字段、串口符号位解码、供电差异和天线罩约束见 `LD2454雷达接入MQTT协议与硬件联调说明_20260718.md`。

`ota_download` 的 `url` 必须使用 HTTPS，`version` 必须与固件镜像内的版本一致，`sha256` 必须是待下载 OTA `.bin` 文件的 64 位十六进制 SHA-256。升级期间暂停完整热成像帧上报，但状态和 OTA 进度仍通过 `status` 主题上报。新固件启动后必须在配置的确认窗口内取得一帧有效热成像数据、连接 MQTT 并连续稳定 30 秒，之后才标记有效；此前崩溃、重启或超时由现有 A/B OTA 启动链自动回滚上一应用槽。

支持版本会在 `heartbeat.diagnostics`、`get_status.data.diagnostics`、`get_version.data.diagnostics` 及 `get_diagnostics.data` 中携带同一诊断对象。平台把最新值写入模组清单，并按 Boot、Wi-Fi、MQTT、OTA 和 Core Dump 状态签名去重保留最近 100 条历史。诊断 REST 接口为 `GET /api/thermal/modules/{moduleSn}/diagnostics?limit=20`。本次 OTA 只替换应用槽，不更新 bootloader 和分区表。

`get_version` 回复中的 `hardware.boardVersion` 来自固件构建配置，代表已经由硬件/BOM 负责人确认并写入构建参数的板卡基线；ESP32-S3 本身不能自动识别未写入 eFuse、EEPROM 或 GPIO 编码的 PCB 丝印版本。`hardware.chipRevision` 则由芯片在运行时真实读取。平台会同时展示这两个字段，不能用芯片 Revision 替代 PCB/BOM 版本。

### 8.1 平台设备身份与资产绑定

| 方法 | REST 路径 | 作用 |
| --- | --- | --- |
| `POST` | `/api/thermal/modules/{moduleSn}/identity-bindings/read` | 生成审计操作并向设备下发 `get_config` |
| `GET` | `/api/thermal/identity-bindings/{operationId}` | 推进读取、重启检测和新编号回读验证 |
| `POST` | `/api/thermal/identity-bindings/{operationId}/apply` | 核对 MAC、目标编号、重复资产和操作冲突后下发 `set_config` |
| `GET` | `/api/thermal/modules/{moduleSn}/identity-bindings` | 查询原值、新值、操作人、时间、回读值和失败原因 |

`apply` 必须携带 `confirmReboot=true`。变更 `moduleSn` 时还必须携带与目标编号完全一致的 `moduleSnConfirmation`。“替换已有设备”只允许替换离线目标，新硬件以目标编号上线后，平台必须再核对原设备 MAC/deviceUid 和递增后的 `configVersion`才完成操作。后续 OTA 以验证后的新 `moduleSn` 为唯一目标。

平台 REST 下发示例：

```http
POST /api/thermal/modules/THM-A305-001/command
Content-Type: application/json

{
  "type": "ota_download",
  "params": {
    "url": "https://firmware.example.com/Thermal-ESP32S3_MQTT-OTA_V2.25.0_ota.bin",
    "version": "2.25.0",
    "sha256": "dd6b8eaae1ab7ff4128f39a7dcf8bdba01474960dcff03bbe3aaf42d5c0d3fa4",
    "force": false
  },
  "expectReply": true
}
```

OTA 进度状态示例：

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A305-001",
  "status": "online",
  "ota": {
    "state": "downloading",
    "currentVersion": "2.24.0",
    "targetVersion": "2.25.0",
    "progress": 55,
    "bytesReceived": 882649,
    "totalBytes": 1604816,
    "pendingVerify": false,
    "detail": "writing_inactive_slot"
  }
}
```

### 8.1 平台固件仓库与 OTA 任务

主控台“热成像模组 OTA 升级”面板提供完整操作链路：

1. 选择 `*_ota.bin` 并上传。
2. 服务端检查 ESP image magic 和固定偏移处的 `esp_app_desc`，从镜像内读取版本。
3. 上传时流式计算 SHA-256，并限制文件大小不得超过 `ota_0` / `ota_1` 单槽 `0x380000` 字节。
4. 固件保存到平台持久化目录，设备从 `https://aiot.deepglint.com/firmware/thermal/{firmwareId}/{filename}` 下载。
5. 选择模组和固件后，平台创建 `thermal_ota_tasks` 记录，以同一 UUID 作为 `task.id` 和 `commandId`。
6. 模组的 `reply` 依 `commandId` 关联；`status.ota` 依模组 SN 和目标版本关联，进度页面实时显示下载字节、百分比、重启、新固件确认或回滚结果。

> `full-flash.bin` 是串口恢复镜像，不是 OTA app 镜像。平台会因为固定偏移处没有 `esp_app_desc` 而拒绝该文件，防止误下发。

上传接口（需 `thermal:write` 权限和平台会话）：

```http
POST /api/thermal/firmware
Content-Type: application/octet-stream
X-Firmware-Filename: Thermal-ESP32S3_MQTT-OTA_V2.25.2_ota.bin
X-Firmware-Version: 2.25.2
X-Firmware-Notes: MQTT OTA 联调版

<raw .bin bytes>
```

固件与任务接口：

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/api/thermal/firmware` | 固件仓库列表，包含版本、字节数、SHA-256 和 HTTPS 地址 |
| `POST` | `/api/thermal/firmware` | 流式上传并校验 OTA app 镜像 |
| `DELETE` | `/api/thermal/firmware/{firmwareId}` | 仅平台管理员可删除未被 OTA 任务引用的固件及文件 |
| `POST` | `/api/thermal/modules/{moduleSn}/ota` | 使用 `firmwareId` 创建任务并下发 `ota_download` |
| `GET` | `/api/thermal/ota-tasks?moduleSn={moduleSn}` | 查询指定模组 OTA 进度和历史 |
| `GET` | `/api/thermal/ota-tasks/{taskId}` | 查询单个 OTA 任务 |
| `POST` | `/api/thermal/ota-tasks/{taskId}/abort` | 下发 `ota_abort` 并将任务置为中止中 |
| `GET` | `/firmware/thermal/{firmwareId}/{filename}` | ESP32-S3 不带会话的 HTTPS 固件下载路由 |

当前云主机通过标准 HTTPS 端口 443 提供 OTA 下载，生产下发地址形如
`https://aiot.deepglint.com/firmware/thermal/...`。平台内部
`127.0.0.1:3000` 仅供 Nginx 反向代理，不作为设备公网 OTA 端口。

平台任务状态链：

```text
queued -> sent -> accepted -> connecting -> downloading -> verifying
       -> rebooting -> pending_verify -> completed

失败分支: failed / aborted / rolling_back -> rolled_back / confirm_failed
```

### 8.2 远程串口调试日志

设备在收到 `debug_log_start` 或 `debug_log_snapshot` 后，通过以下主题上报，QoS 必须为 `1`，不得 retain：

```text
gbox/v1/thermal/{moduleSn}/debug
```

批量上报示例：

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "sessionId": "debug-2f42ceae-9c1d-44c7-8d4b-57a1e9901bf1",
  "batchId": "debug-2f42ceae-9c1d-44c7-8d4b-57a1e9901bf1:18",
  "seq": 18,
  "source": "serial",
  "dropped": 2,
  "ts": "2026-07-19T04:22:31.451Z",
  "lines": [
    {
      "ts": "2026-07-19T04:22:31.402Z",
      "level": "I",
      "tag": "MQTT_THERMAL",
      "message": "debug log batch published"
    },
    {
      "ts": "2026-07-19T04:22:31.447Z",
      "level": "E",
      "tag": "OTA_MANAGER",
      "message": "worker create failed: free=4259 largest=1664"
    }
  ]
}
```

字段约定：

| 字段 | 必填 | 约定 |
| --- | --- | --- |
| `sessionId` | 是 | 必须原样使用平台 `debug_log_start` 命令中的会话 ID |
| `batchId` | 是 | 同一批重发时保持不变；平台以 `moduleSn + batchId + lines 数组下标` 去重 |
| `seq` | 建议 | 会话内递增批次号，便于定位漏批和乱序 |
| `source` | 是 | 串口调试固定为 `serial`，后续可扩展 `runtime` |
| `dropped` | 建议 | 自上个批次以来设备环形缓冲丢弃的行数 |
| `lines` | 是 | 每批最多 50 行，每行消息默认最多 1024 字符 |
| `lines[].level` | 是 | 支持 `V/D/I/W/E/F` 或 `verbose/debug/info/warning/error/fatal` |
| `lines[].tag` | 建议 | ESP-IDF 日志 TAG，例如 `OTA_MANAGER`、`MQTT_THERMAL` |
| `lines[].ts` | 建议 | 设备日志时间；平台另存独立的接收时间 `createdAt` |

端侧实现必须使用固定大小环形缓冲和独立低优先级上传任务。MQTT 命令回调只解析、校验并投递控制消息，不得在回调栈中创建日志工作任务或同步批量上传；OTA 期间应降低日志发送频率，避免再次挤压 `mqttCommand` 栈和内部堆。设备重试同一批时不得生成新的 `batchId`。

平台接口：

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/api/thermal/modules/{moduleSn}/debug-logs` | 查询日志，支持 `level`、`search`、`sessionId`、`limit` |
| `GET` | `/api/thermal/modules/{moduleSn}/debug-logs/export` | 按当前筛选条件导出 UTF-8 `.log` 文件 |
| `DELETE` | `/api/thermal/modules/{moduleSn}/debug-logs` | 删除平台日志；可选 `sessionId`，需 `thermal:write` 权限 |

严重栈溢出、看门狗复位或供电掉电可能发生在 MQTT 上报前，因此现场首轮定位仍需同时连接物理 UART；平台日志用于复现、远程协同和跨版本留痕，不能替代硬崩溃时的串口捕获。

## 9. 命令回复 reply

模组发布：

```text
gbox/v1/thermal/{moduleSn}/reply
```

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "commandId": "6f2d5e5e-1ab2-4da7-91b0-8d4f1a8db5c8",
  "ts": "2026-06-15T10:22:00.300Z",
  "ok": true,
  "type": "capture_once",
  "message": "accepted",
  "data": {
    "nextFrameSeq": 1026
  }
}
```

版本查询回复示例：

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "commandId": "d22d7f64-38d2-4fb5-b85d-c216843b8230",
  "ts": "2026-07-18T10:22:00.300Z",
  "ok": true,
  "type": "get_version",
  "message": "ok",
  "data": {
    "software": {
      "version": "2.25.3",
      "project": "senxorESP32S3",
      "idfVersion": "v5.2.3",
      "buildDate": "Jul 18 2026",
      "buildTime": "10:20:11",
      "secureVersion": 0
    },
    "hardware": {
      "boardModel": "GBOX-Thermal-ESP32S3-MLX90640",
      "boardVersion": "Demo1-R1",
      "boardVersionSource": "build_config",
      "sensorModel": "MLX90640",
      "sensorWidth": 32,
      "sensorHeight": 24,
      "chipModel": "ESP32-S3",
      "chipRevision": 0,
      "cpuCores": 2,
      "flashBytes": 16777216,
      "psramBytes": 8388608
    },
    "identity": {
      "moduleSn": "THM-A301-001",
      "deviceUid": "...",
      "assetCode": "...",
      "mac": "AA:BB:CC:DD:EE:FF"
    }
  }
}
```

失败示例：

```json
{
  "protocol": "gbox-thermal-mqtt-v1",
  "moduleSn": "THM-A301-001",
  "commandId": "6f2d5e5e-1ab2-4da7-91b0-8d4f1a8db5c8",
  "ts": "2026-06-15T10:22:00.300Z",
  "ok": false,
  "type": "raw_trigger_hex",
  "error": "invalid_hex"
}
```

## 10. 兼容旧平台传感器主题

旧主题仍可用于只上报人数：

```text
iot/room/{roomId}/status
```

```json
{
  "suite_sn": "GBOX-A301-1000",
  "timestamp": "2026-06-15 10:20:30",
  "signal_quality": 92,
  "sensors": {
    "thermal": { "count": 2, "confidence": 0.91 }
  }
}
```

新模组优先使用 `gbox/v1/thermal/...`。平台会将新协议中的 `humanCount` 同步到原有房间热成像读数。

## 11. 联调命令示例

安装 MQTT 命令行工具后，可用下面的命令模拟模组。当前联调平台部署在百度云 `106.12.156.131`，命令优先使用域名 `aiot.deepglint.com`。

心跳：

```bash
mosquitto_pub -h aiot.deepglint.com -p 1883 \
  -i thermal-THM-A301-001 \
  -t gbox/v1/thermal/THM-A301-001/heartbeat \
  -q 1 \
  -m '{"protocol":"gbox-thermal-mqtt-v1","moduleSn":"THM-A301-001","roomId":"room-a-0301","ts":"2026-06-15T10:20:30.123Z","status":"online","model":"MLX90640-32x24","firmware":"1.0.3","width":32,"height":24,"signalQuality":92}'
```

订阅平台命令：

```bash
mosquitto_sub -h aiot.deepglint.com -p 1883 \
  -i thermal-THM-A301-001-debug \
  -t gbox/v1/thermal/THM-A301-001/command -v
```

发布一帧调测帧：

```bash
mosquitto_pub -h aiot.deepglint.com -p 1883 \
  -i thermal-THM-A301-001 \
  -t gbox/v1/thermal/THM-A301-001/frame \
  -m '{"protocol":"gbox-thermal-mqtt-v1","moduleSn":"THM-A301-001","roomId":"room-a-0301","ts":"2026-06-15T10:20:37.000Z","frame":{"seq":1,"width":4,"height":3,"format":"matrix_centi_c","unit":"centi_c","values":[2680,2700,2710,2690,2695,2850,2860,2705,2682,2690,2700,2688]},"metrics":{"humanCount":1,"confidence":0.86,"min":26.8,"max":28.6,"avg":27.2,"hotspot":{"x":2,"y":1}}}'
```

发布一批串口日志：

```bash
mosquitto_pub -h aiot.deepglint.com -p 1883 \
  -i thermal-THM-A301-001 \
  -t gbox/v1/thermal/THM-A301-001/debug \
  -q 1 \
  -m '{"protocol":"gbox-thermal-mqtt-v1","moduleSn":"THM-A301-001","sessionId":"debug-local-test","batchId":"debug-local-test:1","seq":1,"source":"serial","dropped":0,"ts":"2026-07-19T04:22:31.451Z","lines":[{"level":"I","tag":"MQTT_THERMAL","message":"debug batch test"}]}'
```

## 12. 接入验收

1. 平台“热成像模组”页面能看到模组上线。
2. 心跳 `lastSeenAt` 持续刷新。
3. 平台下发 `get_status` 后，模组在 `reply` 主题回复 `ok=true`。
4. 平台下发 `capture_once` 后，模组发布一条 `frame`。
5. 最新帧热力图可显示，尺寸、人数、置信度、温度范围合理。
6. 绑定房间后，态势总览中的热成像人数读数同步变化。
7. 异常/低置信度时，模组发布 `event`，平台 MQTT 日志能看到报文。
8. 平台下发 `debug_log_start` 后，模组在 `reply` 主题确认并使用命令中的 `sessionId` 上报日志。
9. 相同 `batchId` 的 QoS 1 重发不会在平台生成重复日志。
10. 工作站可实时显示、筛选、导出和清理平台日志，且同时展示设备时间与平台接收时间。
11. 串口日志开启时，热成像、雷达、MQTT 心跳和 OTA 不得因日志上传发生阻塞或栈溢出。
