Module Document

15.05_IoT命令接入指引-APP端

15.05_IoT命令接入指引-APP端

15.05 IoT 命令接入指引-APP端

> 版本 v2.0 · 2026-09-14 · 交付对象:手机 App / Web 客户端团队与端侧 AI Agent · 口径=最终目标态契约(历史裁决与迁移过程见附录 C)

> 参照实现:C 端 Web infimaker_user_fe(timelapse/api.ts、devices/iotApi.ts、commandCards.ts)与验证控制台/远程加工仓 infimaker_admin_fe(timelapseVerify.ts、deviceCloud.ts、DeviceCloud/catalog.ts、commandCards.ts)

> 本册自包含:单独读本册即可完成 IoT 命令面接入——架构、全部指令、每条指令的接入配方(端点/鉴权 lane/前置条件/请求 JSON/成功与错误响应 JSON)、命令全生命周期状态机、WSS 协议(含 message.log/command.lifecycle 观测帧与 watch 声明)、HMS/Status/State 字段字典均已内联,无需翻阅其他仓库或文档。所有契约标注源码出处(附录 A)。

> 命名总则(D6 全协议终态):HTTP REST / WSS 帧 / MQTT 命令信封的键一律 snake_case;int64 值发 proto3 规范十进制字符串(number 形态亦接受);服务端上行解析只认 snake(camel 违约帧 fail-fast 落 parse_error,可观测不静默)。记录在案的 camel 豁免域:timelapse 媒体载荷(设备 MQTT 命令 payload 内层与 timelapse/* 上行帧,见 §3 各处标注)与加工账本详情嵌套段(见 15.03 §3.1)。

---

0. 一页速览

判定项 结论
命令下发端点(两条 lane) C 端 POST /api/v1/commands/{sn}(用户 JWT);admin POST /api/admin/iot/devices/{sn}/commands(admin-be 会话 + WSS 在场前置
转译业务命令(薄载荷) timelapse.playback-start / playback-seek / playback-stop / relay-create / compose-create 五条(user-fe 与 admin-fe 均已集成);另有 binding.request(设备出码)经 REST 包装触发
裸指令(自构 payload) FPV 指令台 16 条 COMMAND_SERVICE_*(急停/机器/CNC/手动/倍率/自动任务/配置标定/外设/状态查询),admin-fe 已集成
命令双模式 INSTANT(ack→result)与 PERSISTENT(ack→progress×N→result);转译命令内部固定 INSTANT 下发设备
命令生命周期(6 态) queued → delivered → executing → succeeded / failed / timeout——无 cancelled(中止加工由 eStop 业务指令承担);每步经 WSS command.status 帧推送,终态另有 command.lifecycle 闭环帧
命令去重 dispatch 层 command_id 级去重:重试同 command_id 返回既有命令行原样(不重发 cmd/req),并记 duplicate_request 对账边
响应判定 成功 = 2xx 裸体(命令面为裸 TranslatedCommandResponse/CommandResponse,无信封壳);失败 = 信封 {code,success:false,msg,timestamp,request_id},按数字 code 分支
离线语义 42007 = HTTP 503(不是 409);42028 = HTTP 412(admin 未建立 WSS 在场订阅);42900 = HTTP 429(seek 限流 30 次/60s)
三通道分工 HTTP=下发与查询;WSS=/ws/v1/client(C 端)//ws/v1/admin(admin)实时推送;MQTT=仅设备↔iot-be,APP 永不直连
WSS 观测 subscribe/unsubscribe/ping + 可选 watch_command_ids 连接级观测声明(定向接收 command.status/command.lifecycle);message.log 全通道审计流(cmd 帧结构化增强)
HMS/Status/State 设备 1Hz/0.1Hz/1Hz 周期上行三帧(内层键 snake 终态),云端全量落库 + WSS 广播;APP 消费 WSS 推送或 REST 查询投影
机读交付包 同目录 promotion-app-iot-commands.md(纯协议版,与本册同口径)

---

1. 架构总览

1.1 三通道分工

flowchart LR
  subgraph APP侧
    U[user-fe / 手机APP<br/>C端 lane=JWT]
    A[admin-fe 验证台/远程加工仓<br/>admin lane]
  end
  subgraph 云端
    I[iot-be<br/>命令面+状态机+投影]
  end
  D[设备/模拟器<br/>MQTT mTLS]
  U -- "HTTP: POST /api/v1/commands/{sn}<br/>REST: 查询/心跳/内容" --> I
  A -- "HTTP: POST /api/admin/iot/devices/{sn}/commands<br/>(前置: WSS 在场订阅)" --> I
  U <-- "WSS: /ws/v1/client 推送<br/>command.status / command.lifecycle / message.log<br/>device.status / device.state / shadow.*" --> I
  A <-- "WSS: /ws/v1/admin 推送" --> I
  I -- "MQTT: devices/{sn}/cmd/req (qos1)" --> D
  D -- "MQTT: devices/{sn}/cmd/ack|progress|result<br/>business_status|state|hms 等" --> I
通道 谁用 用途 明确不用
HTTP REST APP(C 端 + admin) 命令下发、命令/任务状态查询、heartbeat、内容拉取、快照查询 不做实时推送
WSS APP(C 端 /ws/v1/client;admin /ws/v1/admin 订阅 SN → 接收 command.status/command.lifecycle(按 command_id watch 定向)、message.log(全通道审计流)、device.status/device.state(遥测)、shadow.reported;admin 路径的 WSS 订阅同时是命令下发的在场前置 不在 WSS 上下发命令(admin 一律禁止;C 端仅保留 command/shadow.desired 入站帧)
MQTT 仅 设备↔iot-be(EMQX,mTLS,clientid=device:{sn} 命令下行 cmd/req、设备上行 `cmd/ack progress resultbusiness_status state hmstimelapse/*live/*` 等 APP 永不直连 MQTT;一切设备交互经 iot-be 中转

1.2 命令面两条 lane 与转译机制

命令端点收到请求后先查转译注册表CommandTranslationRegistry):

C 端 lane admin lane
端点 POST /api/v1/commands/{sn} POST /api/admin/iot/devices/{sn}/commands
鉴权 Authorization: Bearer <用户 JWT>(auth-be 签发;iot-be 经 auth-be resolve 出 internalUserId admin-be 管理会话(AdminSessionCallbackAuthFilter 回调 admin-be 校验)
物权 DeviceAccessGuard.checkOwnership(userId, sn) fail-closed(非 owner → 42009/403) 转译器内:已激活设备按 owner UID;未激活设备回退 admin:<adminId> 代行
额外前置 WSS 在场:发起 HTTP 的同一 adminId 必须持有一条已 subscribe 该 SN 的活跃 /ws/v1/admin 连接,否则 412 / 42028
转译 lane 标记 LANE_USER LANE_ADMIN

SN 错位防护:转译器把领域对象(recording_id/session_id)解析出的 SN 与 URL {sn} 比对,不一致直接 404(防跨设备错投)。

1.3 双模式与命令全生命周期状态机

设备命令帧模式mode 字段,设备侧枚举同名):

云端状态机(6 态,无 cancelled;R32 语义内化):

stateDiagram-v2
  [*] --> queued: dispatch 落库+发布 cmd/req
  queued --> delivered: cmd/ack
  delivered --> delivered: 迟到/重复 ack 幂等吸收
  queued --> executing: cmd/progress 隐式证据<br/>(竞态幂等推进, 不拒绝)
  delivered --> executing: cmd/progress
  executing --> executing: cmd/progress×N
  queued --> succeeded: cmd/result 竞态直达
  delivered --> succeeded: cmd/result success=true
  executing --> succeeded: cmd/result success=true
  queued --> failed: cmd/result success=false
  delivered --> failed: cmd/result success=false
  executing --> failed: cmd/result success=false
  delivered --> timeout: 超时扫描(timeout_at 到点)
  executing --> timeout: 超时扫描
  succeeded --> [*]
  failed --> [*]
  timeout --> [*]

要点(源码 CommandStateMachine):

  1. progress 允许集 = {queued, delivered, executing}——progress 到达即设备已执行的隐式证据,从 queued 幂等推进到 executing,不拒绝(拒绝=丢进度→假超时)。
  2. result 同理:任一非终态收到 result 直达 succeeded/failed;仅三终态(succeeded/failed/timeout)拒绝为重复结果冲突(留痕 *_skipped_terminal)。
  3. INSTANT 命令收到 progress → 状态机冲突(设备侧契约错误),落交互留痕。
  4. 终态后的迟到 ack/progress/result:**只记录留痕(*_skipped_terminal),不推进、不报错**;终态后晚到的 result 另落 late_result 对账边。
  5. 超时:timeout_at(客户端 timeout_seconds,0/负 → 服务端默认值兜底)到点由扫描器置 timeout;另有无响应追查边 response_missing(静默设备也得到结论性结局)。
  6. 去重:dispatch 层 command_id 级去重——重试同 command_id 返回既有命令行原样(状态机零副作用、不重发 cmd/req),并记 duplicate_request 对账边(SLS 按 command_id 可查)。并发重试由 uk_command_id 唯一键闭环(竞态落败方返回赢家行)。

每个状态迁移产生一条 WSS command.status 推送帧;命令进终态时另推一条 command.lifecycle 闭环聚合帧(§7.3):

{
  "v": 1,
  "type": "command.status",
  "sn": "BCZJ00000101KE00002682F000000000050T",
  "data": {
    "command_id": "0b1f...e9",
    "status": "executing",
    "event_type": "progress",
    "payload": { "command_id": "0b1f...e9", "phase": "COMMAND_PROGRESS_PHASE_RUNNING", "progress_permille": 500, "stage": "HOMING", "execution_id": "exec-0b1f...e9", "timestamp_ms": "1757568000123" }
  }
}

字段:data.command_id / data.status(枚举 queued/delivered/executing/succeeded/failed/timeout)/ data.event_typeack|progress|result|timeout)/ data.payload(设备上行帧原文)/ 失败时附加 data.error_message投递路由 = watch:仅声明观测该 command_id 的连接收到(§7.2);其余帧类型仍按 SN 扇出。

1.4 全链路时序(一条 PERSISTENT 裸指令的完整生命周期)

sequenceDiagram
  participant APP as APP(user/admin)
  participant IOT as iot-be
  participant DEV as 设备(MQTT)
  Note over APP,IOT: C端: JWT 已获取 / admin: WSS 已在场订阅该SN
  APP->>IOT: POST 命令端点 {command_id?,command_type,mode,payload,idempotency_key?,timeout_seconds}
  IOT->>IOT: command_id 去重→限流→幂等键→在线断言→可行性断言→queued 落库
  IOT->>DEV: MQTT devices/{sn}/cmd/req {"command_id","mode","command_type","payload"}
  IOT-->>APP: 200 裸 CommandResponse {command_id, status:"queued", ...}
  IOT-->>APP: WSS message.log(cmd/req 下行帧, 结构化 command_id/command_type/execution_id/payload)
  DEV-->>IOT: devices/{sn}/cmd/ack {"command_id","status":"COMMAND_ACK_STATUS_ACCEPTED","execution_id","received_at_ms"}
  IOT-->>APP: WSS command.status {status:"delivered", event_type:"ack"}
  loop PERSISTENT 进度
    DEV-->>IOT: devices/{sn}/cmd/progress {"command_id","phase","progress_permille","stage","execution_id","timestamp_ms"}
    IOT-->>APP: WSS command.status {status:"executing", event_type:"progress", payload:↑}
  end
  DEV-->>IOT: devices/{sn}/cmd/result {"command_id","success":true,"result":{...}}
  IOT-->>APP: WSS command.status {status:"succeeded", event_type:"result", payload:↑}
  IOT-->>APP: WSS command.lifecycle {终态聚合帧: request+timeline 全边+progress_snapshot}
  Note over APP,DEV: 任一步超时: IOT 扫描器置 timeout 并推送 event_type:"timeout" + lifecycle 帧

转译命令(五条 timelapse 动词)的差异:HTTP 同步响应已是业务结果(会话/任务已建好),设备侧命令(timelapse.playback-start/seek/stop/file-upload/compose)由服务端在事务内自动下发,APP 无需再发第二条指令,后续只做 REST 轮询。

---

2. 鉴权、在场前置与通用协议

2.1 C 端 lane:JWT 获取与物权校验

  1. 取 token:登录走 auth-be(Auth0 托管域,PKCE;参照 user-fe getAccessTokenSilently() 静默续期)。该 JWT 即所有 C 端 iot 接口的 Bearer token。
  2. iot-be 侧解析Authorization: Bearer <JWT> → iot-be JwtAuthFilter 调 auth-be resolve → 得 internalUserId(属性名 iot.userId)。解析失败=未认证(401 / 40100)。
  3. SN 级准入(S1,fail-closed;两层权模型):命令端点先 DeviceAccessGuard.checkOwnership(userId, sn)——机主(物权)或控制权持有者(多人扫码获得 iot_device_control_grant)均放行;两者皆无 → 403 / 42009DEVICE_ACCESS_DENIED)。设备未激活 → 403 / 42008。模型全貌见 15.07《设备管理》。

2.2 admin lane:opaque token + WSS 在场(412/42028 触发与解除)

admin 命令端点走 admin-be 管理会话(不是 JWT):

第 1 步 · 拿 admin 会话 token(opaque 串,非 JWT):

curl -s -X POST https://admin-be.<env>/api/admin/auth/local-login \
  -H 'Content-Type: application/json' \
  -d '{"username":"<admin账号>","password":"<密码>"}'
# 响应信封 data.accessToken = "5bffd0b3-….ead56"(opaque,≈101 字符)

第 2 步 · 建立 admin 控制 WSS(token 唯一合法载体 = 子协议头):

URL:   wss://iot-be.<env>/ws/v1/admin
Header: Sec-WebSocket-Protocol: bearer.<accessToken>     ← 无空格,不带 "Bearer " 前缀
期望:   HTTP 101 Switching Protocols

禁项:query 传 token(已移除)、Authorization 头(握手不读)、非浏览器客户端带 Origin 头(须命中 CORS 白名单)。握手限流:每 IP 60 次/60s(超了 401 rate_limited,等窗口再试)。

第 3 步 · 订阅目标 SN(412 的解除条件):

{"type": "subscribe", "sn": "<目标SN>"}

成功回 {"type":"ack","result":"subscribed","sn":"<SN>"}。此后 AdminPresence.hasAdminSubscriber(sn, adminId)=true——第 4 步的 HTTP 命令下发就要求发起请求的同一 adminId 持有该订阅,否则:

HTTP 412
{
  "code": 42028,
  "success": false,
  "msg": "active admin control websocket required for device <SN>",
  "timestamp": 1757568000123,
  "request_id": "8f3c19a0..."
}

语义:snapshot 等无副作用读不要求在场;命令下发/通知推送是危险操作,要求"发起请求的 admin 正在通过 WSS 盯着这台设备"——刻意安全设计。断开或 unsubscribe 后 412 复现,操作全程保持 socket 在线。

2.3 通用请求头与追踪

必带 说明
Authorization: Bearer <token> C 端=用户 JWT;admin 面=admin-be 会话 token
Content-Type: application/json
X-Request-Id 建议 每请求唯一(如 uuid);响应信封回显 request_id,报障必带
X-Trace-Id 可选 链路追踪(优先级高于 X-Request-Id 的回显选择)

2.4 响应判定规则(裸体 vs 信封)

信封 v2 结构(错误时才出现;成功命令面响应是裸体):

{
  "code": 0,
  "success": true,
  "msg": "ok",
  "timestamp": 1757568000123,
  "request_id": "8f3c19a0...",
  "data": { }
}

客户端判定(与 user-fe 拦截器同款逻辑):

  1. 响应 body 含 code 键 → 信封:成功唯一判据 code == 0(业务载荷在 data);非 0 → 按 code 分支,msg 展示、request_id 留痕。
  2. code 键 → iot 裸 DTO:2xx 即成功。
  3. 命令端点专属:200 = 裸 TranslatedCommandResponse / CommandResponse(无信封壳),业务结果取 .data;错误必为信封。devices 域 REST(bind-codes 等)显式包信封(成功信封 data 为载荷);admin verify 投影 GET 显式包信封。

2.5 错误码全表(含 HTTP 码与处理建议)

iot 专属段 42xxx(登记簿 IotErrorCodes)+ 通用段(ErrorCodes):

code HTTP 语义 触发场景(命令面) APP 处理建议
40100 401 UNAUTHENTICATED 无/坏 token、WSS 握手身份解析失败 引导重新登录;WSS 见 §7.1
40300 403 FORBIDDEN 权限不足(admin 面无 device.cloud.read/*:*:* 提示无权限
40400 404 NOT_FOUND recording_id/session_id/task_id/compose_id 不存在;SN 错位防护(领域对象属于另一台 SN) 检查 ID;刷新列表
40900 409 CONFLICT(通用) 通用冲突 读 msg
42900 429 RATE_LIMITED timelapse.playback-seek > 30 次/60s(按 session_id 限流);WSS 握手超限为 401 rate_limited(字符串码) 退避到窗口结束再试;SEEK 目标先按 GOP 2s 量化减少请求
50000 500 INTERNAL_ERROR 兜底 request_id 报障
42001 409 IDEMPOTENCY_CONFLICT idempotency_key 配了不同请求体 幂等键必须请求级唯一
42003 409 STATE_CONFLICT 状态机拒绝:FPV 占用(msg 含已开时长)、录像已删除、会话/任务终态操作、命令终态重复 result msg 可操作:先关 FPV/换录像/不再操作终态对象
42004 409 SHADOW_VERSION_CONFLICT shadow expected_version 不匹配 重读 shadow 再试
42005 422 COMMAND_NOT_FEASIBLE 命令对目标设备不可行 检查设备能力/状态
42007 503 DEVICE_OFFLINE 命令下发时设备离线(在线存储判定) 提示"设备离线,请先开机";不是冲突
42008 403 DEVICE_NOT_ACTIVATED 设备无激活记录 引导激活流程
42009 403 DEVICE_ACCESS_DENIED 当前用户既非机主也不持控制权(两层权模型) 扫码/输码获得控制权,或切换机主账号
42025 404 TRANSFER_EMAIL_NOT_FOUND 过户目标邮箱无已验证账号(POST /devices/{sn}/transfer 核对对方邮箱(须为已验证注册邮箱)
42014 400 BIND_CODE_INVALID 绑定码无效/过期/已核销 重新出码
42015 400 UNBIND_CODE_INVALID 解绑验证码无效/过期 重新获取
42016 503 MESSAGING_UNAVAILABLE MQTT 通道不可用(broker 不可达;命令会以 failed 留痕) 稍后重试;带 request_id 报障
42023 409 RELAY_TASK_CONCURRENT 同 (sn,asset_type) 活跃中转任务已达上限且非同资产幂等重放 等当前任务终态或轮询既有任务
42024 409 ASSET_VERSION_MISMATCH asset_version 与当前不符(msg 含 requested/current) 重取详情拿新版本再试
42028 412 PRECONDITION_FAILED admin 命令/通知下发时无在场 WSS 订阅 按 §2.2 三步建立并保持订阅
42029 503 SERVICE_UNAVAILABLE ResponseStatus 503 泛化 稍后重试

关键错误信封样例(其余同构,替换 code/msg):

// 42007 设备离线(HTTP 503)
{ "code": 42007, "success": false, "msg": "device is offline — open the device (simulator) first",
  "timestamp": 1757568000123, "request_id": "c01d..." }

// 42003 FPV 占用冲突(HTTP 409,msg 可操作:含 FPV 已开时长)
{ "code": 42003, "success": false,
  "msg": "live FPV already streaming for 3分12秒 (sn=BCZJ..., liveSessionId=...) — close FPV before timelapse playback",
  "timestamp": 1757568000123, "request_id": "c01d..." }

// 42900 seek 限流(HTTP 429)
{ "code": 42900, "success": false, "msg": "seek rate limit exceeded for session <sessionId>",
  "timestamp": 1757568000123, "request_id": "c01d..." }

// 42024 版本不匹配(HTTP 409)
{ "code": 42024, "success": false, "msg": "recording assetVersion has moved on (requested=1, current=2)",
  "timestamp": 1757568000123, "request_id": "c01d..." }

// 42028 admin 未在场(HTTP 412)
{ "code": 42028, "success": false, "msg": "active admin control websocket required for device BCZJ...",
  "timestamp": 1757568000123, "request_id": "c01d..." }

---

3. 业务命令接入配方(转译命令·五条 timelapse + binding.request)

统一请求体CommandDispatchRequest,snake;五条转译命令只需 command_type + payload 两键;command_id 可省——服务端生成):

{
  "command_id": "可选-客户端自定幂等ID(重试同id=取回既有命令行,不重发)",
  "command_type": "timelapse.playback-start",
  "payload": { },
  "mode": "INSTANT",
  "idempotency_key": "可选-幂等键",
  "timeout_seconds": 0
}

统一成功响应(200 裸体,snake;data = 领域结果;下文各指令只给 data 内容):

{ "command_id": null, "command_type": "timelapse.playback-start", "data": { } }

统一设备侧五体(APP 不接触,供联调对照;设备命令与 APP 动词的转译对应:playback-seek→timelapse.seekrelay-create→timelapse.file-uploadcompose-create→timelapse.compose,其余同名直达):

3.1 timelapse.playback-start —— 打开延时摄影回放(建会话+双侧凭证分发)

业务含义:为指定录像创建播放会话——云端签发 veRTC 房间与 viewer 凭证、命令设备从指定偏移起推流。是回放链的入口动作。

前置:设备在线(否则 42007/503);无活跃实时 FPV(否则 42003/409,msg 含 FPV 已开时长);录像未删除(DELETED → 42003/409);C 端须为设备 owner 或控制权持有者。

请求 JSON(C 端)

POST /api/v1/commands/BCZJ0000...50T
Authorization: Bearer <用户JWT>
{
  "command_type": "timelapse.playback-start",
  "payload": { "recording_id": "rec-9f21ab34", "start_offset_ms": 0 }
}

payload 字段:recording_id(必填)、start_offset_ms(可选,默认 0,毫秒)。转译器只认 snake(camel 键不解析)。

成功响应(200 裸体,data = CreateResult,snake)

{
  "command_id": null,
  "command_type": "timelapse.playback-start",
  "data": {
    "session_id": "6f1e2b4a-9c3d-4e5f-8a7b-2c1d3e4f5a6b",
    "recording_id": "rec-9f21ab34",
    "sn": "BCZJ00000101KE00002682F000000000050T",
    "status": "CREATED",
    "start_offset_ms": 0,
    "provider": "vertc",
    "app_id": "vertc-app-xxxx",
    "viewer_token": "eyJhbGciOi...(viewer 接流令牌)",
    "viewer_identity": "viewer-6f1e2b4a",
    "expires_at_epoch_seconds": 1757571600
  }
}

注意:data 绝无 publish_token——设备推流凭证仅存在于下行命令载荷(零日志/零前端/零落库)。admin lane 同构:POST /api/admin/iot/devices/{sn}/commands,未激活设备 owner 由 admin:<adminId> 代行。

关键错误分支:42007(503 离线)/ 42003(409 FPV 占用·含已开时长;409 录像已删除)/ 42009(403 非 owner)/ 40400(404 recording_id 不存在或属于别的 SN)。样例见 §2.5。

设备侧五体(联调对照;payload 内层 camel·媒体豁免域)

// ① cmd/req(服务端铸厚载荷;publishToken 仅此处出现)
{ "command_id": "a1b2c3d4-...", "mode": "INSTANT", "command_type": "timelapse.playback-start",
  "payload": { "sessionId": "6f1e2b4a-...", "startOffsetMs": 0,
    "room": "timelapse-6f1e2b4a-...", "provider": "vertc", "appId": "vertc-app-xxxx",
    "publishToken": "<设备推流令牌>", "expiresAtEpochSeconds": 1757571600 } }

// ② cmd/ack
{ "command_id": "a1b2c3d4-...", "status": "COMMAND_ACK_STATUS_ACCEPTED",
  "execution_id": "exec-a1b2c3d4-...", "received_at_ms": "1757568000123" }

// ③ cmd/progress —— 无(INSTANT)

// ④ 领域上行 devices/{sn}/timelapse/stream(推流就绪校准)
{ "sessionId": "6f1e2b4a-...", "streamInstanceId": "<设备生成UUID>", "event": "STREAM_STARTED",
  "actualOffsetMs": 0, "gopSeconds": 2 }

// ⑤ cmd/result
{ "command_id": "a1b2c3d4-...", "success": true,
  "result": { "sessionId": "6f1e2b4a-...", "streamInstanceId": "<UUID>" } }

后续动作:veRTC joinRoom(token=viewer_token, roomId="timelapse-"+sessionId, userId=viewer_identity)——userId 逐字用返回值(token 绑定身份);轮询 GET /api/v1/timelapse/playback-sessions/{sessionId}status=STREAMING 并取 actual_offset_ms 校准播放轴;会话 TTL 600s,剩 60s 时 POST .../heartbeat 续期(绝对寿命 4h)。

3.2 timelapse.playback-seek —— 拖动进度(换推流器重推,拉流不断)

业务含义:SEEK 事务——云端旧流实例置 SUPERSEDED、新实例建档、现场再铸设备凭证下发,设备换推流器从新点位重推。

前置:会话非终态(终态 → 42003/409);未触发限流(>30 次/60s → 429/42900)。

请求 JSON(C 端)

POST /api/v1/commands/BCZJ0000...50T
{
  "command_type": "timelapse.playback-seek",
  "payload": { "session_id": "6f1e2b4a-...", "target_offset_ms": 132000 }
}

target_offset_ms 可省(默认回读会话当前 actual_offset_ms)。客户端先行把目标按 GOP 2s 量化再换算毫秒。

成功响应(data = SeekResult,snake)

{
  "command_id": null,
  "command_type": "timelapse.playback-seek",
  "data": { "session_id": "6f1e2b4a-...", "stream_instance_id": "e7d8f9a0-...", "status": "STREAMING" }
}

设备侧五体(转译为设备命令 timelapse.seek;payload camel·媒体豁免域):

// ① cmd/req(凭证现场再铸,随载荷下发)
{ "command_id": "b2c3d4e5-...", "mode": "INSTANT", "command_type": "timelapse.seek",
  "payload": { "sessionId": "6f1e2b4a-...", "room": "timelapse-6f1e2b4a-...",
    "startOffsetMs": 132000, "streamInstanceId": "e7d8f9a0-...",
    "appId": "vertc-app-xxxx", "publishToken": "<现铸设备令牌>" } }

// ② cmd/ack
{ "command_id": "b2c3d4e5-...", "status": "COMMAND_ACK_STATUS_ACCEPTED",
  "execution_id": "exec-b2c3d4e5-...", "received_at_ms": "1757568000123" }

// ③ 领域上行 STREAM_SEEKED(streamInstanceId 必须回显①里的载荷值)
{ "sessionId": "6f1e2b4a-...", "streamInstanceId": "e7d8f9a0-...", "event": "STREAM_SEEKED",
  "actualOffsetMs": 132000, "gopSeconds": 2 }

// ④ cmd/result
{ "command_id": "b2c3d4e5-...", "success": true,
  "result": { "sessionId": "6f1e2b4a-...", "streamInstanceId": "e7d8f9a0-...", "repushed": true } }

后续动作:本地 playhead 立即跳 + Loading(旧流持续渲染),≤5s 首帧宽限退出;迟到旧实例事件由云端 SUPERSEDED 幂等吸收,APP 无需处理。

3.3 timelapse.playback-stop —— 主动停止(幂等终态闭环)

业务含义:一次性执行三步——①删 viewer(provider 销毁拉流凭证)②命令设备停推 ③销毁房间。幂等由状态机守门(重复 stop 返回既有 STOPPED)。

请求 JSON(C 端)

POST /api/v1/commands/BCZJ0000...50T
{ "command_type": "timelapse.playback-stop", "payload": { "session_id": "6f1e2b4a-..." } }

成功响应(data = StopResult,snake)

{ "command_id": null, "command_type": "timelapse.playback-stop",
  "data": { "session_id": "6f1e2b4a-...", "status": "STOPPED" } }

设备侧五体(设备命令同名 timelapse.playback-stop;payload camel·媒体豁免域):

// ① cmd/req
{ "command_id": "c3d4e5f6-...", "mode": "INSTANT", "command_type": "timelapse.playback-stop",
  "payload": { "sessionId": "6f1e2b4a-...", "room": "timelapse-6f1e2b4a-..." } }

// ② cmd/ack
{ "command_id": "c3d4e5f6-...", "status": "COMMAND_ACK_STATUS_ACCEPTED",
  "execution_id": "exec-c3d4e5f6-...", "received_at_ms": "1757568000123" }

// ③ 领域上行 STREAM_STOPPED(幂等:无推流器也回)
{ "sessionId": "6f1e2b4a-...", "streamInstanceId": "<UUID>", "event": "STREAM_STOPPED" }

// ④ cmd/result
{ "command_id": "c3d4e5f6-...", "success": true,
  "result": { "sessionId": "6f1e2b4a-...", "stopped": true } }

后续动作:销毁本地播放器与轮询;页面离开/关闭 FPV 时也应触发 stop。

3.3.1 live.publisher.renew —— veRTC 凭证 15min 滚动续期(域无关媒体面)

背景契约:veRTC AccessToken TTL=15min(Apollo iot.media.volcengine.token-ttl-seconds=900);token 到期即被移出房间。延时摄影回放与 FPV live 共用同一 veRTC 通道 → 续期协议不分域,按 room 前缀分流(timelapse-*=按偏移续推;其他=live 推流器重启)。

两条通道

通道 载体 语义 台账
publisher(设备) MQTT 命令 live.publisher.renew 设备换推流器续凭证(seek 同语义,viewer 拉流不断) live_publisher_renew_issued(SLS)+ 命令台账 + cmd/result
viewer(APP) HTTP 回包(heartbeat 响应新增字段) App 在房 engine.updateToken() 无感换发 live_viewer_renew_issued(SLS)

触发锚点(云端自动,APP 不感知 publisher 侧):回放=每次 POST /timelapse/playback-sessions/{id}/heartbeat(STREAMING 时)现铸双端凭证;live=设备 5s 心跳半衰窗。

设备命令五体(设备侧,APP 仅供理解;payload camel·媒体豁免域)

// ① cmd/req(cloud → device; publishToken 仅此出现, 零日志零落盘)
{ "command_id": "e5f6a7b8-...", "mode": "INSTANT", "command_type": "live.publisher.renew",
  "payload": { "sessionId": "6f1e2b4a-...", "room": "timelapse-6f1e2b4a-...",
    "appId": "<veRTC AppId>", "publishToken": "<新铸 device token>",
    "expiresAtEpochSeconds": 1789065000, "resumeOffsetMs": 4200 } }

// ② cmd/ack
{ "command_id": "e5f6a7b8-...", "status": "COMMAND_ACK_STATUS_ACCEPTED",
  "execution_id": "exec-e5f6a7b8-...", "received_at_ms": "1757568000123" }

// ③ cmd/result(幂等: 无上下文 renewed=false + reason, 不伪造成功)
{ "command_id": "e5f6a7b8-...", "success": true,
  "result": { "sessionId": "6f1e2b4a-...", "room": "timelapse-6f1e2b4a-...",
    "renewed": true, "resumeOffsetMs": 4200 } }

APP 侧动作(viewer 续期,三行代码):heartbeat 响应字段 viewer_token / viewer_token_expires_at_epoch_seconds(现铸、同 identity 绑定)——在房时调 engine.updateToken(res.viewer_token);不在房忽略。注意:§3.2"剩 60s 续期"语义=每次续期同时完成 veRTC 双端凭证滚动换发,App 心跳调用方式不变。

3.4 timelapse.relay-create —— 高光/原片中转取件(clip / master / 幂等重放)

业务含义:建按需中转任务——云端铸 uploadToken/objectKey,命令设备裁剪(clip 按高光窗口)或读原片(master)后预签 PUT 上对象存储;APP 稍后经 content 拉预签 URL。零持久化:对象即用即删。

前置asset_version 对位(不符 → 42024/409);同 (sn,asset_type) 并发上限(非幂等重放场景 → 42023/409);clip 必传 highlight_id

请求 JSON(C 端)

POST /api/v1/commands/BCZJ0000...50T
{
  "command_type": "timelapse.relay-create",
  "payload": { "recording_id": "rec-9f21ab34", "asset_type": "clip",
               "asset_version": 1, "highlight_id": "H3" }
}

master 不传 highlight_id。幂等语义:同 recording+asset_type+version 的重复创建返回既有活动任务(不报错);终态后重看=新建任务。

成功响应(data = CreateResult,snake)

{
  "command_id": null,
  "command_type": "timelapse.relay-create",
  "data": { "task_id": "d4e5f6a7-...", "recording_id": "rec-9f21ab34",
            "sn": "BCZJ0000...50T", "asset_type": "clip", "asset_version": 1, "status": "CREATED" }
}

设备侧五体(转译为设备命令 timelapse.file-upload;uploadToken/objectKey 仅此处出现;payload camel·媒体豁免域):

// ① cmd/req
{ "command_id": "d4e5f6a7-...", "mode": "INSTANT", "command_type": "timelapse.file-upload",
  "payload": { "taskId": "d4e5f6a7-...", "uploadToken": "Ab3d...-_(128bit URL-safe B64)",
    "objectKey": "timelapse/relay/d4e5f6a7-....mp4", "assetType": "clip", "highlightId": "H3" } }
// (compose 产物中转时 payload 换成 {taskId, uploadToken, objectKey, assetType:"compose-product", composeId})

// ② cmd/ack
{ "command_id": "d4e5f6a7-...", "status": "COMMAND_ACK_STATUS_ACCEPTED",
  "execution_id": "exec-d4e5f6a7-...", "received_at_ms": "1757568000123" }

// ③ 领域进度 devices/{sn}/timelapse/relay(两帧:半程+完成;首帧回显 uploadToken)
{ "taskId": "d4e5f6a7-...", "event": "RELAY_UPLOAD_PROGRESS", "uploadedBytes": 1024,
  "totalBytes": 2048, "uploadToken": "Ab3d...-_" }
{ "taskId": "d4e5f6a7-...", "event": "RELAY_UPLOAD_PROGRESS", "uploadedBytes": 2048, "totalBytes": 2048 }

// ④ cmd/result
{ "command_id": "d4e5f6a7-...", "success": true,
  "result": { "taskId": "d4e5f6a7-...", "uploadedBytes": 2048, "videoBytes": true } }
// 失败样例(预签 PUT 失败):{ "command_id":"...", "success": false,
//   "result": { "taskId": "...", "step": "presign-put", "httpStatus": 403 } }

后续动作:轮询 GET /api/v1/timelapse/relay-tasks/{task_id}{task_id,status,uploaded_bytes,total_bytes})到 READY_FOR_PULLGET .../content(一次性拉取语义:pull 即 COMPLETED)→ 窗内下载。中途放弃 → POST .../cancel

3.5 timelapse.compose-create —— 高光合成分(设备渲染 + 产物上传)

业务含义:选高光集合下发设备本地 concat 渲染;产物经中转通道(compose-product)上 OSS;APP 轮询五阶段。产物契约=所选窗口时长之和(1:1 原速)。

请求 JSON(C 端)

POST /api/v1/commands/BCZJ0000...50T
{
  "command_type": "timelapse.compose-create",
  "payload": { "recording_id": "rec-9f21ab34", "highlight_ids": ["H3", "H5"], "template": "standard" }
}

template 可省(默认 "standard")。

成功响应(data = ComposeResult,snake)

{
  "command_id": null,
  "command_type": "timelapse.compose-create",
  "data": { "compose_id": "e5f6a7b8-...", "recording_id": "rec-9f21ab34",
            "sn": "BCZJ0000...50T", "status": "DISPATCHED" }
}

设备离线时命令面抛 42007/503,且 compose 任务以 FAILED(dispatch_failed) 留痕(可查)。

设备侧五体(转译为设备命令 timelapse.compose;payload camel·媒体豁免域):

// ① cmd/req
{ "command_id": "e5f6a7b8-...", "mode": "INSTANT", "command_type": "timelapse.compose",
  "payload": { "composeId": "e5f6a7b8-...", "recordingId": "rec-9f21ab34",
    "highlightIds": ["H3", "H5"], "template": "standard" } }

// ② cmd/ack
{ "command_id": "e5f6a7b8-...", "status": "COMMAND_ACK_STATUS_ACCEPTED",
  "execution_id": "exec-e5f6a7b8-...", "received_at_ms": "1757568000123" }

// ③ 领域进度 devices/{sn}/timelapse/compose(两帧)
{ "composeId": "e5f6a7b8-...", "event": "COMPOSE_PROGRESS", "percent": 30, "phase": "LOCAL_MATERIAL" }
{ "composeId": "e5f6a7b8-...", "event": "COMPOSE_PROGRESS", "percent": 70, "phase": "CONCAT" }

// ④ 领域结果帧
{ "composeId": "e5f6a7b8-...", "event": "COMPOSE_RESULT", "result": "ok", "productBytes": 2048 }

// ⑤ cmd/result(此后云端自动建产物中转任务并下发 file-upload[compose-product])
{ "command_id": "e5f6a7b8-...", "success": true,
  "result": { "composeId": "e5f6a7b8-...", "productBytes": 2048, "videoBytes": true } }

后续动作:轮询 GET /api/v1/timelapse/composes/{compose_id}{compose_id,status,percent?,phase?,failure_reason?,product_relay_task_id?}):DISPATCHED → RENDERING(percent/phase) → SUCCEEDED;SUCCEEDED 后按 product_relay_task_id 复用 §3.4 的 GET 轮询 + content 取产物。

3.6 binding.request —— 请求设备出码(绑定码 v2.1,REST 包装触发)

业务含义:FPV 面板"请求设备出码"——云端下发 INSTANT binding.request 命令,设备本地出 4 位 ticket + 一机一密签票并 sync 回云端;APP 轮询 latest 拿码渲染二维码。两层权模型:已确权(已激活/已绑定)设备照常出码——扫码/输码兑换获得的是控制权(多人可共控),物权不受影响。机主过户走 POST /api/v1/devices/{sn}/transfer(输入对方已验证邮箱直接转出,控制权全清需重新扫码),详见 15.07。 HTTP 面(C 端,信封包装域)

POST /api/v1/devices/BCZJ0000...50T/bind-codes/request
Authorization: Bearer <用户JWT>
// 无请求体

// 200 成功(信封)
{ "code": 0, "success": true, "msg": "ok", "timestamp": 1757568000123,
  "request_id": "8f3c...", "data": { "sn": "BCZJ0000...50T", "requested": true } }

// 失败:42007/503 离线、42005/422 不可行、42009/403 非 owner(异常类型化透传,不折叠成 requested:false)

admin 面同构:POST /api/admin/iot/devices/{sn}/bind-codes/request(412/42028 在场前置)。

设备侧五体

// ① cmd/req(payload snake)
{ "command_id": "f6a7b8c9-...", "mode": "INSTANT", "command_type": "binding.request",
  "payload": { "type": "bind-code-request", "sn": "BCZJ0000...50T",
    "requested_by": "user:<internalUserId>", "at": "2026-09-11T08:00:00Z" } }

// ② cmd/ack
{ "command_id": "f6a7b8c9-...", "status": "COMMAND_ACK_STATUS_ACCEPTED",
  "execution_id": "exec-f6a7b8c9-...", "received_at_ms": "1757568000123" }

// ③ cmd/result(设备如实回报出码+sync 结果;ticket=4 位数字)
{ "command_id": "f6a7b8c9-...", "success": true, "result": { "ticket": "4831" } }

后续动作:轮询 GET /api/v1/devices/{sn}/bind-codes/latest(信封域,无码 → data:null):

{ "code": 0, "success": true, "msg": "ok", "timestamp": 1757568000123, "request_id": "8f3c...",
  "data": { "device_sn": "BCZJ0000...50T", "ticket": "4831", "version": 21,
            "sign": "<一机一密签名>", "status": "ACTIVE", "expires_at": "2026-09-11T08:05:00Z" } }

QR 票据四元组 = {ticket, sn(取 device_sn), version, sign};状态机 ACTIVE → SCANNED_VERIFIED → BOUND(残标随 5min TTL 消失)。

---

4. 裸指令目录(FPV 指令台 16 条 COMMAND_SERVICE_*

未命中转译注册表的 command_type 直接投递设备(admin-fe 远程加工仓已全量集成)。APP 端集成时按目录自构 payload;命令面响应为裸 CommandResponsestatus:"queued"),进度经 WSS command.status 帧。

4.1 分组总表(admin-fe catalog 全量)

分组 command_type(service 名) 模式 字段(key: kind[必填])
急停与机器 COMMAND_SERVICE_CNC_ESTOP_CTRL INSTANT action: enum[必] {ESTOP_ACTION_ENGAGE, ESTOP_ACTION_RESET}
COMMAND_SERVICE_CNC_MACHINE_CTRL INSTANT action: enum[必] {MACHINE_ACTION_ENABLE/DISABLE/RESET/RESTART}
COMMAND_SERVICE_CNC_SET_MANUAL_MODE INSTANT
CNC 加工 COMMAND_SERVICE_SELECT_FILE INSTANT programPath: text[必](如 /programs/demo.ngc
COMMAND_SERVICE_CNC_CTRL PERSISTENT action: enum[必] {CNC_ACTION_START/FEED_HOLD/RESUME/STOP};programPath?: text(START 可指定);startLine?: number
COMMAND_SERVICE_CNC_MDI INSTANT commands: stringList[必](每行一条,如 G0 X10
手动与回零 COMMAND_SERVICE_CNC_JOG_CTRL PERSISTENT action: enum[必] {JOG_ACTION_CONTINUOUS/INCREMENTAL/STOP};axis?: enum {AXIS_X..AXIS_C};direction?: enum {JOG_DIRECTION_POSITIVE/NEGATIVE};speed?: number(mm/min 或 deg/min);distance?: number(仅增量)
COMMAND_SERVICE_CNC_HOME PERSISTENT target: enum[必] {HOME_TARGET_ALL/X/Y/Z/B/C}
倍率 COMMAND_SERVICE_CNC_OVERRIDE_SET INSTANT target: enum[必] {OVERRIDE_TARGET_FEED/SPINDLE};value: number[必] 0..2(1.0=100%)
自动任务 COMMAND_SERVICE_CNC_AUTO_TASK_START PERSISTENT task: enum[必] {AUTO_TASK_TYPE_CENTER_FINDING/TOOL_SETTING};timeoutS?: number(0=默认)
COMMAND_SERVICE_SELF_TEST_START PERSISTENT
配置与标定 COMMAND_SERVICE_CNC_INI_CTRL INSTANT action: enum[必] {INI_ACTION_READ/WRITE};key: text[必](section.key);value?: text(WRITE 必填)
COMMAND_SERVICE_CNC_CALIBRATION_CTRL INSTANT action: enum[必] {CALIBRATION_ACTION_READ/WRITE};scope: enum[必] {CALIBRATION_SCOPE_ITEM/ALL};section/key?: text(ITEM 必填);value?: text(WRITE+ITEM 必填)
外设 COMMAND_SERVICE_LIGHT_CTRL INSTANT target: enum[必] {LIGHT_TARGET_AMBIENT/WORK};enabled: switch[必]
COMMAND_SERVICE_MQL_CTRL INSTANT on: switch[必]
COMMAND_SERVICE_SAFETY_POLICY_SET INSTANT target: enum[必] {SAFETY_POLICY_TARGET_DOOR_OPEN/MQL_LOW_WATER};pauseEnabled: switch[必]
状态 COMMAND_SERVICE_STATUS INSTANT 无(一次性拉取 status/state/hms 快照)

payload 内层键形态说明:裸指令 payload 内层键为 camel 过渡形态programPath/startLine/timeoutS/pauseEnabled——admin-fe catalog 与设备侧解析一致的现行 wire 形态;设备参考实现同时容忍 program_path 等 snake 别名,为遗留容错未收口域)。cmd/req 信封四键(command_id/mode/command_type/payload)已 snake 终态,与内层形态正交。

另有同通道收编的设备命令(APP 一般经领域 REST 间接触发,不在指令台目录):live.stop(载荷 {sessionId, reason:"viewer_closed"}·媒体豁免域,设备停推后上行 live/stopped)。

4.2 请求/响应与进度时间线(统一五体)

请求 JSON(admin,C 端同构换端点/鉴权;snake)

POST /api/admin/iot/devices/BCZJ0000...50T/commands
Authorization: Bearer <admin-be会话token>
{
  "command_type": "COMMAND_SERVICE_CNC_HOME",
  "mode": "PERSISTENT",
  "payload": { "target": "HOME_TARGET_ALL" },
  "timeout_seconds": 60
}

成功响应(200 裸 CommandResponse,snake,14 键;status:"queued";末三键=progress 快照列,首帧 progress 到达前为 null)

{
  "command_id": "1a2b3c4d-5e6f-...",
  "sn": "BCZJ00000101KE00002682F000000000050T",
  "mode": "PERSISTENT",
  "command_type": "COMMAND_SERVICE_CNC_HOME",
  "status": "queued",
  "idempotency_key": null,
  "error_message": null,
  "queued_at": "2026-09-11T08:00:00Z",
  "delivered_at": null,
  "completed_at": null,
  "timeout_at": "2026-09-11T08:01:00Z",
  "last_phase": null,
  "last_permille": null,
  "last_stage": null
}

断线重连恢复:GET /api/v1/commands/{command_id} 详情即携带 last_phase/last_permille/last_stage——轻量 REST 对齐断档(无需帧重放)。

设备侧五体(以 HOME 为例;设备 cmd/req 信封与 §3 统一)

// ① cmd/req
{ "command_id": "1a2b3c4d-...", "mode": "PERSISTENT", "command_type": "COMMAND_SERVICE_CNC_HOME",
  "payload": { "target": "HOME_TARGET_ALL" } }

// ② cmd/ack(永远立即)
{ "command_id": "1a2b3c4d-...", "status": "COMMAND_ACK_STATUS_ACCEPTED",
  "execution_id": "exec-1a2b3c4d-...", "received_at_ms": "1757568000123" }

// ③ cmd/progress×N(PERSISTENT 才有;permille/phase/stage 取自 SCENARIOS 表)
{ "command_id": "1a2b3c4d-...", "phase": "COMMAND_PROGRESS_PHASE_RUNNING",
  "progress_permille": 500, "stage": "HOMING",
  "execution_id": "exec-1a2b3c4d-...", "timestamp_ms": "1757568000123" }

// ④ cmd/result
{ "command_id": "1a2b3c4d-...", "success": true, "result": "simulated-ok" }

PERSISTENT 进度时间线出厂值(设备 SCENARIOS 表,permille 为 0..1000):

command_type duration(s) (permille, stage) 序列
COMMAND_SERVICE_CNC_ESTOP_CTRL 0.3 (100, ESTOP_RECEIVED) (1000, ESTOP_APPLIED)
COMMAND_SERVICE_CNC_MACHINE_CTRL / SET_MANUAL_MODE 0.5 —(默认 (500, EXECUTING))
COMMAND_SERVICE_SELECT_FILE 0.8 (500, LOADING_FILE)
COMMAND_SERVICE_CNC_CTRL / CNC_JOG_CTRL 0.5 / 0.4
COMMAND_SERVICE_CNC_AUTO_TASK_START 1.0 (200, TASK_STARTED) (1000, TASK_RUNNING)
COMMAND_SERVICE_CNC_CALIBRATION_CTRL 2.0 (500, CALIBRATING)
LIGHT_CTRL / MQL_CTRL 0.2
SAFETY_POLICY_SET 0.6
STATUS 0.2
CNC_MDI 8.0 (200, MDI_EXECUTING) (800, MDI_EXECUTING)
CNC_HOME 1.0 (500, HOMING)
OVERRIDE_SET 2.0
SELF_TEST_START 20.0 (200, SELF_TEST_ELECTRICAL) (500, SELF_TEST_DRIVES) (800, SELF_TEST_SPINDLE)
CNC_INI_CTRL 5.0 (300, INI_WRITING) (900, INI_VERIFYING)
REC_UPLOAD(云录制,内部) 6.0 (300, CLIP_GENERATING) (800, UPLOADING)

命令历史/详情(admin):GET /api/admin/iot/devices/{sn}/commands?page&pageSize(分页)、GET /api/admin/iot/devices/commands/{command_id}(详情)、.../commands/{command_id}/events(ack/progress/result 事件轨迹)、.../commands/{command_id}/interactions(全生命周期交互台账)。C 端:GET /api/v1/commands/{command_id}(详情)。

4.3 命令效果与状态联动(驾驶舱闭环)

每条指令同时驱动真实机器模型,状态/告警随之收敛(APP 下发后可在 WSS 推送/快照里观察到):ESTOP → controller_status.estop=ESTOP_STATE_ACTIVE、主轴/进给归零、HMS 出现 ESTOP_ACTIVE(CRITICAL);HOME → 轴坐标归零、motion_state.axes_homed 全 TRUE;JOG/MDI → 轴坐标向目标移动;AUTO 任务 → progress_pct/program_line 推进;SELF-TEST → 期间 HMS 出现 SELF_TEST_RUNNING(INFO);灯光/MQL → peripheral_status 对应位翻转;安全策略 → config_status.door_pause_enabled/mql_pause_on_low 翻转。详见 §8。

---

5. live 实时画面面(FPV)

live 的控制面主体不是命令端点(APP 侧不直接下发 live 设备指令;云端在会话建立/关闭时自动下发 live.start/live.stop 给设备):

动作 C 端(user-fe 同款) admin(验证台同款) 响应
能力查询 GET /api/v1/live/provider GET /api/admin/iot/live/provider {provider:"vertc"}(空=禁用)
打开/取流凭证 POST /api/v1/live/{sn} GET /api/admin/iot/live/{sn}/credentials?viewer_id=<稳定观看会话ID>(无活跃会话则触发创建) LiveSessionResponse/凭证 Map(含 viewer_token 等)
活跃会话探测 GET /api/admin/iot/live/{sn}/active {active:true/false, session_id?, status?, provider?, opened_at?}
渲染心跳(30s) POST /api/v1/live/{sn}/viewers/{viewerId}/heartbeat 同构 admin 面 {ok:true}
主动离开 POST /api/v1/live/{sn}/viewers/{viewerId}/unsubscribe 同构 {ok:true}
关闭 DELETE /api/v1/live/{sessionId} (viewer 全退后房间自动评估关闭;云端对设备下发 live.stop LiveSessionResponse

unsubscribe 离场归因契约(leave_reason):可选 JSON body {"reason":"user_closed"|"background_playback_timeout"}——缺 body/缺字段/空白 → user_closed;白名单外(含服务端专属值 heartbeat_timeout/server_closed/viewer_ttl_expired、拼写错误、非字符串)→ 400(fail-fast,不静默回退)。完整分类法(五值,SLS live_viewer_left.leave_reason 唯一完整归因流):user_closed(用户主动关)/ background_playback_timeout(UI 后台播放≥2min 自动退订,FE 携带)/ heartbeat_timeout(服务端 stale 清扫)/ server_closed(房间销毁三通道滞留归因)/ viewer_ttl_expired(历史审计事件展示映射,新代码不再写)。

注意互斥:活跃 FPV 与延时摄影回放互斥(先开者占用,冲突 42003 + msg 含已开时长)。

---

6. 保留 REST 面清单(为什么不是命令通道)

命令面统一只收编五类设备写操作;以下保留 REST,原因标注:

端点(C 端基址 /api/v1 动作 保留原因(非命令通道)
POST /timelapse/playback-sessions/{id}/heartbeat TTL 续期 + veRTC 双端凭证滚动续期(回包携带现铸 viewer_token/viewer_token_expires_at_epoch_seconds → App engine.updateToken();STREAMING 时云端同时下发 live.publisher.renew 设备命令;台账 live_viewer_renew_issued/live_publisher_renew_issued 云端会话状态 + 凭证铸造;publisher 侧由云端代理下发,APP 只消费 viewer_token 字段
GET /timelapse/playback-sessions/{id} 会话状态回读(actual_offset_ms 校准) 只读投影轮询
GET /timelapse/relay-tasks/{id} 中转进度轮询(uploaded_bytes/total_bytes) 只读投影
GET /timelapse/relay-tasks/{id}/content 预签 GET 拉取 {task_id,status,url,expires_in_seconds} 云端对象操作(pull 即 COMPLETED)
POST /timelapse/relay-tasks/{id}/cancel 中断中转 {task_id,status:"CANCELLED"} 云端任务状态操作 + 对象删除
GET /timelapse/composes/{id} 合成进度轮询 只读投影
DELETE /timelapse/recordings/{recording_id} 删除录像(幂等) 云端投影元数据操作
GET /machining-jobs/{sn}GET /machining-jobs/{sn}/{job_id} 加工账本/详情(timeline/highlights 唯一来源) 只读账本
GET /shadow/{sn}GET /devices/{sn}/events、`GET /hms/{sn}/current summary` 快照/事件/HMS 查询 只读
bind-codes latest/绑定/解绑域 设备绑定生命周期 REST 包装(request 内部已走命令管线,见 §3.6)

admin 验证台只读投影同构(信封包装):GET /api/admin/iot/timelapse/verify/playback-sessions/{id}/relay-tasks/{id}/relay-tasks/{id}/content/composes/{id}

---

7. WSS 客户端协议

7.1 连接与鉴权

C 端:  wss://iot-be.<env>/ws/v1/client   子协议: ["iot-json", "bearer.<用户JWT>"]
admin: wss://iot-be.<env>/ws/v1/admin    子协议: ["bearer.<admin会话token>"]

7.2 客户端 → 服务端帧(v=1)

// 订阅(C 端须为 owner/控制权;admin 注册在场)
{ "v": 1, "type": "subscribe", "sn": "BCZJ0000...50T",
  "watch_command_ids": ["1a2b3c4d-...", "9f8e7d6c-..."] }
// → {"type":"ack","result":"subscribed","sn":"..."}

// 退订
{ "v": 1, "type": "unsubscribe", "sn": "BCZJ0000...50T" }
// → {"type":"ack","result":"unsubscribed","sn":"..."}

// 保活(纯连接心跳;不刷新任何观看台账)
{ "v": 1, "type": "ping" }
// → {"type":"ack","result":"pong","sn":""}

watch_command_ids(连接级观测声明,替换语义):可选数组键,声明本连接要观测的 command_id 集合——command.statuscommand.lifecycle 两类帧只投递给 watch 了该 command_id 的连接(不 watch 不收;无 command_id 不可路由时服务端丢弃并 WARN,绝不退化为 SN 扇出)。约束:每连接上限默认 100(Apollo iot.ws.max-watch-command-ids,超限 error 帧 WATCH_INVALID);声明被拒时既有 SN 订阅与旧 watch 集不变;终态帧到达后 FE 应剪枝 watched 集并重声明(避免占满上限)。

错误回帧:{"type":"error","code":"...","message":"..."}——稳定字符串码:AUTH_REQUIRED(未认证即关连)、BAD_FRAME(非 JSON)、VERSION_UNSUPPORTED(v≠1,软失败不关连)、MISSING_SNMISSING_TYPEUNSUPPORTED_TYPEWATCH_INVALIDFORBIDDEN(admin 只许 subscribe/unsubscribe/ping;C 端非 owner SN 帧被拒,code 取物权异常稳定码 DEVICE_NOT_ACTIVATED/DEVICE_ACCESS_DENIED)。C 端其他 SN 帧类型(commandshadow.desired)同样先过物权校验。

7.3 服务端 → 客户端推送帧全集

统一帧形:{"v":1,"type":"...","sn":"...","data":{...}}watch 路由面(command_id 定向):command.statuscommand.lifecycleSN 扇出面(订阅该 SN 即收):其余全部。

// ① 命令生命周期(每 ack/progress/result/timeout 一条;watch 路由)
{ "v": 1, "type": "command.status", "sn": "BCZJ0000...50T",
  "data": { "command_id": "1a2b3c4d-...", "status": "succeeded", "event_type": "result",
            "payload": { "command_id": "1a2b3c4d-...", "success": true, "result": "simulated-ok" } } }

// ①' 命令闭环聚合帧(终态时一条:request + timeline 全边 + progress 快照;watch 路由)
{ "v": 1, "type": "command.lifecycle", "sn": "BCZJ0000...50T",
  "data": { "command_id": "1a2b3c4d-...", "command_type": "COMMAND_SERVICE_CNC_HOME",
            "mode": "PERSISTENT", "execution_id": "exec-1a2b3c4d-...", "status": "succeeded",
            "boot_id": "boot-...",
            "request": { "payload": { "target": "HOME_TARGET_ALL" }, "timeout_seconds": 60 },
            "timeline": [ { "edge": "request", "direction": "OUT", "timestamp": "1757568000123" },
                          { "edge": "downlink", "direction": "OUT", "timestamp": "1757568000031" },
                          { "edge": "awaiting_response", "direction": "OUT", "timestamp": "1757568000032" },
                          { "edge": "ack", "direction": "IN", "timestamp": "1757568000180" },
                          { "edge": "progress", "direction": "IN", "timestamp": "1757568000520" },
                          { "edge": "result", "direction": "IN", "timestamp": "1757568001210" } ],
            "progress_snapshot": { "last_phase": "COMMAND_PROGRESS_PHASE_RUNNING",
                                   "last_permille": 500, "last_stage": "HOMING" } } }

// ② 消息审计流(全通道 DOWN/UP 帧;SN 扇出;cmd/* 帧带结构化增强字段)
{ "v": 1, "type": "message.log", "sn": "BCZJ0000...50T",
  "data": { "event_type": "mqtt.cmd.progress", "direction": "UP",
            "topic": "devices/BCZJ0000...50T/cmd/progress",
            "payload_json": "{\"command_id\":\"1a2b...\",\"phase\":\"COMMAND_PROGRESS_PHASE_RUNNING\",...}",
            "occurred_at_ms": 1757568000520,
            "command_id": "1a2b3c4d-...", "command_type": "COMMAND_SERVICE_CNC_HOME",
            "execution_id": "exec-1a2b3c4d-...", "payload": { "phase": "COMMAND_PROGRESS_PHASE_RUNNING", "progress_permille": 500, "stage": "HOMING" },
            "sequence": 7, "boot_id": "boot-...", "timestamp": "1757568000520" } }
// (非 cmd 通道的 message.log 保持五字段基础形态:event_type/direction/topic/payload_json/occurred_at_ms)

// ③ 高频遥测(business_status 广播;1Hz;内层键 snake)
{ "v": 1, "type": "device.status", "sn": "BCZJ0000...50T",
  "data": { "seq": "1024", "timestamp_ms": "1757568000123",
    "status": { "motion_realtime": { "command_position": { "x": 10.5, "y": 0, "z": 2, "b": 0, "c": 0 }, "...": "..." },
      "machining_realtime": { "program_line": 42, "progress_pct": 36.5, "elapsed_s": 1024.0,
        "spindle_speed_rpm": 12000.0, "spindle_temperature_c": 31.0, "feed_rate": 500.0 } } } }

// ③' 连接状态(EMQX webhook 产;与③同 type,按 data 形状区分:含 online 键)
{ "v": 1, "type": "device.status", "sn": "BCZJ0000...50T",
  "data": { "sn": "BCZJ0000...50T", "online": true, "protocol": "mqtt" } }

// ④ 低频状态(business_state 广播;0.1Hz + 变更即推;内层键 snake)
{ "v": 1, "type": "device.state", "sn": "BCZJ0000...50T",
  "data": { "seq": "104", "timestamp_ms": "1757568000123",
    "state": { "summary_status": { "display_state": "DISPLAY_STATE_READY", "since_ts": 1757567000 },
      "controller_status": { "...": "见 §8.3" } } } }

// ⑤ 影子报告(shadow.reported)
{ "v": 1, "type": "shadow.reported", "sn": "BCZJ0000...50T",
  "data": { "reported": { "...": "整帧 status/state/hms 快照" }, "version": 57 } }

message.log cmd 帧增强字段(仅 topic 为 cmd/req|ack|progress|result 的帧携带;其余通道五字段基础形态):command_id(解析自 payload,无则省略)/ command_type / execution_id(回帧取帧值,cmd/req 派生 exec-{command_id})/ payload(结构化对象:cmd/req=指令 payload 原样;ack=execution_id 字符串;progress={phase,progress_permille,stage};result={success,error})/ sequence(帧内 sequence→command_seq→seq 首命中,数字)/ boot_id(该 SN 当前 boot 作用域)/ timestamp(帧内 timestamp_ms→received_at_ms→finished_at_ms 首命中,int64 字符串)。增强失败静默降级为基础五字段(审计行永不丢)。

command.lifecycle timeline 边全集(按实际出现顺序):request / downlink / awaiting_response / ack / progress×n / result / timeout / response_missing / late_result / duplicate_request / status / publish_failed / parse_error / unknown_command。可省键一律省略不置 null;timestamp 为 int64 字符串。

FE 实时面板消费口径(参照 admin-fe/user-fe commandCards 模块):cmd 类 message.log 帧(结构化 command_id 非空)按 command_id 聚合为命令卡片(全生命周期:请求→下发→ACK→进度→终态),command.lifecycle 帧为权威时间线(终态章优先,推知终态粘滞不回退),command.status 终态作兜底;非命令消息(business_* 等)保留流式行展示 boot_id/sequence/timestamp 列。观测面板四栏位:指令(BY 指令聚合)/ HMS / State / Status 观测流。

---

8. HMS/Status/State 字段字典

8.1 三帧通道与顶层字段

设备经 MQTT 周期上行三帧,云端全量落库(device_events,event_type=business.status|state|hms)并 WSS 广播(§7.3 ③④);HMS 另进告警区间聚合(current/summary 查询面)。APP 消费路径:WSS 实时帧 / REST 快照与历史查询。

通道 频率 顶层键 载荷键 WSS type
business_status 1Hz seq,timestamp_ms status device.status
business_state 0.1Hz + 变更即推 seq,timestamp_ms,boot_id?,uptime_s? state device.state
business_hms 1Hz seq,timestamp_ms hms (落库+HMS 聚合;无独立 WSS type)

顶层字段语义:

8.2 BusinessStatus(高频状态位·motion_realtime + machining_realtime)

整帧样例(模拟器真实出厂帧):

{
  "seq": "1024", "timestamp_ms": "1757568000123",
  "status": {
    "motion_realtime": {
      "command_position": { "x": 10.5, "y": 20.25, "z": 3.0, "b": 0.0, "c": 0.0 },
      "actual_position":  { "x": 10.501, "y": 20.251, "z": 3.001, "b": 0.0, "c": 0.0 },
      "work_position":    { "x": 0, "y": 0, "z": 0, "b": 0, "c": 0 },
      "work_actual_position": { "x": 0, "y": 0, "z": 0, "b": 0, "c": 0 },
      "joint_position":   { "x": 0, "y": 0, "z": 0, "b": 0, "c": 0 }
    },
    "machining_realtime": {
      "program_line": 42, "progress_pct": 36.5, "elapsed_s": 1024.0,
      "spindle_speed_rpm": 12000.0, "spindle_temperature_c": 31.0, "feed_rate": 500.0,
      "spindle_load": 42.0, "rapid_rate": 8000.0
    }
  }
}

字段字典(proto BusinessStatus snake 终态;标 ※ 为模拟器增量字段,proto 外、影子原样保留):

路径 类型 业务含义
status.motion_realtime.command_position AxisPosition 五轴指令绝对坐标(x/y/z/b/c,单位 mm/deg)
status.motion_realtime.actual_position AxisPosition 五轴实测绝对坐标
status.motion_realtime.work_position AxisPosition 指令工件坐标
status.motion_realtime.work_actual_position AxisPosition 实测工件坐标
status.motion_realtime.joint_position AxisPosition 物理关节坐标
status.machining_realtime.program_line int32 当前执行程序行号
status.machining_realtime.progress_pct double 行基进度 0..100
status.machining_realtime.elapsed_s double 已加工时长(秒)
status.machining_realtime.spindle_speed_rpm double 主轴实时转速 RPM(ESTOP 时 0)
status.machining_realtime.spindle_temperature_c double 主轴温度 ℃
status.machining_realtime.feed_rate double 实时进给速度(机器单位/分钟)
status.machining_realtime.spindle_load double 主轴负载(proto 外,模拟器增量)
status.machining_realtime.rapid_rate double 快移速率(proto 外,模拟器增量)

数值无效值约定(proto 保留):int32 无效=2147483647;int64/double 无效=9007199254740991——接收方计算/展示前必须检查。

8.3 BusinessState(低频状态位·运行态与元数据)

整帧样例:

{
  "seq": "104", "timestamp_ms": "1757568000123", "boot_id": "boot-...", "uptime_s": 3600,
  "state": {
    "summary_status": { "display_state": "DISPLAY_STATE_READY", "since_ts": 1757567000.0 },
    "controller_status": {
      "connection": "CONNECTION_STATE_CONNECTED",
      "estop": "ESTOP_STATE_RELEASED",
      "enable": "ENABLE_STATE_ENABLED",
      "mode": "CONTROLLER_MODE_IDLE",
      "task_state": "CONTROLLER_TASK_STATE_IDLE",
      "interp_state": "INTERPRETER_STATE_IDLE"
    },
    "task_status": {
      "machining": { "phase": "MACHINING_PHASE_IDLE", "result": "TASK_RESULT_NONE",
                     "started_ts": 0, "finished_ts": 0 },
      "homing": { "phase": "HOMING_PHASE_IDLE", "result": "TASK_RESULT_NONE", "axis": "AXIS_NONE" },
      "self_test": { "phase": "SELF_TEST_PHASE_IDLE", "result": "TASK_RESULT_NONE" },
      "auto_center": { "phase": "AUTO_TASK_PHASE_IDLE", "result": "TASK_RESULT_NONE" },
      "auto_tool": { "phase": "AUTO_TASK_PHASE_IDLE", "result": "TASK_RESULT_NONE" }
    },
    "safety_status": {
      "drive": { "state": "DRIVE_AGGREGATE_STATE_NORMAL" },
      "soft_limit": { "active": false },
      "door": { "position": "DOOR_POSITION_CLOSED", "changed_ts": 1757566000.0,
                "interlock": "INTERLOCK_STATE_ARMED", "paused": "BOOL_STATE_FALSE" },
      "mql_interlock": { "state": "INTERLOCK_STATE_ARMED" },
      "hardware_estop": { "active": "BOOL_STATE_FALSE", "occurred": "BOOL_STATE_FALSE" }
    },
    "process_status": {
      "spindle": { "state": "SPINDLE_PHASE_OFF", "commanded_on": "BOOL_STATE_FALSE",
                   "enabled": "BOOL_STATE_FALSE", "override": 1.0 },
      "feed": { "override": 1.0, "override_enabled": "BOOL_STATE_TRUE", "hold": "BOOL_STATE_FALSE" },
      "current_tool": { "number": 3, "length": 52.1, "radius": 3.175 },
      "work_coordinate_system": { "g5x_index": 1, "rotation_xy_deg": 0.0 }
    },
    "peripheral_status": {
      "mql": { "connection": "PERIPHERAL_CONNECTION_CONNECTED", "relay": "PERIPHERAL_RELAY_OFF",
               "available": true },
      "handwheel": { "connection": "CONNECTION_STATE_CONNECTED", "enable": "ENABLE_STATE_ENABLED",
                     "axis": "AXIS_X", "rate": "HANDWHEEL_RATE_X1", "scale": 0.1 },
      "timelapse": { "connection": "CONNECTION_STATE_CONNECTED", "stream": "STREAM_STATE_OFF" },
      "ambient_light": { "available": true, "enabled": "BOOL_STATE_TRUE" },
      "work_light": { "available": true, "on": "BOOL_STATE_TRUE" },
      "beep": { "available": true }
    },
    "machine_metadata": { "boot_id": "boot-...", "protocol_version": "1.0.0",
                         "active_axes": ["AXIS_X", "AXIS_Y", "AXIS_Z", "AXIS_B", "AXIS_C"],
                         "capabilities": ["MACHINE_CAPABILITY_MACHINING", "MACHINE_CAPABILITY_TIMELAPSE"] },
    "program_metadata": { "selected_path": "/programs/demo.ngc", "loaded_path": "/programs/demo.ngc",
                         "total_lines": 1204 },
    "motion_state": {
      "homing": "BOOL_STATE_FALSE",
      "axes_homed": { "x": "BOOL_STATE_TRUE", "y": "BOOL_STATE_TRUE", "z": "BOOL_STATE_TRUE",
                     "b": "BOOL_STATE_TRUE", "c": "BOOL_STATE_TRUE" },
      "all_homed": "BOOL_STATE_TRUE"
    },
    "config_status": { "door_pause_enabled": true, "mql_pause_on_low": false,
                      "timelapse_server_url": "", "timelapse_actual_url": "",
                      "spindle_overtemp_threshold_c": 60.0 }
  }
}

字段字典(proto BusinessState,键=snake 终态;recovery_status 为 proto 字段,参考实现样本常省略):

路径 业务含义 取值(枚举全集)
summary_status.display_state 设备总显示态(不含故障明细) DISPLAY_STATE_UNKNOWN/OFFLINE/ESTOP/FAULT/SELF_TESTING/HOMING/PAUSE/MACHINING/DISABLED/READY
summary_status.since_ts 当前显示态起始时刻(Unix 秒) double
controller_status.connection 控制器连接 CONNECTION_STATE_UNKNOWN/DISCONNECTED/CONNECTED
controller_status.estop 急停状态 ESTOP_STATE_UNKNOWN/RELEASED/ACTIVE
controller_status.enable 使能状态 ENABLE_STATE_UNKNOWN/DISABLED/ENABLED
controller_status.mode 控制模式 CONTROLLER_MODE_UNKNOWN/MANUAL/AUTO/MDI
controller_status.task_state LinuxCNC 任务态 CONTROLLER_TASK_STATE_UNKNOWN/ESTOP/ESTOP_RESET/OFF/ON
controller_status.interp_state 解释器态 INTERPRETER_STATE_UNKNOWN/IDLE/READING/PAUSED/WAITING
controller_status.recovery_status 急停恢复流程态 RECOVERY_STATUS_UNKNOWN/NONE/RUNNING/SUCCEEDED/FAILED
task_status.machining.phase/result 加工任务阶段/结果 phase:MACHINING_PHASE_UNKNOWN/IDLE/READY/RUNNING/PAUSE;result:TASK_RESULT_NONE/SUCCEEDED/FAILED/CANCELLED(各任务同构,含 started_ts/finished_ts Unix 秒;homing 另有 axis,self_test 另有 items[]{name,progress,state}
task_status.homing/auto_center/auto_tool/self_test.phase 回零/自动对中/自动对刀/自检阶段 *_PHASE_UNKNOWN/IDLE/RUNNING
safety_status.drive.state 驱动聚合态 DRIVE_AGGREGATE_STATE_UNKNOWN/NORMAL/FAULT
safety_status.soft_limit.active 软限位触发 bool(axes 细分:SOFT_LIMIT_DIRECTION_NONE/MIN/MAX
safety_status.door.position/changed_ts/interlock/paused 安全门位置/变更时刻/联锁/门致暂停 position:DOOR_POSITION_UNKNOWN/CLOSED/OPEN/ERROR;interlock:INTERLOCK_STATE_UNKNOWN/DISABLED/ARMED/BLOCKING/TRIGGERED;paused:BoolState
safety_status.mql_interlock.state MQL 低液位联锁 InterlockState 同上
safety_status.hardware_estop.active/occurred 硬件急停输入 当前/本次运行曾发生 BoolState
process_status.spindle.state/commanded_on/enabled/override 主轴相位/指令开启/使能/倍率 state:SPINDLE_PHASE_UNKNOWN/OFF/STARTING/RUNNING/COASTING;override 1.0=100%
process_status.feed.override/override_enabled/hold 进给倍率/倍率启用/进给保持 override 1.0=100%;hold:BoolState
process_status.current_tool.number/length/radius 当前刀号(0=无刀)/长度补偿 mm/半径补偿 mm int32/double
process_status.work_coordinate_system.g5x_index/rotation_xy_deg 工件坐标系(1=G54…)/XY 旋转角 int32/double
peripheral_status.mql.connection/level/relay/available MQL 连接/液位/继电器/能力在位 level:MQL_LEVEL_UNKNOWN/NORMAL/LOW;relay:RELAY_STATE_UNKNOWN/OFF/ON
peripheral_status.handwheel.* 手轮连接/使能/当前轴/速率/每脉冲移动量 rate:HANDWHEEL_RATE_UNKNOWN/X1/X10/X100;scale double
peripheral_status.timelapse.connection/stream 延时摄影服务连接/推流态 stream:STREAM_STATE_UNKNOWN/OFF/ON
peripheral_status.ambient_light.available/enabled 环境灯在位/开启 bool/BoolState
peripheral_status.work_light.available/on 工作灯在位/开启 bool/BoolState
peripheral_status.beep.available 蜂鸣器在位 bool
machine_metadata.boot_id/protocol_version/active_axes/capabilities 启动标识(持久化记忆)/协议版本/生效轴/能力集 capabilities:MACHINE_CAPABILITY_MACHINING/HOMING/SELF_TEST/TIMELAPSE/MQL/HANDWHEEL/AMBIENT_LIGHT/WORK_LIGHT/BEEP
program_metadata.selected_path/loaded_path/total_lines 选中/已装载程序路径与总行数 string/int32
motion_state.homing/axes_homed{}/all_homed 回零中/各轴已回零/全部已回零 BoolState(axes_homed 键小写轴名)
config_status.door_pause_enabled/mql_pause_on_low 开门暂停/MQL 低水位暂停策略 bool(驾驶舱开关位)
config_status.timelapse_server_url/timelapse_actual_url/spindle_overtemp_threshold_c 延时服务配置地址(配置/实际)/主轴过温阈值 string/double

注:常驻模拟器出厂 state 帧为该结构的稳定子集,个别安全位以布尔镜像形态上报(safety_status.chunk_door/lamp_on/mql_on/feed_override_pct/spindle_override_pct)——云端按 snake 全量落库原样保留,FE 按本表规范键实现并对未知键宽容。

8.4 BusinessHms(故障信息)

整帧样例(安全门开 + 急停叠加态):

{
  "seq": "2048", "timestamp_ms": "1757568000123",
  "hms": {
    "alarm_status": {
      "severity": "ALARM_SEVERITY_CRITICAL",
      "active_codes": ["DOOR_OPEN", "ESTOP_ACTIVE"],
      "items": [
        { "code": "DOOR_OPEN", "severity": "ALARM_SEVERITY_ERROR", "message": "safety door open",
          "source": "safety_status.door", "occurred_at": 1757568.0,
          "first_seen_ts": 1757568.0, "last_seen_ts": 1757568.0 },
        { "code": "ESTOP_ACTIVE", "severity": "ALARM_SEVERITY_CRITICAL", "message": "emergency stop engaged",
          "source": "controller_status.estop", "occurred_at": 1757568.0,
          "first_seen_ts": 1757568.0, "last_seen_ts": 1757568.0 }
      ]
    },
    "task_diagnostics": [], "drive_diagnostics": [],
    "peripheral_diagnostics": [], "controller_diagnostics": []
  }
}

无警报时:alarm_status = {severity:"ALARM_SEVERITY_NONE", active_codes:[], items:[]},四个诊断数组恒在(空数组)。

字段字典(proto BusinessHms,snake 终态):

路径 业务含义
hms.alarm_status.severity 当前活动告警最高严重级:ALARM_SEVERITY_UNSPECIFIED/NONE/INFO/WARNING/ERROR/CRITICAL
hms.alarm_status.active_codes[] 活动告警码集合(空=已知无警)
hms.alarm_status.items[].code 告警码(如 DOOR_OPENMQL_LOW_LEVELESTOP_ACTIVESELF_TEST_RUNNING 或厂商数字码)
hms.alarm_status.items[].severity/message/source 级别/人读文本/来源状态路径(如 safety_status.door
hms.alarm_status.items[].first_seen_ts/last_seen_ts/occurred_at 首见/末次确认/发生时刻(Unix 秒,double)
hms.task_diagnostics[] 任务类诊断(回零/自检/加工运行错误);条目=同 AlarmItem 形
hms.drive_diagnostics[] 驱动诊断(带 axis 轴名 + error_code CiA402/厂商码,0=正常 + description
hms.peripheral_diagnostics[] 外设诊断(手轮/延时服务等)
hms.controller_diagnostics[] 控制器/采集诊断(含 controller last_error)

查询面(告警区间聚合,REST):GET /api/v1/hms/{sn}/current[{sn,error_code,first_seen_at,last_seen_at,resolved_at?}]GET /api/v1/hms/{sn}/summary?from&to[{error_code,occurrences,active_count,total_active_seconds,first_seen_at?,last_seen_at?}]

8.5 命令→状态联动语义(模拟器出厂行为,真实设备同契约)

指令 状态收敛(观察点)
ESTOP ENGAGE controller_status.estop=ACTIVEenable=DISABLED、status 主轴/进给=0、summary_status.display_state=ESTOP、HMS 增 ESTOP_ACTIVE(CRITICAL)
ESTOP RESET 上述反转,HMS 告警消除
HOME 轴坐标归零、motion_state.axes_homed→TRUE、all_homed=TRUE
JOG/MDI motion_realtime.command_position 向目标移动;MDI 可令主轴旋转(8000RPM 档)
CNC_CTRL START mode=AUTOtask_state=ONinterp_state=READINGprogress_pct/program_line 推进
AUTO_TASK / SELF_TEST task_status 对应段 phase=RUNNING;自检期间 HMS 增 SELF_TEST_RUNNING(INFO)
LIGHT/MQL peripheral_status.work_light.on/mql.relay 翻转(EnumState,非布尔)
SAFETY_POLICY_SET config_status.door_pause_enabled/mql_pause_on_low 翻转(bool)
门开(设备侧场景) safety_status.door.position=OPEN + HMS DOOR_OPEN(ERROR)

8.6 查询面(REST 投影)

端点 返回
GET /api/v1/shadow/{sn} {sn, reported(整帧快照), desired, version}
admin GET /api/admin/iot/devices/{sn}/snapshot {sn,last_report_at,status,state,hms,version}(三帧最新值,回落 shadow;排序/鲜度按 received_at 权威列)
GET /api/v1/devices/{sn}/events?event_type&… 事件分页({id,sn,event_type,source,command_id?,payload_json,occurred_at,received_at})——status/state/hms 历史唯一来源
admin 报表 list_status_reports/list_state_reports/list_hms_reports(时间窗分页)、list_current_hms/list_hms_summarylist_message_logs(EMQX 投递台账)

---

9. 红线清单

  1. 凭证纪律publish_token/upload_token/object_key 只存在于设备命令载荷;任何 APP 响应/日志/落盘不得出现。viewer_token 绑定 viewer_identity——joinRoom userId 逐字用返回值。
  2. 键名纪律:请求/响应/WSS 帧/命令信封全 snake(camel 违约服务端不识别);仅 timelapse 媒体载荷与加工账本详情嵌套段为记录在案的 camel 豁免域。int64 值按字符串消费(number 亦接受)。
  3. 离线≠冲突:503/42007 提示"设备离线";409/42003 才是业务冲突(msg 可操作,照读给用户)。
  4. admin 命令必须先建 WSS 在场(§2.2 三步),操作全程保持订阅在线;412/42028 = 没盯设备就下发,不是故障。
  5. SEEK 先 GOP 2s 量化再发;429/42900 自觉退避到窗口结束。
  6. 裸指令必须显式 mode(INSTANT/PERSISTENT);INSTANT 无 progress,别等进度帧。
  7. 命令结果是事件驱动的:以 WSS command.status/command.lifecycle 帧 + REST 查询为准,不要本地猜超时(服务端 timeout_at 会兜底推送 timeout 帧);断档恢复走轻量 REST(详情自带 progress 快照三列),不依赖帧重放。
  8. 重试语义:重试同 command_id = 取回既有命令行(不重发设备);幂等键请求级唯一;relay 幂等语义="同资产返回既有任务",终态后重看=新建。
  9. FE 幂等契约:UI 状态按 command_id 幂等收敛——终态粘滞不可回退(lifecycle 权威 > status 推知),重复/乱序帧无害;command.status/command.lifecycle 只会到达 watch 了该 command_id 的连接(先声明再等帧)。
  10. asset_version 对位:操作前重取详情;42024=版本已前进。
  11. content 只拉一次(pull 即 COMPLETED);重看走新建任务。
  12. WSS token 只走子协议头;query/Authorization 传 token 一律 401 missing_token
  13. APP 永不直连 MQTT;一切设备语义经 iot-be 命令/查询面。
  14. 终态后的迟到事件是留痕不是错误——不要把 *_skipped_terminal/late_result 当异常处理。
  15. 离场归因如实上报:unsubscribe body 只发 user_closed/background_playback_timeout 两值(或不发=缺省 user_closed);服务端专属值伪造=400。

---

附录 A. 源码出处索引(终态)

契约 文件
C 端命令端点/裸 TranslatedCommandResponse infimaker_iot_be/.../api/CommandController.java
admin 命令端点 + 412 在场前置 infimaker_iot_be/.../api/AdminDeviceCloudController.java
命令请求 DTO(六键 snake)/ 响应 DTO(14 键含 progress 快照三列) .../dto/CommandDispatchRequest.javaCommandResponse.java@JsonNaming(SnakeCase)
dispatch 流程(command_id 去重/限流/幂等/在线/可行性/queued/publish fail-closed) .../application/command/CommandService.java(dispatch/acceptDuplicateDispatch)
6 态状态机(R32 幂等推进/终态拒绝/超时/response_missing/late_result) .../domain/CommandStateMachine.javaCommandStatus.java
cmd/req 信封(command_id/mode/command_type/payload) CommandService.publishToDevice
cmd/ack·progress·result 上行解析(只认 snake) .../application/command/CmdAckMessageHandler.javaCmdProgressMessageHandler.javaCmdResultMessageHandler.javaCommandProtocolFields.java
WSS 帧类型注册与 watch 路由面 .../dto/ClientFrame.javaWATCH_ROUTED_TYPES)、infrastructure/ws/LocalWsFrameSender.java
watch 声明(上限/替换语义) .../api/ClientWebSocketHandler.java(declareWatches)、.../application/command/CommandWatchService.java
message.log 帧增强(cmd 结构化字段) .../application/command/CommandLogFrameEnricher.javaapplication/audit/MessageLogService.java
command.lifecycle 闭环帧 .../application/command/CommandLifecycleReporter.java
转译注册表双 lane / 五转译器(薄载荷 snake/物权/SN 错位防护) .../application/command/CommandTranslationRegistry.java.../application/timelapse/TimelapseCommandTranslators.java
playback Create/Seek/Stop 结果与设备载荷(camel 媒体豁免域) .../application/timelapse/PlaybackSessionService.java
relay CreateResult/幂等重放/并发/版本/file-upload·compose-product 载荷 .../application/timelapse/RelayTaskService.java
compose ComposeResult/载荷/离线 FAILED 留痕 .../application/timelapse/ComposeTaskService.java
bind-codes request(REST→INSTANT binding.request,payload snake)/latest 视图 .../api/DeviceDirectoryController.java.../application/binding/BindingService.java
leave_reason 分类法(白名单/400/服务端归因) .../application/media/LeaveReason.javaAdminLiveController.javaLiveController.java
错误码登记簿 42xxx / 异常→信封映射 .../api/IotErrorCodes.javaIotExceptionHandler.java
通用错误码 / 信封 v2 结构 infimaker_common/.../errorcodes/ErrorCodes.javaenvelope/ApiEnvelope.java
WSS 端点/子协议鉴权/身份矩阵/帧协议 .../api/ClientWebSocketAuthInterceptor.javaClientWebSocketHandler.javaconfig/WebSocketConfig.java
business 三帧解析(snake only)/落库/WSS 广播 `.../application/business/BusinessStatus State HmsMessageHandler.javaBusinessReportSupport.java`
seq 消费告警(regressed/gap/boot_changed/restart_signature) .../application/business/BusinessSeqMonitor.java
device.status 连接态帧 / shadow.reported 帧类型 .../application/device/DeviceConnectivityService.java.../application/shadow/ShadowService.java
业务事件结构化日志(SLS 键) infimaker_common/.../BusinessEvents.java + iot-be 各域调用点
proto 字段字典(Status/State/Hms/枚举全集) infimaker_iot_be/src/main/proto/infimaker/iot/v1/business_{status,state,hms,common}.proto(纯参考文档,D1(b))
设备侧帧出厂值(build_status/state/hms snake、SCENARIOS、ack/progress/result 构造) infimaker_upper_machine/src/infimaker_upper_machine/domain/machining.pybusiness_report.pyinfimaker_device_simulator/src/device_simulator/simulator.pytimelapse.py
FE 已集成清单 infimaker_user_fe/src/features/timelapse/api.tssrc/features/devices/iotApi.tscommandCards.tsinfimaker_admin_fe/src/services/deviceCloud.tssrc/pages/DeviceCloud/catalog.tscommandCards.tsRemoteMachining.tsx

附录 B. 机读交付包

同目录 promotion-app-iot-commands.md:本册的纯协议机读版(通道 → 命令四体全 JSON → 时序 → 错误码表),AI Agent 可直接摄取实现 APP 侧;与本册差异为零(同源同口径)。

附录 C. 历史过程存档(裁决与迁移记录)

正文为最终目标态契约;以下为达到该形态的关键裁决与迁移过程,留档备查(详细过程见 15.06 附录·执行记录)。

C.1 命令面统一(2026-09-10,iot-be 0.0.147)

C.2 veRTC 凭证滚动续期(2026-09-11 裁决)

C.3 D6 全协议 snake 迁移(2026-09-13,X4/X5/X7/X8 批)

C.4 命令状态机与幂等终局(2026-09-13,D2/D3/D4 裁决)

C.5 时间与序列模型(2026-09-13,D5 裁决)

C.6 WSS 观测面(2026-09-13,D7 裁决 + X11/X12/X13/X14 批)

C.7 观测与断言治理(2026-09-13,P0 治理)