下行命令

接口

  • POST /api/v1/open/downlink/commands:受理一条云端到设备命令并返回平台受理结果。

交互模型

001 的下行命令接口实现 同步受理、异步投递事件创建
  1. 接入方提交命令。
  2. 平台完成签名鉴权、nonce 防重放、租户解析、Casbin 授权、请求校验和幂等判断。
  3. 平台在同一个 PostgreSQL 事务中写入 command fact 和 dispatch event。
  4. 平台返回 command_idACCEPTEDaccepted_atrequest_id
  5. 事务提交后,API 服务尝试发布 NATS 事件 tenant.<tenant_id>.command.accepted
202 Accepted 只表示平台已受理并持久化命令,不表示设备已经执行成功。

命令事件与状态模型

Open Platform 把“可订阅事件”和“命令状态”分开建模。当前可订阅的 command 事件是:
event_type含义
command.accepted平台已经完成鉴权、授权、幂等校验和持久化,命令进入受理事实。
command.completedadapter 或设备返回终态结果,事件 payload 携带具体执行结果。
command.problemadapter 在命令边界发现无法安全关联到具体命令的诊断问题,例如 malformed accepted-command payload。
不要把每个状态都理解成一个独立 callback 事件。命令是否成功、失败、超时、不支持或被拒绝,体现在 command.completed 的 payload 以及命令查询响应中。command.problem 不表示正常命令失败;它会作为设备 recent event 保留原始诊断 payload。 命令查询响应中的聚合状态使用 public_status
public_status
ACCEPTED
DISPATCHING
DELIVERED
RUNNING
SUCCEEDED
FAILED
TIMED_OUT
UNSUPPORTED
CANCELLED
UNKNOWN
adapter 侧处理状态使用 adapter_status
adapter_status
RECEIVED
REJECTED_BY_ADAPTER
SENT_TO_DEVICE
TRANSPORT_FAILED
RESPONSE_TIMEOUT
设备侧执行状态使用 device_execution_status
device_execution_status
ACCEPTED_BY_DEVICE
SUCCEEDED
TEMPORARILY_REJECTED
DENIED
UNSUPPORTED
TIMED_OUT
PX4 adapter 当前在 command.completed.status 中返回的原始终态包括:
status
rejected_by_agent
sent_to_px4
ack_accepted
ack_temporarily_rejected
ack_denied
ack_unsupported
ack_timeout
transport_failed
parameter_value_confirmed
parameter_error
parameter_response_mismatch
parameter_timeout
平台会把这些 adapter 原始状态映射为查询响应中的 adapter_statusdevice_execution_statuspublic_status PX4 adapter 的查询响应还会在 adapter_result 中返回:
字段说明
human_summaryagent 生成的中文可读终态摘要。
hardware_responsePX4 硬件响应 typed payload。MAV_CMD 使用 COMMAND_ACK,参数读写使用 PARAM_VALUEPARAM_ERROR;mission、FTP、timesync 接入时分别使用 MISSION_ACKFILE_TRANSFER_PROTOCOLTIMESYNC typed payload。
source_evidenceagent 采用的 PX4/MAVLink 本地源码证据路径和说明。
示例:
{
  "adapter_result": {
    "public_status": "SUCCEEDED",
    "reason_code": "parameter_value_confirmed",
    "human_summary": "PX4 已确认 COM_RC_IN_MODE 设置为 MAVLink only。",
    "hardware_response": {
      "source": "px4_mavlink",
      "message_name": "PARAM_VALUE",
      "payload": {
        "kind": "parameter_value",
        "param_id": "COM_RC_IN_MODE",
        "param_value": 1,
        "param_type": "MAV_PARAM_TYPE_INT32",
        "readable_value": "MAVLink only"
      }
    },
    "source_evidence": [
      {
        "source": "px4",
        "path": "docs/px4/src/modules/mavlink/mavlink_parameters.cpp"
      }
    ]
  }
}

鉴权

  • 必须通过开放平台签名鉴权:X-Api-IdX-Api-TimestampX-Api-NonceX-Api-Signature
  • X-Request-Id 可选;未传时平台生成并在响应与日志中返回
  • 创建命令需要租户域内授权:open:command:create
  • 签名计算示例见 鉴权与签名

请求体

FieldTypeRequiredDescription
vendorstringyes厂商标识,如 dji
device_idstringyes目标设备 ID
command_typestringyes命令类型,必须使用平台支持清单中的值
payloadobjectyes命令负载
idempotency_keystringyes幂等键
timeout_secondsintegerno超时秒数;缺省为 30,有效范围 1~300
payload 不是任意对象。它必须匹配对应厂商和 command_type 的命令协议。DJI 全量命令 payload 字段、JSON 示例和代码演示见 命令 Payload
{
  "vendor": "dji",
  "device_id": "drone-001",
  "command_type": "camera_mode_switch",
  "payload": {
    "payload_index": "52-0-0",
    "camera_mode": 0
  },
  "idempotency_key": "req-20260422-0001",
  "timeout_seconds": 30
}

响应

  • 202 Accepted:首次受理成功
  • 200 OK:命中幂等重放,返回已存在命令
  • 4xx/5xx:返回标准错误结构
成功示例:
{
  "command_id": "100e2826-e45b-50cb-b41f-e8df6d20bea2",
  "status": "ACCEPTED",
  "accepted_at": "2026-04-22T12:00:00Z",
  "request_id": "req-20260422-0001"
}

幂等语义

  • 幂等作用域为认证后的 tenant_id + client_id + idempotency_key
  • 同一作用域内,相同业务请求体重放时返回同一 command_id
  • 同一作用域内,相同幂等键但不同业务请求体返回 IDEMPOTENCY_CONFLICT
  • 语义哈希包含 vendordevice_idcommand_type、规范化后的 payload 和规范化后的 timeout_seconds
  • 语义哈希不包含 X-Request-IdX-Api-NonceX-Api-Timestamp 或签名头

持久化与投递语义

命令受理响应表示平台已经把 command fact 与 dispatch event 写入同一个 PostgreSQL 事务。事务提交后,API 服务会立即尝试向 NATS 发布:
tenant.<tenant_id>.command.accepted
如果事务已提交但 NATS 发布失败,命令仍保持 ACCEPTED。平台会保留 dispatch event 及失败原因,供后续恢复流程补偿。

常见错误

codeHTTP说明
UNAUTHORIZED401缺少或无效的认证身份
SIGNATURE_INVALID401签名不匹配
TIMESTAMP_EXPIRED401请求时间戳超过允许偏差窗口
NONCE_REPLAYED401nonce 在重放窗口内重复使用
FORBIDDEN403当前客户端无租户域内权限
TENANT_NOT_FOUND404无法解析有效租户
TENANT_BINDING_INVALID403客户端与租户绑定关系无效
INVALID_REQUEST_BODY400JSON 请求体非法
VALIDATION_FAILED400字段缺失、类型非法或超出范围
UNSUPPORTED_VENDOR400厂商未在平台命令目录中注册
UNSUPPORTED_COMMAND400命令类型不受支持或不在可信能力范围内
IDEMPOTENCY_CONFLICT409同一幂等键对应不同语义请求
INTERNAL_ERROR500平台内部错误

示例

下面示例只演示调用结构。生产环境必须按照 鉴权与签名 计算 X-Api-Signature
BODY='{"vendor":"dji","device_id":"drone-001","command_type":"camera_mode_switch","payload":{"payload_index":"52-0-0","camera_mode":0},"idempotency_key":"req-20260422-0001","timeout_seconds":30}'
TIMESTAMP="$(date +%s)"
NONCE="$(uuidgen)"
SIGNATURE="YOUR_CALCULATED_SIGNATURE"

curl -X POST "https://dev.utmos.dev/api/v1/open/downlink/commands" \
  -H "Content-Type: application/json" \
  -H "X-Api-Id: YOUR_API_ID" \
  -H "X-Api-Timestamp: $TIMESTAMP" \
  -H "X-Api-Nonce: $NONCE" \
  -H "X-Api-Signature: $SIGNATURE" \
  -d "$BODY"