发送命令
本指南介绍双向命令派发中运维人员的一侧:发出命令、区分接受与拒绝,并跟踪结果。设备一侧的接收与结果报告见连接设备。两侧共同推进的生命周期见命令。
使用租户访问令牌,在 command-delivery 端点 https://<your-host>/api/command-delivery/graphql 发出、读取和取消命令。发出和取消需要 command:write,读取历史需要 command:read。
下面第一步是唯一例外。查询设备接受什么是 device-management 查询,使用不同端点 https://<your-host>/api/device-management/graphql 和权限 device:read。
查询设备接受什么
设备命令集合来自配置文件,因此应询问 device-management,不要猜测:
query {
deviceCommandVocabulary(deviceToken: "sensor-001") {
constrained
commands { commandKey name description parameterSchema }
}
}
commandKey 是标识符,name 是标签PublishedCommand 同时携带两者。接受/拒绝检查匹配 commandKey,应将它放入 createCommand 的字段,尽管该字段令人困惑地叫作 name。集合条目的 name 只是显示标签,不参与匹配。发送标签会对设备明明支持的命令返回 COMMAND_NOT_IN_VOCABULARY。
读取 constrained,而不是根据 commands 长度判断:
constrained: false:列表为空,接受任何命令键。空列表不是不接受命令,而是配置文件没有声明命令集合。constrained: true:键必须精确匹配某个条目,包括大小写。载荷按该命令参数模式验证。
发出命令
mutation {
createCommand(request: {
token: "6f1c0f8e-6d1e-4a1a-9a3f-1f2b0d0a5c11",
deviceToken: "sensor-001",
name: "reboot", # the commandKey, not the display name
payload: "{\"delaySeconds\":5}",
expiresAt: "2026-08-15T00:00:00Z"
}) {
command { token status queuedTime }
rejection { code reason }
}
}
token由你选择,之后用于引用命令。payload和metadata是 JSON 字符串。expiresAt可选,参见设置合适的 TTL。
使用已有令牌重新发出,不会创建第二条命令,而是原样返回原命令。因此网络故障后的重试安全。命令是物理控制操作,响应丢失不能导致设备重启两次。
这种重放只适用于你拥有的命令。如果令牌属于平台为批次创建的命令,返回 TOKEN_IN_USE。将另一台设备的操作当作你的命令返回,比拒绝更糟。
入队被拒绝时
rejection,不只是检查错误createCommand 恰好返回 command 或 rejection 之一。入队拒绝是携带 rejection 的成功 GraphQL 响应,不是 GraphQL 错误。只检查 errors 数组的客户端会将拒绝视为成功,报告从未创建的命令。
这种区分是有意的。拒绝是已确定判断:请求有误,拒绝说明具体原因。GraphQL 错误表示平台根本无法回答。无法区分的机器调用方会不断重试永久无效命令,直到重投递上限耗尽,表现与故障相同。
根据 code 分支,绝不根据 reason。原因是给人看的文字,措辞可能变化。
code | 含义 | 是否重试? |
|---|---|---|
HELD_CEILING_EXCEEDED | 租户达到未投递命令上限:全部 QUEUED、HELD、PARKED,不只离线设备保留命令。 | 是,这些命令投递后会解除 |
WINDOW_AFTER_EXPIRY | 命令具有破坏性,其设备类型的维护窗口在 expiresAt 之后才开启,因此只会过期。 | 否:延长 TTL 或修改窗口 |
DEVICE_NOT_FOUND | 此租户没有该令牌设备。 | 否 |
DEVICE_DISABLED | 设备已停用。命令不会保留到以后。 | 否 — 先启用设备 |
COMMAND_NOT_IN_VOCABULARY | 配置文件限制命令,而此键不在集合中。检查大小写。 | 否 |
PAYLOAD_SCHEMA_VIOLATION | 载荷违反参数模式:未知参数、错误类型、超范围或缺少必填参数。 | 否 |
PAYLOAD_NOT_JSON / METADATA_NOT_JSON | 字符串不是有效 JSON。 | 否 |
PAYLOAD_TOO_LARGE / METADATA_TOO_LARGE | 载荷或元数据字符串超过 64 KiB。 | 否 |
COMMAND_NAME_TOO_LONG | 命令名称超过 128 字节。 | 否 |
EXPIRES_AT_INVALID | expiresAt 不是 RFC3339 时间戳。 | 否 |
TOKEN_IN_USE | 令牌由不属于你的命令占用,实际通常是平台为批次创建的命令。 | 否,请换令牌 |
COMMAND_REJECTED | 收到没有分类的拒绝。 | 否 |
列表不是封闭的。未知代码应视为无法分类的拒绝,绝不视为成功。
只有 HELD_CEILING_EXCEEDED 是临时情况。其他代码都说明请求下次仍有相同错误。重试浪费次数,并向能修复的人隐藏真实缺陷。
即使整个设备群在线,也可能达到上限。上限约束未投递工作,排队命令等待下一投递扫描时也计入。参见租户可以保留多少积压。
跟踪结果
命令没有订阅,因此需要轮询。按令牌获取命令:
query {
commandsByToken(tokens: ["6f1c0f8e-6d1e-4a1a-9a3f-1f2b0d0a5c11"]) {
token status sentTime respondedTime responsePayload error
}
}
也可以搜索,用 status 筛选单个状态,或 statuses 筛选一组状态:
query {
commands(criteria: {
pageNumber: 1, pageSize: 50,
deviceToken: "sensor-001",
statuses: ["HELD", "PARKED", "SENT"]
}) {
results { token name status queuedTime }
pagination { totalRecords }
}
}
关注状态集合时使用 statuses。“此设备所有仍在处理的命令”包括:
HELD:设备缺席或已停用,因此保留。PARKED:已发布,但设备实际未唤醒。SENT:已派发,尚无答复。
空 statuses 列表会被忽略,而不是匹配零条。
各终态含义见命令。要记住的区别是:EXPIRED 表示未到达设备,TIMEOUT 表示已发出。连续 EXPIRED 指向派发,连续 TIMEOUT 指向设备。
取消命令
mutation {
cancelCommand(token: "6f1c0f8e-6d1e-4a1a-9a3f-1f2b0d0a5c11") { token status }
}
可以从 QUEUED、HELD 和 PARKED 取消,即平台仍持有的状态,记录为 CANCELLED。适用情况包括因设备缺席而保留,或已发布但设备实际休眠。两者都可以在投递前取消,这正是保留而不是盲目发送的主要意义。
SENT 命令不会取消。 取消无法召回已派发命令。将它改为 CANCELLED 不会阻止任何执行,只会使平台丢弃之后到达的真实答案。设备执行、响应消失、记录却说操作已取消。因此调用成功,但原样返回仍为 SENT 的命令。取消与投递竞争,未抢先属于正常情况。
取消终态命令也不是错误。它按已达到的状态原样返回。因此,取消输给响应的情况,看起来是成功调用返回 SUCCESSFUL。
两种情况下都应检查返回的 status,不要假定取消已生效。没有匹配命令的令牌会产生错误。
cancelCommandBatch 对整个设备群操作采用完全相同的刹车:取消同样状态,并停在 SENT 边界。参见取消批次。
为已停用设备保留的命令
设备被停用后,此前已为它排队的命令会被保留:进入 HELD,不发布,也不取消。设备重新启用后,若设备在线,它们被释放并按从旧到新的顺序发出;若设备启用时仍离线,命令会继续保持 HELD,直到设备重新连接,与任何离线设备相同。等待期间,命令仍会在各自的 expiresAt 到期,记录为 EXPIRED。这种保留只在设备被启用时结束,这也是它与“为仅仅离线的设备保留的命令”的区别。
要查看保留了什么,查询设备的保留情况。heldCount 是设备尚未发送的所有命令(QUEUED、HELD 或 PARKED)的数量,已启用设备为 0:
query {
deviceCommandHold(deviceToken: "pump-17") { disabled heldCount }
}
要改为丢弃它们,请取消平台仍为该设备持有的全部命令。它对每条命令适用与 cancelCommand 相同的规则,因此 SENT 命令绝不会被触及,并报告取消了多少条:
mutation {
cancelDeviceCommands(deviceToken: "pump-17") { cancelled }
}
控制台在设备的 Commands 选项卡显示同样内容:“N 条排队命令因设备已停用而被保留”,并为有命令写入权限的运维人员提供 Cancel all 按钮。
设置合适的 TTL
每条命令都带有 TTL。传入 expiresAt 设置,否则使用平台默认七天。
等待七天才得知命令失败很久。如果设备不报告结果,命令会整周停在 SENT,才由 TIMEOUT 确认你早已怀疑的情况。按该操作“仍有用途”的时间设置 expiresAt,十分钟内未执行的重启通常不会再执行。
仅限部分客户的成员
成员对租户的访问可以限定为你的部分客户。这样的成员只能处理分配给这些客户的设备:
- 读取。
commands、commandsById和commandsByToken返回设备分配给该成员某个客户期间发给它的命令。命令会记录发送时设备分配给了谁,因此之后重新分配设备,不会移动已发给它的命令。实例开始记录这一信息之前发送的命令,不会显示给这样的成员。 - 发送。 持有
command:write时,成员可以向当前分配给其某个客户的设备发送命令。其他任何设备都以DEVICE_NOT_FOUND拒绝,措辞与不存在的设备完全相同,无论该命令还有什么其他问题。 - 保留。
deviceCommandHold只对当前分配给该成员某个客户的设备作答,并统计为其保留的全部命令。其他任何设备都以NOT_FOUND失败。 - 其他所有操作都以
FORBIDDEN失败:取消命令,以及所有批量命令操作。
未投递命令上限(HELD_CEILING_EXCEEDED)统计的是整个租户的命令,而不只是该成员的命令。
同时命令多台设备
上述操作都是向一台设备发一条命令。要向显式指定或从实体组解析的整个设备群发送同一命令,作为可审计、可取消的单个操作,参见向设备群发送命令。设备群命令不是此变更操作的循环。它固定触发时分组成员关系,记录被拒绝设备及原因,并作为整体取消。
五种不面向你的操作
模式中有 markCommandSent、confirmCommandDispatch、releaseHeldCommands 和 parkCommand,但它们受租户访问令牌不携带的系统级权限控制:前两者需要 command:claim,后两者分别为 command:wake、command:park。它们供拥有设备连接的传输使用:
- LwM2M 设备通过刚建立的会话排空积压;
- LwM2M 适配器在执行前确认投递仍有效;
- 消息代理报告设备返回;
- 传输发现目标设备不可达,将命令退回。
应用调用这些操作,会与平台投递流程争夺物理操作控制权。
drainableCommands 是这些传输先进行的读取。它由 command:claim 控制,与 markCommandSent 相同,而不是再新增一项权限:有权认领设备命令的调用方,也正是有权查询待认领命令的调用方。给定设备令牌,它返回仍等待该设备的 HELD 和 PARKED,排除过期命令,按最旧优先排列,并受 limit 限制。省略或非正 limit 使用 32,上限 1000。
顺序正是该查询的目的。固件写入必须先于执行到达设备,其他顺序不仅是延迟,而是倒着执行更新流程。
command:read 不能使用此查询,应用也不需要它。查看设备等待内容时,使用 commands 查询,配合 statuses: ["HELD", "PARKED"]。