Skip to content

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

Bring-up

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.

Modes

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.

Coupling

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.

Gated reads

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.

Dataflow

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.

Control

Driven from anywhere

Start, stop and restart nodes or whole subtrees from a request handler, a button or a test, through a lossless mailbox.

Pools

Workers that follow load

An elastic pool grows under pressure and shrinks after a cooldown, inside a member budget you set in the declaration.

Health

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

Field devices

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.

Hubs

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.

Control

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.

Products

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.

Recorders

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.

Production

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.

Product lines

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.

Streaming

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.