Skip to content

Device RPC & commanding

Telemetry flows device→server. RPC flows the other way: the platform sends a command to a device — set a fan speed, read back a config, reboot, start a firmware update. This page covers the three command types, the status lifecycle every command moves through, how responses are correlated, what happens when the device is offline, and the authorization and audit rules around commanding.

TypeWaits for a result?Survives an offline device?
One-wayNo — fire and forgetNo (delivered only if online)
Two-wayYes — returns the device’s responseNo (times out if offline)
PersistentOptionallyYes — stored and delivered on reconnect, retried
  • One-way commands are dispatched and forgotten. Use them for idempotent actuations where you don’t need a reply: setFan, setLed.
  • Two-way commands carry a timeout_ms and return the device’s response inline. Use them to read state back: getConfig, getStatus.
  • Persistent commands are stored durably and delivered when the device next connects, retried until they succeed, are cancelled, or expire. Use them for must-happen actions to intermittently connected fleets — including OTA, which rides on persistent RPC.

Every command is an RPC record that moves through a defined set of statuses:

stateDiagram-v2
  [*] --> queued : RPC created
  queued --> sent : adapter publishes to device
  sent --> delivered : device ACKs receipt
  delivered --> successful : device returns result
  sent --> successful : one-way / fast result

  queued --> timeout : non-persistent, device offline
  sent --> timeout : no response before timeout_ms
  delivered --> failed : device reports an error

  queued --> expired : persistent, expiration_time passed
  sent --> queued : persistent retry (no ack)

  queued --> cancelled : operator cancels
  sent --> cancelled : operator cancels

  successful --> [*]
  timeout --> [*]
  failed --> [*]
  expired --> [*]
  cancelled --> [*]

The full status set: queued, sent, delivered, successful, timeout, expired, failed, cancelled, deleted.

  • queued — record created, not yet on the wire.
  • sent — the protocol adapter published the command to the device’s command topic/channel.
  • delivered — the device acknowledged receipt.
  • successful — the device returned a result; it lands in response_json.
  • timeout — no response within timeout_ms (or a non-persistent command for an offline device).
  • failed — the device executed but reported an error (error_code is set).
  • expired — a persistent command’s expiration_time passed before delivery.
  • cancelled — an operator cancelled it before completion.

Every command carries a correlation_id. The protocol adapter stamps it on the outbound message; the device echoes it on its response; the adapter matches the response back to the originating RPC record by that id. This is what lets a device — or a whole fleet over a shared MQTT response topic — reply asynchronously and have each answer routed to the right command.

A response whose correlation_id matches no active RPC is ignored (or stored as an orphan diagnostic, per adapter config), and corem_rpc_orphan_responses_total{protocol="…"} increments.

What happens to a command for an offline device depends entirely on whether it is persistent.

No adapter can deliver the command. After timeout_ms elapses the request returns DEADLINE_EXCEEDED / HTTP 504 and the RPC record becomes timeout.

sequenceDiagram
  participant Op as Operator
  participant Core as CORE-M
  participant Dev as Device (offline)
  Op->>Core: SendDeviceRpc (non-persistent)
  Core-->>Core: status=queued, device offline
  Note over Core: timeout_ms elapses
  Core->>Op: DEADLINE_EXCEEDED / 504 (status=timeout)
  Dev--xCore: never reached

A persistent command’s expiration_time is a hard deadline. Once it passes, the RPC scheduler marks the record expired, no command is delivered even if the device later reconnects, and event rpc.expired.{tenant}.{rpc_id} is published. Set expiration_time to the longest you’d still want the action to happen — a reboot you queued days ago is probably no longer wanted.

  • Permission. RPC requires the rpc permission. A member without it is rejected with PERMISSION_DENIED and no record is created.
  • Token scope. A scoped API token must carry rpc:execute. A token scoped to, say, telemetry:read calling RPC is rejected with PERMISSION_DENIED and rpc.denied records reason="missing_scope".
  • Audit. Creating an RPC writes audit event rpc.created with the actor, the device, the method, the persistent flag, and a hash of the params. If the method is marked sensitive, raw params are redacted — only the hash is kept.

Read a device’s config back with a two-way RPC. You send the command and get a correlation_id; you then fetch the matched response.

  1. Send the command. Call the device RPC endpoint with the method, JSON params, and a timeout. You need the rpc:execute scope (token) or rpc permission (user).

    Terminal window
    curl -X POST https://api.kronoxdata.com/api/v1/devices/dev_8f3a/rpc \
    -H "Authorization: Bearer <token-with-rpc:execute>" \
    -H "Content-Type: application/json" \
    -d '{
    "method": "getConfig",
    "params_json": "{}",
    "timeout_ms": 10000
    }'
  2. Receive the correlation id. The call returns immediately with the id you use to retrieve the result.

    { "correlation_id": "c-7f3a91" }
  3. The device replies. The adapter delivers getConfig to the device, the device responds {"report_interval":30} stamped with c-7f3a91, and the adapter matches it to the RPC record. The record transitions queued → sent → delivered → successful and the response lands in response_json.

  4. Fetch the result. Poll by correlation id.

    Terminal window
    curl https://api.kronoxdata.com/api/v1/devices/rpc/c-7f3a91 \
    -H "Authorization: Bearer <token-with-rpc:execute>"
    {
    "found": true,
    "payload_json": "{\"report_interval\":30}"
    }

The RPC record is transport-agnostic; the protocol adapter handles the actual delivery and response correlation.

ProtocolOutbound commandResponse correlation
MQTTPublished to the device’s configured command topicDevice publishes to a response topic; adapter matches by correlation_id
HTTPDevice fetches/long-polls; or device submits an uplink of kind="rpc_response"Uplink carries the correlation_id
CoAP / LwM2MDelivered over the device’s CoAP channelResponse matched by correlation_id / protocol request id
GatewayRelayed to a child device via its gatewayGateway relays the child’s response with correlation_id