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 | result、business_status |
state | hms、timelapse/*、live/*` 等 |
APP 永不直连 MQTT;一切设备交互经 iot-be 中转 |
1.2 命令面两条 lane 与转译机制
命令端点收到请求后先查转译注册表(CommandTranslationRegistry):
- 命中(五条
timelapse.*业务动词)→ 服务端做领域裁决(物权/互斥/状态机/凭证铸造)+ 组装厚载荷下发设备 + 同步返回业务结果(裸TranslatedCommandResponse)。APP 只送薄载荷,凭证(publishToken/uploadToken)只出现在设备命令载荷里,响应里绝无设备凭证。 - 未命中(调试/FPV 指令台的
COMMAND_SERVICE_*)→ 裸命令:自构 payload 原样投递设备,同步返回CommandResponse(状态queued),业务结果靠 WSScommand.status/command.lifecycle帧 +GET /commands/{command_id}追踪。
| 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 字段,设备侧枚举同名):
INSTANT:一次性动作。设备立即cmd/ack→ 执行 →cmd/result(无 progress)。PERSISTENT:长任务。设备立即cmd/ack→ 按 SCENARIOS 时间线发cmd/progress×N →cmd/result。
云端状态机(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):
- progress 允许集 = {queued, delivered, executing}——progress 到达即设备已执行的隐式证据,从
queued幂等推进到executing,不拒绝(拒绝=丢进度→假超时)。 - result 同理:任一非终态收到 result 直达
succeeded/failed;仅三终态(succeeded/failed/timeout)拒绝为重复结果冲突(留痕*_skipped_terminal)。 INSTANT命令收到 progress → 状态机冲突(设备侧契约错误),落交互留痕。- 终态后的迟到 ack/progress/result:**只记录留痕(
*_skipped_terminal),不推进、不报错**;终态后晚到的 result 另落late_result对账边。 - 超时:
timeout_at(客户端timeout_seconds,0/负 → 服务端默认值兜底)到点由扫描器置timeout;另有无响应追查边response_missing(静默设备也得到结论性结局)。 - 去重: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_type(ack|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 获取与物权校验
- 取 token:登录走 auth-be(Auth0 托管域,PKCE;参照 user-fe
getAccessTokenSilently()静默续期)。该 JWT 即所有 C 端 iot 接口的 Bearer token。 - iot-be 侧解析:
Authorization: Bearer <JWT>→ iot-beJwtAuthFilter调 auth-be resolve → 得internalUserId(属性名iot.userId)。解析失败=未认证(401 / 40100)。 - SN 级准入(S1,fail-closed;两层权模型):命令端点先
DeviceAccessGuard.checkOwnership(userId, sn)——机主(物权)或控制权持有者(多人扫码获得iot_device_control_grant)均放行;两者皆无 → 403 /42009(DEVICE_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 拦截器同款逻辑):
- 响应 body 含
code键 → 信封:成功唯一判据code == 0(业务载荷在data);非 0 → 按code分支,msg展示、request_id留痕。 - 无
code键 → iot 裸 DTO:2xx 即成功。 - 命令端点专属: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.seek、relay-create→timelapse.file-upload、compose-create→timelapse.compose,其余同名直达):
- 下行
devices/{sn}/cmd/req:{"command_id","mode":"INSTANT","command_type","payload":{...}}(信封固定四键,snake;payload 内层为 timelapse 媒体域 camel 豁免) - 上行
devices/{sn}/cmd/ack:{"command_id":"...","status":"COMMAND_ACK_STATUS_ACCEPTED","execution_id":"exec-...","received_at_ms":"..."}(设备收到即回,永远第一帧) - 上行
devices/{sn}/cmd/progress:仅 PERSISTENT 命令;五条 timelapse 设备命令均为 INSTANT,无 cmd/progress(进度走领域通道:timelapse/relay、timelapse/compose) - 上行
devices/{sn}/cmd/result:{"command_id","success":true|false,"result":{...}}(失败可带"error":"...") - 领域上行帧:
devices/{sn}/timelapse/stream|relay|compose(camel·媒体豁免域,各指令小节给出)
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_PULL → GET .../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;命令面响应为裸 CommandResponse(status:"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>"]
- token 只走
Sec-WebSocket-Protocol子协议头(query/Authorization 均不被读取——防进网关日志/浏览器历史)。 - 路径身份矩阵:
/ws/v1/admin只认 admin 会话 token(用户 token → 401user_on_admin_path);/ws/v1/client认用户 JWT(admin token 过渡期容忍,iot.ws.client-path-strict=false时)。 - 浏览器页面天然满足 Origin;非浏览器客户端不带 Origin 或须命中 CORS 白名单。
- 握手限流 60 次/60s/IP(401
rate_limited);成功标志 101。
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.status 与 command.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_SN、MISSING_TYPE、UNSUPPORTED_TYPE、WATCH_INVALID、FORBIDDEN(admin 只许 subscribe/unsubscribe/ping;C 端非 owner SN 帧被拒,code 取物权异常稳定码 DEVICE_NOT_ACTIVATED/DEVICE_ACCESS_DENIED)。C 端其他 SN 帧类型(command、shadow.desired)同样先过物权校验。
7.3 服务端 → 客户端推送帧全集
统一帧形:{"v":1,"type":"...","sn":"...","data":{...}}。watch 路由面(command_id 定向):command.status、command.lifecycle;SN 扇出面(订阅该 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) |
顶层字段语义:
seq:单调递增生产者序号(int64 发字符串,number 亦接受);per-kind(status/state/hms 各自独立),boot_id为重启作用域。服务端消费语义:回退(business_seq_regressed)与跳号(business_seq_gap)只告警进 SLS 不拒绝;重启签名(seq 与 uptime_s 同步归零 →business_seq_restart_signature);boot 切换(business_boot_changed)。timestamp_ms:设备生产时刻 Unix 毫秒(双形态接受;缺省/非法时云端用接收时刻兜底)。boot_id/uptime_s:一次开机周期稳定(boot_id 设备持久化记忆,跨进程重启不变)/ 开机时长纯内存秒计数(进程重启归零)。- 时间双列:
occurred_at(机器时间,展示)+received_at(服务端接收时间,排序/快照/E2E 断言权威列)。 - 键名只认 snake:服务端解析 camel 键一律不识别(camel 违约 → 字段按缺失处理/
*_payload_invalid拒帧/时间回退 now,可观测不静默);APP 按 snake 键实现。
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_OPEN、MQL_LOW_LEVEL、ESTOP_ACTIVE、SELF_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=ACTIVE、enable=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=AUTO、task_state=ON、interp_state=READING、progress_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_summary、list_message_logs(EMQX 投递台账) |
---
9. 红线清单
- 凭证纪律:
publish_token/upload_token/object_key只存在于设备命令载荷;任何 APP 响应/日志/落盘不得出现。viewer_token绑定viewer_identity——joinRoom userId 逐字用返回值。 - 键名纪律:请求/响应/WSS 帧/命令信封全 snake(camel 违约服务端不识别);仅 timelapse 媒体载荷与加工账本详情嵌套段为记录在案的 camel 豁免域。int64 值按字符串消费(number 亦接受)。
- 离线≠冲突:503/42007 提示"设备离线";409/42003 才是业务冲突(msg 可操作,照读给用户)。
- admin 命令必须先建 WSS 在场(§2.2 三步),操作全程保持订阅在线;412/42028 = 没盯设备就下发,不是故障。
- SEEK 先 GOP 2s 量化再发;429/42900 自觉退避到窗口结束。
- 裸指令必须显式
mode(INSTANT/PERSISTENT);INSTANT 无 progress,别等进度帧。 - 命令结果是事件驱动的:以 WSS
command.status/command.lifecycle帧 + REST 查询为准,不要本地猜超时(服务端 timeout_at 会兜底推送 timeout 帧);断档恢复走轻量 REST(详情自带 progress 快照三列),不依赖帧重放。 - 重试语义:重试同
command_id= 取回既有命令行(不重发设备);幂等键请求级唯一;relay 幂等语义="同资产返回既有任务",终态后重看=新建。 - FE 幂等契约:UI 状态按 command_id 幂等收敛——终态粘滞不可回退(lifecycle 权威 > status 推知),重复/乱序帧无害;command.status/command.lifecycle 只会到达 watch 了该 command_id 的连接(先声明再等帧)。
asset_version对位:操作前重取详情;42024=版本已前进。- content 只拉一次(pull 即 COMPLETED);重看走新建任务。
- WSS token 只走子协议头;query/Authorization 传 token 一律 401
missing_token。 - APP 永不直连 MQTT;一切设备语义经 iot-be 命令/查询面。
- 终态后的迟到事件是留痕不是错误——不要把
*_skipped_terminal/late_result当异常处理。 - 离场归因如实上报: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.java、CommandResponse.java(@JsonNaming(SnakeCase)) |
||
| dispatch 流程(command_id 去重/限流/幂等/在线/可行性/queued/publish fail-closed) | .../application/command/CommandService.java(dispatch/acceptDuplicateDispatch) |
||
| 6 态状态机(R32 幂等推进/终态拒绝/超时/response_missing/late_result) | .../domain/CommandStateMachine.java、CommandStatus.java |
||
| cmd/req 信封(command_id/mode/command_type/payload) | CommandService.publishToDevice |
||
| cmd/ack·progress·result 上行解析(只认 snake) | .../application/command/CmdAckMessageHandler.java、CmdProgressMessageHandler.java、CmdResultMessageHandler.java、CommandProtocolFields.java |
||
| WSS 帧类型注册与 watch 路由面 | .../dto/ClientFrame.java(WATCH_ROUTED_TYPES)、infrastructure/ws/LocalWsFrameSender.java |
||
| watch 声明(上限/替换语义) | .../api/ClientWebSocketHandler.java(declareWatches)、.../application/command/CommandWatchService.java |
||
| message.log 帧增强(cmd 结构化字段) | .../application/command/CommandLogFrameEnricher.java、application/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.java、AdminLiveController.java、LiveController.java |
||
| 错误码登记簿 42xxx / 异常→信封映射 | .../api/IotErrorCodes.java、IotExceptionHandler.java |
||
| 通用错误码 / 信封 v2 结构 | infimaker_common/.../errorcodes/ErrorCodes.java、envelope/ApiEnvelope.java |
||
| WSS 端点/子协议鉴权/身份矩阵/帧协议 | .../api/ClientWebSocketAuthInterceptor.java、ClientWebSocketHandler.java、config/WebSocketConfig.java |
||
| business 三帧解析(snake only)/落库/WSS 广播 | `.../application/business/BusinessStatus | State | HmsMessageHandler.java、BusinessReportSupport.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.py、business_report.py;infimaker_device_simulator/src/device_simulator/simulator.py、timelapse.py |
||
| FE 已集成清单 | infimaker_user_fe/src/features/timelapse/api.ts、src/features/devices/iotApi.ts、commandCards.ts;infimaker_admin_fe/src/services/deviceCloud.ts、src/pages/DeviceCloud/catalog.ts、commandCards.ts、RemoteMachining.tsx |
附录 B. 机读交付包
同目录 promotion-app-iot-commands.md:本册的纯协议机读版(通道 → 命令四体全 JSON → 时序 → 错误码表),AI Agent 可直接摄取实现 APP 侧;与本册差异为零(同源同口径)。
附录 C. 历史过程存档(裁决与迁移记录)
正文为最终目标态契约;以下为达到该形态的关键裁决与迁移过程,留档备查(详细过程见 15.06 附录·执行记录)。
C.1 命令面统一(2026-09-10,iot-be 0.0.147)
- 五类 timelapse 写操作从 REST POST 收编命令端点;
live.stop/binding.request同通道收编。 - admin 命令面 WSS 在场门(412/42028)对 timelapse 域豁免(本域全轮询驱动,不依赖 command.status 实时帧)。
C.2 veRTC 凭证滚动续期(2026-09-11 裁决)
- veRTC AccessToken TTL=15min,回放与 FPV 共用通道 → 续期协议不分域:publisher 走
live.publisher.renew设备命令(seek 同语义),viewer 走 heartbeat 回包字段(viewer_token/viewer_token_expires_at_epoch_seconds)。
C.3 D6 全协议 snake 迁移(2026-09-13,X4/X5/X7/X8 批)
- 迁移前形态:proto3-canonical camelCase 发射 + 服务端双键容错。裁决改判为全协议(WSS/MQTT/HTTP)统一 snake_case。
- 三步落地:① 服务端 snake 优先双读 + camel fallback SLS 观测;② 发射面全量切 snake(iot-be 下行/推送、两 FE 读键、常驻模拟器上行);③ 收口删双读容错——上行解析只认 snake,camel 违约帧 fail-fast(
parse_error/*_payload_invalid/时间回退 now,可观测)。 - 键映射要点:
timestampMs→timestamp_ms、commandId→command_id、receivedAtMs→received_at_ms、motionRealtime→motion_realtime、summaryStatus→summary_status、activeCodes→active_codes、firstSeenTs→first_seen_ts等(全表见 X4/X5 批报告)。 - cmd/progress 数值语义同步收敛:
percent(0..100)→progress_permille(0..1000);短词status弃用,phase发 proto 枚举名。 - 豁免/过渡域裁决:timelapse 媒体载荷(设备命令 payload 内层与 timelapse/* 上行帧)与加工账本详情嵌套段保持 camel;裸指令 payload 内层键(programPath/startLine/timeoutS/pauseEnabled 等)为 camel 过渡形态(设备侧容忍 snake 别名,未收口)。
C.4 命令状态机与幂等终局(2026-09-13,D2/D3/D4 裁决)
- D2(a):dispatch 层 command_id 级去重落地——重试返回既有行 +
duplicate_request对账边;onAck 消费 DUPLICATE_ACCEPTED 不做(设备侧 _seen DUPLICATE_ACCEPTED 回环因服务端不再重发而不可达)。 - D3(b):删除
cancelled死状态(原 7 态→6 态)——中止加工由既有 eStop 业务指令承担,命令状态机不做 cancel 生命周期;S26 场景取消。 - D4(b):终态后晚到 result 落
late_result对账边(不做 cancel 下行)。 - R32 语义(progress 允许集/隐式证据幂等推进)为 2026-09-05 权威裁决,已内化进状态机。
C.5 时间与序列模型(2026-09-13,D5 裁决)
- D5-a:
occurred_at双写——机器时间列(展示)+received_at服务端接收时间列(排序/快照/E2E 断言权威列)。 - D5-b:business seq 消费——per-kind 记上次 seq(boot_id 为重启作用域),回退/gap 只告警进 SLS(
business_seq_regressed/gap)不拒绝。 - D5-c:boot_id 持久化记忆(一次开机周期稳定、跨进程重启不变)+ uptime_s 纯内存秒计数;进程重启签名(seq 与 uptime_s 同步归零)进告警分类。
- D5-d:execution_id 一律派生
exec-{command_id}(INSTANT/PERSISTENT 同规),服务端不消费。
C.6 WSS 观测面(2026-09-13,D7 裁决 + X11/X12/X13/X14 批)
- D7-a:跨会话按 SN 扇出是设计非缺陷(命令生命周期归属 SN+command_id,不归属连接)。
- D7-b:WSS 连接级 watch 属性(
watch_command_ids)定向投递 command.status(否决帧内 initiator 回显方案);X11 扩展 command.lifecycle 同路由。 - D7-c:FE 幂等契约成文(本册 §9.9)。
- D7-d:断档恢复走轻量 REST(命令详情自带 progress 快照三列
last_phase/last_permille/last_stage),不引帧重放。 - X11/X12:message.log cmd 帧结构化增强 + command.lifecycle 闭环帧 + FE 命令卡片视图(按 command_id 聚合全生命周期)。
- X13/X14:live 离场归因分类法(leave_reason 五值 taxonomy)+ 两 FE unsubscribe 携带 reason + 后台 120s 自动退订。
- live.* WSS 帧面已于 2026-09-12 整体移除(FPV 事务/通知/续期全在 REST + 心跳响应)。
C.7 观测与断言治理(2026-09-13,P0 治理)
- 业务事件全结构化(BusinessEvents:message=事件名 + snake 键值字段;SLS 索引键
command_id/boot_id/sequence)。 - E2E 断言迁 SlsAssert 字段等值引擎(五规则),全文关键词查询通道删除(R17 收口)。