Run-time supervision for async Rust firmware
The graph wires static and runtime coupling to fit any design.
embassy-supervisor is run-time supervision for complex embedded systems. Model the firmware as a checked graph of tasks, resources, and control points. The supervisor brings it up in order, keeps it consistent through failures, and exposes every node to runtime control. For industrial, automotive, and safety-critical products where wiring tasks together is the hard part.
---
config:
layout: elk
---
flowchart TD
accDescr: Runtime view of the demo firmware's supervised graph, with signals and resources
n_NET["NET<br/>Terminate · task"]:::task
n_HTTP[["HTTP<br/>pool ×2 · task · HTTP_FLOOR..HTTP_CEIL"]]:::pool
n_WATCHDOG["WATCHDOG<br/>Terminate · task"]:::task
n_HEARTBEAT["HEARTBEAT<br/>Pause · task · @HIGH · beat 15000"]:::paused
n_OTA["OTA<br/>Terminate · task"]:::disabled
n_BENCH["BENCH<br/>Terminate · task · @CORE1"]:::disabled
n_OTA_CONFIRM["OTA_CONFIRM<br/>Terminate · task"]:::task
n_NET -. "spawn · ready" .-> n_HTTP
n_NET -. "spawn · ready" .-> n_OTA
n_HEARTBEAT -. "spawn · ready bound" .-> n_BENCH
n_HTTP -. "spawn" .-> n_OTA_CONFIRM
n_NET -. "spawn · ready" .-> n_OTA_CONFIRM
s_crate__net__STACK[/"crate::net::STACK"/]:::signal
s_heartbeat__PERIOD_MS[/"heartbeat::PERIOD_MS"/]:::signal
n_NET -- "discovered" --> s_crate__net__STACK
n_HTTP -- "discovered" --> s_heartbeat__PERIOD_MS
s_crate__net__STACK -- "discovered" --> n_OTA
s_crate__net__STACK -- "gated" --> n_OTA_CONFIRM
s_heartbeat__PERIOD_MS -- "discovered" --> n_HEARTBEAT
r_USB_DEV@{ shape: notch-rect, label: "USB_DEV" }
r_USB_DEV:::resource
r_HTTP_STATS@{ shape: notch-rect, label: "HTTP_STATS" }
r_HTTP_STATS:::resource
r_NET_STACK@{ shape: notch-rect, label: "NET_STACK" }
r_NET_STACK:::resource
r_WD_DEV@{ shape: notch-rect, label: "WD_DEV" }
r_WD_DEV:::resource
r_LED@{ shape: notch-rect, label: "LED" }
r_LED:::resource
r_FLASH_DEV@{ shape: notch-rect, label: "FLASH_DEV" }
r_FLASH_DEV:::resource
r_USB_DEV --> n_NET
r_HTTP_STATS --> n_HTTP
r_NET_STACK -- "local · shared" --> n_HTTP
r_WD_DEV --> n_WATCHDOG
r_LED --> n_HEARTBEAT
r_FLASH_DEV --> n_OTA
n_NET -- "provides" --> r_NET_STACK
executor HIGH;
executor CORE1;
node WATCHDOG = Terminate, task: crate::watchdog::watchdog_task,
resources: [WD_DEV: embassy_rp::watchdog::Watchdog];
node HEARTBEAT = Pause, executor: HIGH,
task: crate::heartbeat::heartbeat_task,
beat_timeout: 15000, discover,
resources: [LED: embassy_rp::gpio::Output<'static>];
node NET = Terminate, task: crate::net::net_task,
dataflow: [crate::net::publish_stack],
provides: [NET_STACK],
resources: [USB_DEV: embassy_rp::Peri<'static, embassy_rp::peripherals::USB>];
node OTA = Terminate, deps: [NET ready], task: crate::ota::ota_task,
dataflow: [crate::net::lease_stack],
resources: [FLASH_DEV: embassy_rp::Peri<'static, embassy_rp::peripherals::FLASH>],
disabled;
node BENCH = Terminate, deps: [HEARTBEAT ready bound], executor: CORE1,
task: crate::bench::bench_task, exit: u32, disabled;
node OTA_CONFIRM = Terminate, deps: [HTTP, NET ready], task: crate::ota_confirm,
dataflow: [crate::net::stack_ready];
pool HTTP = [Terminate, OnDemand], deps: [NET ready],
task: crate::http::http_task,
resources: [HTTP_STATS: crate::http::WorkerStats,
NET_STACK: shared local embassy_net::Stack<'static>],
dataflow: [crate::heartbeat::set_period_ms],
The diagram above is what this declaration compiles into. Full source infirmware/src/main.rs.
What the supervisor takes over
Ordered start, reverse teardown
Dependencies become a compile-time order. Starts and stops run as waves that overlap what can overlap, so a sensor does not queue behind a slow radio, and a provider keeps serving while its dependents shut down.
A lifecycle per task
Terminate respawns clean. Pause parks a task holding its bus or socket. OnDemand members grow only when a pool needs them.
The graph reacts at runtime
Epochs let a running task notice its provider restarted; bound edges stop dependents when a provider drops and bring them back with it; restart re-gates one subtree without a reset.
Reads that wait, holds that count
Opening a signal can start its producer and wait for it; leased handles are counted so nothing frees a value a consumer still holds. Ordering can live entirely in the runtime couplings, with no task dependencies at all.
Declared, even derived
Say who writes and who reads each signal, or let the attribute derive it from the code. One-sided and dead signals get caught at build time.
Driven from anywhere
Start, stop and restart nodes or whole subtrees from a request handler, a button or a test, through a lossless mailbox.
Workers that follow load
An elastic pool grows under pressure and shrinks after a cooldown, inside a member budget you set in the declaration.
A sweep catches what stalls
Heartbeat budgets turn wedged tasks into reported events, readiness means "actually producing", and each node can describe itself in one line. Escalation stays yours.
Where it fits
Battery sensor nodes
Wake, publish, sleep. The graph that powers bring-up also drives the cycle: no per-loop glue, no manual tear-down, the same declaration runs every wake.
Connected gateways
Add a service to the graph and it joins bring-up. Remove one and it tears down cleanly. Runtime reconfiguration becomes a graph edit, not a code change.
Robotics and motion
A preemptive control tier and a cooperative telemetry tier share one graph. Mission-phase start and stop are normal lifecycle transitions, and the runtime keeps both tiers ordered.
OTA-first devices
An update is just a different graph state. Drain live services, free their memory, swap the image, roll back. The same declaration drives boot and update alike.
Data loggers
Sample, buffer, and flush to a shared card. The teardown order is the reverse of bring-up, so the writer closes before the mount goes down. Stalls surface before they fill the buffer.
Factory test rigs
The same bring-up order runs on every unit. A failed subsystem shows up as one readable line and gets restarted in isolation, so one bad device does not stop the line.
Hardware variants
Nodes that aren't on this board compile out and cost nothing. Optional drivers start on first read instead of at boot. The graph stays the same across the product line.
Audio and video nodes
Pipeline stages come up in strict order, worker pools scale with the load, and a stalled stage is reported before the buffer underruns. One declaration, one runtime.