Synchronise a device's reported and desired state with the cloud's
authoritative shadow document. This module wraps the AWS IoT Device Shadow
service and adds optional KV-backed persistence so a device can resume from
its last known state across reboots.
See the AWS IoT Device Shadow documentation for the service specification (named shadows, classic vs named, deltas).
- Define your shadow type with the
#[shadow_root]and#[shadow_node]attribute macros fromrustot_derive. The macros generate aReported,Desired, andDeltarepresentation, plus FNV1A hashes for change-detection. - Pick a
StateStore—InMemoryfor ephemeral,FileKVStore(std-only),SequentialKVStore(flash viasequential-storage), or your own. - Construct a
ShadowwithShadow::new(&store, &mqtt). - Drive the lifecycle:
load— repopulate from the KV store on boot.create_shadow— initialize the cloud shadow on first run.sync_shadow— pull the cloud's current state.wait_delta— block until the cloud sends a delta; apply it.update_reported— push device-side changes to the cloud.commit— flush dirty fields back to the KV store.
| Direction | Purpose | Topic |
|---|---|---|
| Pub | Get shadow | $aws/things/{Thing}/shadow/get |
| Sub | Get reply | $aws/things/{Thing}/shadow/get/accepted / .../rejected |
| Pub | Update shadow | $aws/things/{Thing}/shadow/update |
| Sub | Update reply | $aws/things/{Thing}/shadow/update/accepted / .../rejected |
| Sub | Delta from cloud | $aws/things/{Thing}/shadow/update/delta |
| Sub | Documents (full) | $aws/things/{Thing}/shadow/update/documents |
| Pub | Delete shadow | $aws/things/{Thing}/shadow/delete |
| Sub | Delete reply | $aws/things/{Thing}/shadow/delete/accepted / .../rejected |
Named shadows replace /shadow/ with /shadow/name/{shadowName}/.
use rustot::shadows::{InMemory, Shadow};
use rustot_derive::{shadow_node, shadow_root};
#[shadow_root]
#[derive(Default)]
struct DeviceState {
network: Network,
sensors: Sensors,
}
#[shadow_node]
#[derive(Default)]
struct Network {
ssid: heapless::String<32>,
rssi_dbm: i16,
}
#[shadow_node]
#[derive(Default)]
struct Sensors {
temperature_c: f32,
occupied: bool,
}
async fn run<C: rustot::mqtt::MqttClient>(mqtt: &C) {
let store = InMemory::<DeviceState>::default();
let shadow = Shadow::new(&store, mqtt);
// First boot: initialize cloud-side shadow with the local default.
shadow.create_shadow().await.ok();
// Pull cloud state so we converge before publishing anything.
let _ = shadow.sync_shadow().await;
loop {
// Block until the cloud requests a change.
let (state, delta) = shadow.wait_delta().await.unwrap();
if let Some(delta) = delta {
// Apply the delta to local hardware here.
log::info!("delta requested: {:?}", delta);
shadow.update_reported(state).await.ok();
}
}
}For periodic publishing of sensor changes alongside delta handling, drive
wait_delta and update_reported from independent tasks against the same
Shadow.
With the shadows_kv_persist feature, fields can be persisted individually
(field-level dirty tracking) to any KVStore. Backing implementations:
| Store | Feature | Notes |
|---|---|---|
InMemory |
always available | RAM only; no persistence. |
FileKVStore |
std + shadows_kv_persist |
One file per field; useful in tests / std targets. |
SequentialKVStore |
sequential_storage |
Flash-backed via sequential-storage. |
| Bring your own | shadows_kv_persist |
Implement KVStore for whatever you have. |
load returns a LoadResult indicating how many fields were restored,
defaulted, or migrated. commit returns CommitStats for telemetry.
With the shadows_multi feature (std-only), crate::shadows::multi
provides a manager for runtime-named shadows with wildcard subscriptions —
useful when the device tracks an unbounded set of shadow instances (e.g. one
per attached peripheral).
| Feature | Default | Effect |
|---|---|---|
shadows_kv_persist |
yes | Field-level KV persistence traits and FileKVStore (std). |
sequential_storage |
no | Flash-backed SequentialKVStore. Implies shadows_kv_persist. |
shadows_builders |
no | Generate bon builder methods on shadow structs (downstream must add bon). |
shadows_multi |
no | Runtime-named shadows with wildcard subscriptions (std-only; AWS SDK). |
- Persistent sessions recommended. Without them, deltas published while the device is offline are lost.
- One unnamed (classic) shadow per
Shadowinstance. Use named shadows or theshadows_multimanager for multiple. - Field migrations are explicit. Renaming a field requires a migration
hook (
MigrationSource); the macros do not silently rename.
Unit tests cover topic format/parse, codegen output for the derive macros, and KV round-trips:
cargo test --lib shadows::The integration test (tests/shadows.rs) runs through delete / update / get
sequences against a real AWS IoT endpoint:
RUST_LOG=trace \
THING_NAME=MyTestThing \
AWS_HOSTNAME=xxxxxxxx-ats.iot.eu-west-1.amazonaws.com \
cargo test --test shadows --features "log"tests/secrets/identity.pfx must hold a valid AWS IoT identity for the
thing; IDENTITY_PASSWORD may be set if the file is password-protected.
pfx files can be built from a certificate + private key with:
openssl pkcs12 -export \
-out identity.pfx \
-inkey private.pem.key -in certificate.pem.crt -certfile root-ca.pemThis test runs as a CI integration test in Factbird's AWS account on every PR.