Node Parameters & Device↔Cloud Messaging
What this covers
The plumbing by which a RainMaker node’s parameters and configuration move between an application, the cloud, and the device firmware. There are two distinct data planes:
Device shadow — the AWS IoT named shadow that holds a node’s parameters. Firmware writes its live state into the shadow’s
reportedsection; an app reads that section to learn current state.to_cloudrequest/response channel — an MQTT topic on which firmware asks the cloud questions (“what group am I in?”, “what schedule version do you hold?”, “is Alexa enabled?”) and receives answers on a pairedfrom_cloudtopic, brokered by the to-cloud request handler.
A third, derived plane — indexed params (iparams) — mirrors a slice of the
reported shadow into DynamoDB so operators can query params without a
per-node shadow read.
This is a design/lifecycle companion to the user-facing
docs/05-device-app-messaging.md. It describes the wire shapes, topics, and
handlers, not the payload semantics of any particular feature carried over
them.
Out of scope (covered elsewhere)
Concern |
Where |
|---|---|
Connect/disconnect/ |
|
Group add/remove shadow migration (moving params between group-named shadows) |
|
The node-config schema itself ( |
config service |
Schedule / trigger payload content and versioning rules |
|
Alexa / Google Voice enablement logic |
|
This spec covers only how those payloads are transported and where the
handlers sit. Where a to_cloud event delegates to one of those features, the
event is listed here but its body is not described.
Actors
Actor |
Role |
|---|---|
Node (FW) |
Writes live parameter state into its |
App / client |
Sets a parameter by publishing a param-set intent to the device’s user-params topic. Reads current state from the |
AWS IoT Shadow service |
Stores named-shadow documents; merges partial updates; computes |
To-cloud request handler Lambda |
Consumes |
|
Routes |
|
Mirrors the |
Data plane A — device shadow (parameters)
Shadow model
A node’s shadow document follows the standard AWS IoT structure: a state with
a reported section (device → cloud state) and a desired section (app →
device intent, see note below), plus server-set metadata and version. Each
section can carry:
params— the per-device parameter maptyped tags — admin / device / user scopes
onlineand disconnect info — connection state (seenode_connection.md)
A node’s parameters live in a group-scoped named shadow, not the classic shadow. The name is derived from group membership:
params-<groupID>[-<subgroupID>...] // subgroups sorted alphabetically
Resolving the name fetches the node’s group if not already known and errors if
the node belongs to no group — an unassociated node has no param shadow. (The
mechanics of moving params between shadows on a group change are in
group.md / node_assoc.md.)
Reading current state (device → cloud)
Firmware publishes its live parameter values into the reported section of the
group shadow. An app reads them back by calling GetThingShadow on the
group-named shadow and returning the reported block. A missing shadow
(ResourceNotFoundException) is treated as empty, not an error.
A narrow accessor reads the reported shadow and returns a single device’s params as a map, or an empty map if the device or params are absent.
Reads are RBAC-gated: reading a node’s shadow requires read-shadow permission on the node.
Setting a parameter (app → device)
The desired-state intent is not written to the shadow’s desired section.
A helper to do so exists “for completeness” but is never used in practice.
Instead, the param-set intent is delivered as a direct MQTT publish to the
node’s user-params topic:
rainmaker/nodes/<thing>/user/<shadowName>/params
where <shadowName> = params-<groupID>[-<subgroupID>...]
The device is subscribed to this topic and applies the new param values, then
publishes the resulting state back into its reported shadow — which closes the
loop for any reader.
App publish params → rainmaker/nodes/<thing>/user/params-<grp>/params
Device applies params, updates its firmware state
Device write reported shadow (Params) via $aws/things/<thing>/shadow/name/params-<grp>/update
App/reader GetThingShadow(name=params-<grp>) → reads reported.Params
This param-set publish is the shared primitive behind the app-facing and integration surfaces — voice assistants, automations, and the MCP tool layer all route through it. Publishing requires publish-to-device permission; writing the reported shadow requires write-shadow permission.
Offline device behaviour. Because the intent is a plain MQTT publish and
not a shadow desired write, it is fire-and-forget: there is no
store-and-forward. The publish path does not check the node’s connection
state, so setting a parameter on an offline device does not fail and does not
report the device as unreachable — the call succeeds as long as the broker
accepts the publish. If the device is offline at that moment it simply never
receives the message, and — unlike a desired-shadow delta, which a device
reconciles on reconnect — there is nothing queued for it to pick up later, so the
change is lost rather than applied when it comes back. The reported shadow
therefore never updates, and a reader keeps seeing the previous value. Knowing a
device is offline is a separate concern: callers read the node’s online /
connectivity state (see node_connection.md) to judge whether delivery is
likely. Only stateful callers that publish and then wait for the
reported-shadow echo with a timeout surface an explicit unreachable/timeout
result — for example the Alexa / Google Voice control flow (see alexa.md). The
bare param-set primitive does not.
Shadow writes populate either the reported or desired section and call
UpdateThingShadow; the Shadow service merges the update into the stored
document. Partial updates merge (not replace), and a JSON null for a key
deletes it — clearing user tags relies on this by sending an explicit
{"data":{"user":{"t":null}}} that omit-empty marshaling would otherwise drop.
A field-level delta between an old and new shadow state can be computed where only the changed subset should be acted on.
Data plane B — the to_cloud request/response channel
Topics and routing
Firmware publishes a request to:
rainmaker/nodes/<thing>/to_cloud
The node_to_cloud_rule IoT rule matches rainmaker/nodes/+/to_cloud, projects
the thing name from the third topic segment and the full message body, and
targets the to-cloud request handler Lambda. Responses are published back to:
rainmaker/nodes/<thing>/from_cloud
at QoS 1. The handler holds iot:Publish only on
rainmaker/nodes/*/from_cloud.
Request shape and dispatch
The request body carries an event array — a list of named requests batched
into one message. The handler iterates the array, dispatches each name, and
accumulates results into a single response, which is published once at the end
(only when at least one event produced output).
Device → to_cloud { "event": ["getGroupInfo","getSchedVer"], ... }
Lambda dispatch each name → append to response
Lambda → from_cloud { "event": ["getGroupInfo","getSchedVer"],
"getGroupInfo": {...}, "getSchedVer": {...} }
If any of getAlexaEn, getGVAEn, getSchedVer, getSchedDetails,
getTriggerDetails, getTriggerVer appear, the handler reads the node’s
details once up front and reuses it across those events, avoiding a
per-event DB read.
Per-event errors are logged and the loop continues — one failing event does not abort the batch. Non-string entries are skipped.
Supported to_cloud event names
Event |
What it returns / does |
Feature spec |
|---|---|---|
|
Node’s parent group ( |
|
|
Echoes the supplied |
— |
|
Persists node config via the config service; replies |
config service |
|
Whether Alexa is enabled for the node |
|
|
Whether Google Voice is enabled |
voice-assistant spec |
|
Current schedule version held by the cloud |
|
|
Full schedule payload + version |
|
|
Current trigger version |
|
|
Full trigger payload + version |
|
|
Current server time as |
— |
An unrecognized event name is not fatal: it is logged as a warning — most often a firmware typo or an event not yet implemented — and the rest of the batch proceeds.
This spec enumerates the events and where they route; the content of the config/schedule/trigger/voice payloads belongs to those feature specs.
The hello / config-refresh pattern
A common firmware flow on (re)connect is to batch a hello with the version
queries (getSchedVer, getTriggerVer, getGroupInfo, getAlexaEn, …) in one
to_cloud message. The device compares the returned versions against what it
holds locally and, when they differ, issues a follow-up getSchedDetails /
getTriggerDetails to pull the full payload. The messaging layer imposes no
ordering beyond “one response per request batch”; the reconciliation logic lives
in firmware and the respective feature handlers.
Deployment modes — direct vs. scalable
The to-cloud request handler’s business logic is identical across two deployment modes, selected at build/deploy time; firmware and the wire protocol are unchanged between them.
Direct (default): the
node_to_cloud_ruleIoT rule invokes the handler Lambda directly — one invoke per message. IoT Core is grantedlambda:InvokeFunction.Scalable: an alternate deployment mode that consumes events via a queue. The rule writes to a Standard SQS queue (with a DLQ); the Lambda consumes batches through an event-source mapping. Each record is validated (non-empty thing name and data) and processed independently, returning per-record batch-item failures so only failed records are retried. Records arrive without ordering guarantees; the queue absorbs bursts that would otherwise fan out as concurrent direct invocations.
Data plane C — indexed params (iparams)
Alongside the group-scoped param shadow, each node has a fixed-name indexed
shadow named iparams. Its reported section is read and written like any
shadow section; it also carries the node’s typed tags (admin / device / user)
and is where user-tag clears operate.
To make params queryable without a per-node GetThingShadow, the
iparams_index_rule IoT rule mirrors this shadow into DynamoDB with no Lambda
in the path — a direct IoT→DynamoDB action. The rule matches the shadow’s
.../update/documents event topic, takes the node id from the third topic
segment, and projects the reported block from the shadow document.
Every time the iparams shadow is updated, the Shadow service emits an
.../update/documents event; the rule extracts the reported block and
PutItems it into the NODE_IPARAMS table (partition key node_id), so the
latest reported iparams for any node is a single-key read. The rule role holds
only dynamodb:PutItem on that table; failures route to a CloudWatch Logs error
action.
Device → $aws/things/<thing>/shadow/name/iparams/update (write reported)
Shadow → $aws/things/<thing>/shadow/name/iparams/update/documents (event)
Rule → PutItem NODE_IPARAMS { node_id: <thing>, iparams: {reported...} }
Query → GetItem NODE_IPARAMS by node_id
Topic reference
Topic |
Direction |
Purpose |
|---|---|---|
|
device → cloud |
Batched requests to the to-cloud request handler |
|
cloud → device |
Responses / cloud-initiated notifications |
|
app → device |
Parameter-set intent |
|
device → cloud |
Reported param state |
|
device → cloud |
Indexed params + tags |
|
shadow → rule |
Feeds |