Skip to main content

embassy_supervisor/
lib.rs

1// `no_std` for the shipped crate and the embedded build; under `cargo test` the
2// crate is built for the host, where the test harness and the unit tests need `std`.
3#![cfg_attr(not(test), no_std)]
4#![forbid(unsafe_code)]
5#![deny(missing_docs)]
6//! # embassy-supervisor — a task-lifecycle supervisor for [embassy](https://embassy.dev)
7//!
8//! Application- and HAL-agnostic primitives for orchestrating a set of embassy
9//! tasks: bringing them up in dependency order, tearing them down in reverse,
10//! scaling an elastic worker pool with load, placing nodes on interrupt-priority
11//! tiers or a second core, and starting/stopping/pausing/resuming individual
12//! tasks at runtime while keeping the dependency graph consistent. The supervisor
13//! orchestrates task *lifecycle* and leaves the rest — allocation, HAL, power,
14//! what the tasks do — to the application.
15//!
16//! ## The model
17//!
18//!   * The graph is declared once with the [`supervisor_graph!`] macro: each
19//!     managed task becomes a [`TaskNode`] `static`, and the macro bundles the node
20//!     slots, dependency table, and a topological order computed **at compile time**
21//!     into a single [`Graph`] (`GRAPH`). The whole graph is validated at compile
22//!     time — a dependency cycle, an unknown or duplicate dependency, a duplicate
23//!     name, or bad pool bounds are compile errors.
24//!   * [`Supervisor::new`] takes `&GRAPH` (no work, no failure) and uses the order
25//!     to bring tasks up in dependency order ([`Supervisor::start`]) and tear them
26//!     down in reverse ([`Supervisor::teardown`]).
27//!   * `executor NAME;` items declare runtime-filled [`SpawnerSlot`]s, and
28//!     `executor: NAME` on a node (or a whole pool) routes its spawn through one —
29//!     an interrupt-priority tier or the second core. Bring-up *awaits* the slot
30//!     (bounded), so an executor that comes up late — or on another core — is a
31//!     rendezvous, not a race.
32//!   * Each managed task names its worker with either `task:` (preferred) — a
33//!     **plain `async fn`** that the macro wraps in a generated
34//!     `#[embassy_executor::task]` shell (one concrete shell per declaration, so a
35//!     *generic* worker is fine) — or `spawn:`, naming a hand-written
36//!     `#[embassy_executor::task]` directly. A `task:` pool emits one shell sized to
37//!     its members; `pool_size: N` sizes a single node's shell.
38//!   * `resources: [NAME: Type, ..]` on a `task:` node threads **owned resources
39//!     from `main`** into the worker through macro-emitted [`ResourceSlot`]s —
40//!     compile-time exclusive ownership (the `Peripherals` field is consumed, no
41//!     `steal()` inside the task), fail-closed provisioning (an unprovided slot
42//!     fails `start` with `SpawnError::Busy`), and restore-on-exit so a respawn
43//!     re-takes the *same instance*. Per-entry kind markers refine that
44//!     default: `consume` hands the worker the value **by value** with no
45//!     restore (drop-at-teardown drivers; rebuilt-per-cycle resources — a
46//!     respawn fail-closes until the app re-`provide()`s); `shared` is a
47//!     fan-out slot for a `Copy` handle (the glue copies via
48//!     [`ResourceSlot::get`], the slot stays filled — any number of nodes and
49//!     whole pools may declare the same name); and `local` swaps in a
50//!     graph-site slot without the `T: Send` bound (`!Send` driver handles,
51//!     single-core contract) — it makes the macro emit an `unsafe impl Sync`
52//!     into the consuming crate, so it requires the non-default
53//!     `local-resources` feature. See the macro docs for the markers' fine
54//!     print.
55//!   * The pre-spawn waits are per-node tunable (`slot_timeout:` /
56//!     [`TaskNode::with_slot_timeout`]), which makes **provider nodes** work: a
57//!     first-in-topo node whose worker *builds* resources at runtime and
58//!     `provide()`s them into other nodes' slots (the graph-native `hw_init`);
59//!     consumers size their timeout to the build and the gate wait becomes a
60//!     rendezvous.
61//!   * Two flags span every lifecycle operation: **disabled** (stopped until an
62//!     explicit `Activate` — declared `disabled` in the graph or control-stopped;
63//!     see [`TaskNode::set_disabled`]) and **detached** (self-managed: after
64//!     [`TaskNode::set_detached`] no supervisor operation touches the node).
65//!   * Each node carries a `TaskHandle` of per-node atomic flags and
66//!     single-consumer `Signal`s. Every node is single-instance — no counts, no
67//!     fan-out. See [`TaskHandle`].
68//!
69//! ## Three lifecycles, distinguished by [`Mode`]
70//!
71//!   * [`Mode::Terminate`] — the task exits its loop on shutdown and is respawned
72//!     on the next bring-up. Stateless services (a network listener, a logger).
73//!   * [`Mode::Pause`] — the task acks the shutdown then parks on
74//!     `wait_resume()`; it is resumed in place, never respawned. Tasks that
75//!     retain a resource across the pause (an open peripheral handle, a socket).
76//!   * [`Mode::OnDemand`] — like `Terminate`, but not started at boot and not
77//!     auto-respawned; the supervisor brings it up and down at runtime to scale
78//!     an elastic worker pool ([`ElasticPool`]) with load.
79//!
80//! ## Writing a supervised task
81//!
82//! A supervised worker's first parameter is its node. With `task:` you write a
83//! plain `async fn` and the macro stamps the `#[embassy_executor::task]` shell
84//! (and, with `resources:`, hands it `&mut` resource handles after the node, in
85//! declared order); with `spawn:` you write the `#[embassy_executor::task]`
86//! yourself. Either way the macro's glue passes the node, and extra arguments come
87//! from the partial-call spawn form. Four rules cover the task side of the protocol:
88//!
89//!   1. select long-lived work against [`TaskNode::wait_shutdown`] — that's how a
90//!      stop reaches you;
91//!   2. ack exactly once per stop with [`TaskNode::ack_dropped`]: on exit
92//!      (`Terminate`/`OnDemand`), or on each pause (`Pause`) *before* parking on
93//!      [`TaskNode::wait_resume`];
94//!   3. an autonomous exit acks too, so the supervisor sees the node as down;
95//!   4. resources follow the mode: a `Terminate` task re-acquires everything on
96//!      respawn (drop-on-exit is the cleanup), a `Pause` task keeps what it holds
97//!      across the park.
98//!
99//! Pool workers additionally report load with [`TaskNode::mark_busy`] /
100//! [`TaskNode::mark_idle`] (a real transition fires the scale signal itself), and
101//! a self-managed daemon or run-once job opts out of supervision with
102//! [`TaskNode::set_detached`]. The README's *Writing supervised tasks* section has
103//! per-mode skeletons.
104//!
105//! ## What the supervisor does *not* do
106//!
107//!   * It does not model any power-state transition (sleep/wake): it reacts to
108//!     "teardown" and "bring-up" requests; the application drives them.
109//!   * It does not allocate, and does no work at construction: the topological
110//!     sort runs at compile time (see the `supervisor_graph!` macro).
111//!   * It does not observe task internals. Tasks self-report their drop state via
112//!     `ack_dropped()`; a task that fails to ack within a timeout panics the
113//!     supervisor with the offending node's name.
114//!
115//! ## Cargo features
116//!
117//!   * `control` *(default)* — the runtime control plane: [`ControlOp`],
118//!     [`request_control`], [`Supervisor::apply_control`].
119//!   * `pool` *(default)* — elastic worker pools: [`ElasticPool`],
120//!     [`Supervisor::run_pools`], and the `pools` field of [`Graph`].
121//!   * `defmt` — route the supervisor's logs through `defmt`; without it the log
122//!     macros are no-ops.
123//!   * `trace` family (all opt-in) — `trace`: the [`trace`] recorders consuming
124//!     embassy-executor's `_embassy_trace_*` hooks; `trace-hooks`:
125//!     `supervisor_graph!` also *defines* the hook symbols; `metadata-names`: node
126//!     names stamped into task Metadata for external consumers (rtos-trace/
127//!     SystemView) — independent of `trace`, so it needs no hook symbols and pairs
128//!     with embassy's own `rtos-trace`; `trace-names`: shorthand for `trace` +
129//!     `metadata-names`; `trace-nested`: preemption-exact accounting (a nested
130//!     higher-tier poll credits its time back to the window it interrupted).
131//!
132//! Build with `default-features = false` for a minimal core that only does
133//! dependency-ordered bring-up/teardown (drops the control plane and pools,
134//! trimming flash and a couple of statics).
135//!
136//! ## Example
137//!
138//! [`supervisor_graph!`] declares the whole graph once — it generates the node
139//! `static`s and a single [`Graph`] value `GRAPH` bundling the node slots, dep
140//! table, and compile-time topological order (a dependency cycle is a compile
141//! error), which [`Supervisor::new`] consumes.
142//!
143//! ```ignore
144//! use embassy_executor::Spawner;
145//! use embassy_supervisor::{supervisor_graph, Supervisor, wait_control};
146//!
147//! // `app` depends on `net`; `task:` names a plain async worker fn the macro wraps
148//! // in its `#[embassy_executor::task]` shell (`spawn:` takes one you wrote yourself).
149//! supervisor_graph! {
150//!     node NET = Terminate, deps: [], task: net_task;
151//!     node APP = Terminate, deps: [NET], task: app_task;
152//! }
153//!
154//! #[embassy_executor::task]
155//! async fn supervisor_task(spawner: Spawner) {
156//!     let sup = Supervisor::new(&GRAPH);
157//!     sup.start(spawner).await.expect("initial spawn"); // brings up `net`, then `app`
158//!     loop {
159//!         // Apply runtime start/stop/pause/resume requests in dependency order.
160//!         let cmd = wait_control().await;
161//!         sup.apply_control(cmd, spawner).await;
162//!     }
163//!     // With the `pool` feature you'd instead drive scaling and control together:
164//!     // `select(sup.run_pools(spawner), wait_control())` (see the `firmware` crate).
165//! }
166//! ```
167//!
168//! The `firmware` crate in the [repository](https://github.com/cedrivard/embassy-supervisor)
169//! is a complete working example (USB-net, an HTTP control plane, an elastic pool,
170//! and OTA).
171
172#[macro_use]
173mod fmt;
174
175use core::cell::Cell;
176use core::sync::atomic::Ordering;
177
178use embassy_executor::{SendSpawner, SpawnError, Spawner};
179use embassy_futures::select::{Either, select};
180use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
181use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
182#[cfg(feature = "control")]
183use embassy_sync::channel::Channel;
184use embassy_sync::signal::Signal;
185use embassy_time::{Timer, with_timeout};
186use portable_atomic::AtomicBool;
187#[cfg(feature = "trace")]
188use portable_atomic::AtomicU32;
189
190// ─── Scale-request signal (task → supervisor) ──────────────────────────────
191//
192// Elastic pool workers fire this when their busy/idle status changes; the
193// supervisor's `run_pools` loop awaits it and re-runs the pool policies
194// (`ElasticPool`). Single-consumer `Signal`: many tasks may `signal()`, only the
195// supervisor `wait()`s. This is the *only* path by which task status reaches the
196// supervisor — it never polls.
197#[cfg(feature = "pool")]
198static SCALE_REQ: Signal<CriticalSectionRawMutex, ()> = Signal::new();
199
200/// Fire the scale-request signal. Called by a task on a busy/idle transition.
201/// A no-op when the `pool` feature is disabled (no pools to re-evaluate).
202pub fn request_scale() {
203    #[cfg(feature = "pool")]
204    SCALE_REQ.signal(());
205}
206
207/// Await the next scale request. The supervisor's driver loop selects this
208/// against its other wake sources and runs the scaling policy on each wake.
209#[cfg(feature = "pool")]
210pub async fn wait_scale() {
211    SCALE_REQ.wait().await;
212}
213
214// ─── Runtime control commands (app → supervisor) ───────────────────────────
215//
216// An application's control surface (e.g. a network endpoint) usually can't drive
217// the supervisor directly: the `Supervisor` and the `Spawner` live on the
218// supervisor task's stack, not in a `static`. So control is decoupled via this
219// channel — the caller `request_control()`s a (node, op) pair and returns
220// immediately; the supervisor's driver loop `wait_control()`s it and runs the
221// dependency-honoring `apply_control`. A `Channel` (not a `Signal`) so
222// back-to-back requests aren't coalesced; capacity 4 is ample for hand-driven
223// control and a full channel simply drops the surplus (`try_send`).
224
225/// Which way to drive a node. Higher-level verbs fold onto these two:
226/// `start`/`resume` → `Activate`, `stop`/`pause` → `Deactivate`. The concrete
227/// mechanism (respawn vs resume vs leave-to-pool) is then chosen per node `Mode`
228/// by the supervisor when it applies the command ([`Supervisor::apply_control`]).
229#[cfg(feature = "control")]
230#[derive(Clone, Copy, PartialEq, Eq, Debug)]
231pub enum ControlOp {
232    /// Bring the node up (start a stopped `Terminate` node, resume a `Pause` node).
233    Activate,
234    /// Take the node down (and its dependents, per the graph).
235    Deactivate,
236}
237
238/// A runtime control request: drive `node` (and, per the dependency graph and
239/// pool membership, the nodes it implies) in the `op` direction.
240#[cfg(feature = "control")]
241#[derive(Clone, Copy, Debug)]
242pub struct ControlCommand {
243    /// The node to drive.
244    pub node: &'static TaskNode,
245    /// The direction to drive it.
246    pub op: ControlOp,
247}
248
249/// App → supervisor control mailbox. `&'static TaskNode` is `Copy + Sync`, so
250/// the target rides the channel directly — no name lookup needed supervisor-side.
251#[cfg(feature = "control")]
252static CONTROL_REQ: Channel<CriticalSectionRawMutex, ControlCommand, 4> = Channel::new();
253
254/// Enqueue a control request. Non-blocking; drops if the mailbox is full (4
255/// outstanding), which is harmless for low-frequency manual control. Called by
256/// the application's control surface.
257#[cfg(feature = "control")]
258pub fn request_control(node: &'static TaskNode, op: ControlOp) {
259    let _ = CONTROL_REQ.try_send(ControlCommand { node, op });
260}
261
262/// Await the next control request. Selected by the supervisor's driver loop
263/// against pool scaling and any other application wake sources.
264#[cfg(feature = "control")]
265pub async fn wait_control() -> ControlCommand {
266    CONTROL_REQ.receive().await
267}
268
269/// Per-node timeout for `wait_dropped`. A task that doesn't ack within this
270/// window is a bug (e.g. a missing `ack_dropped()` call) and panics the
271/// supervisor with the offending node's name. 2 s comfortably exceeds a typical
272/// task's poll period and peripheral settle time.
273const SHUTDOWN_ACK_TIMEOUT_MS: u64 = 2_000;
274
275/// How long the supervisor's bring-up waits for a node's `executor:`
276/// [`SpawnerSlot`] to be filled before failing the spawn with
277/// [`SpawnError::Busy`]. A genuine cross-core rendezvous resolves in microseconds;
278/// a slot empty this long is a misconfiguration (the app never registered that
279/// executor's spawner). Bounded, so a misconfigured graph fails loudly instead of
280/// hanging bring-up forever.
281const SLOT_READY_TIMEOUT: embassy_time::Duration = embassy_time::Duration::from_millis(100);
282
283// ─── Mode ────────────────────────────────────────────────────────────────
284
285/// Lifecycle policy for a managed task: what the task does on shutdown and what
286/// the supervisor does to bring it back.
287#[derive(Clone, Copy, PartialEq, Eq, Debug)]
288pub enum Mode {
289    /// Task exits its loop on shutdown. The supervisor respawns it via the
290    /// node's `spawn` fn from `respawn_terminate`.
291    Terminate,
292    /// Task acks shutdown and parks on `wait_resume()`. The supervisor resumes
293    /// it from `resume_pausable`; the task is never respawned, so it keeps any
294    /// resource it holds (a peripheral handle, a socket) across the pause.
295    Pause,
296    /// Like `Terminate` (exits on shutdown), but **not** started at boot and
297    /// **not** auto-respawned. The supervisor brings it up and down at runtime
298    /// via `start_node` / `stop_node` in response to load — see [`ElasticPool`].
299    /// `start()` skips it; `respawn_terminate()` leaves it down (it
300    /// re-grows under demand); `teardown()` only acts on it while it is running.
301    OnDemand,
302}
303
304impl Mode {
305    /// Stable lower-case wire name, used both for serialization (e.g. a JSON
306    /// task-state view) and for `defmt` logging — the single source of these
307    /// strings.
308    pub fn as_str(&self) -> &'static str {
309        match self {
310            Mode::Terminate => "terminate",
311            Mode::Pause => "pause",
312            Mode::OnDemand => "ondemand",
313        }
314    }
315}
316
317#[cfg(feature = "defmt")]
318impl defmt::Format for Mode {
319    fn format(&self, f: defmt::Formatter) {
320        defmt::write!(f, "{}", self.as_str());
321    }
322}
323
324// ─── TaskHandle ──────────────────────────────────────────────────────────
325
326/// Coordination state for one task. Embedded inside [`TaskNode`].
327///
328/// Every node is single-instance, so each field is a per-node atomic flag or a
329/// single-consumer signal — no counts, no fan-out. Written by one side (task or
330/// supervisor) and read by the other:
331///   * `shutdown` / `shutdown_wake` — supervisor requests exit; the task parks
332///     on the signal and reads the flag.
333///   * `dropped` / `dropped_wake` — the task acks its exit; the supervisor
334///     parks on the signal (with a timeout) and reads the flag.
335///   * `resume_wake` — supervisor resumes a parked Pause-mode task.
336///   * `running` — supervisor's record that the node is spawned; `busy` — the
337///     task's active/idle status. Both read by the elastic scaling policy.
338///   * `disabled` — the node has been manually deactivated; see below.
339pub struct TaskHandle {
340    /// Set true by the supervisor when shutdown is requested.
341    /// Cleared by `reset()` before the next spawn.
342    shutdown: AtomicBool,
343    /// Wake source for `wait_shutdown()`. Fired by `signal_shutdown()`.
344    shutdown_wake: Signal<CriticalSectionRawMutex, ()>,
345    /// Set true by the instance when it acks the shutdown (a bool, not a count,
346    /// since every node is single-instance). Cleared by `reset()`.
347    dropped: AtomicBool,
348    /// Wake source for `wait_dropped()`. Fired by `ack_dropped()`.
349    dropped_wake: Signal<CriticalSectionRawMutex, ()>,
350    /// True while the supervisor has the node spawned and it hasn't exited.
351    /// Always-on nodes are set true by `start()`; `OnDemand` nodes are set
352    /// true/false by `start_node()` / `stop_node()`. `teardown()` only acts on
353    /// `running` nodes, so a down `OnDemand` node doesn't stall it.
354    running: AtomicBool,
355    /// True while the task is actively serving (its active/idle status). Set by
356    /// `mark_busy()` / `mark_idle()`; read by the scaling policy.
357    busy: AtomicBool,
358    /// Wake source for `wait_resume()` on Pause-mode tasks. Fired by
359    /// `signal_resume()`.
360    resume_wake: Signal<CriticalSectionRawMutex, ()>,
361    /// True while the node has been manually deactivated (stopped/paused) via the
362    /// runtime control interface (`Supervisor::deactivate`). Unlike the other
363    /// flags this one is **lifecycle-spanning**: it is *not* cleared by
364    /// `reset()`, so a manual stop "sticks" — the automatic bring-up paths
365    /// (`start`, `respawn_terminate`, `resume_pausable`, and the elastic pool's
366    /// grow) skip a node while it is set. Cleared only by `Supervisor::activate`.
367    /// Because it lives in a `static`, it also survives a power-state transition
368    /// that retains RAM (e.g. a warm-resume from deep sleep).
369    disabled: AtomicBool,
370    /// Self-managed: while set, the supervisor never drives this node — teardown,
371    /// deactivate/activate, `stop_node`, respawn, and pause-resume all skip it. Not
372    /// cleared by `reset()`. Full rationale on [`TaskNode::set_detached`].
373    detached: AtomicBool,
374    /// The executor task id currently running this node (`TaskRef::id()`, captured
375    /// from the `SpawnToken` by the macro's spawn glue). `0` = unknown (not yet
376    /// spawned, or a parked/closure-spawned node that never registered). Overwritten
377    /// on every (re)spawn, so — unlike an external tracker — it stays correct across
378    /// respawns without any unlinking.
379    #[cfg(feature = "trace")]
380    task_id: AtomicU32,
381    /// Accumulated executor-poll time for this node, in embassy-time ticks,
382    /// wrapping. Consumers sample twice and `wrapping_sub` to get a rate; the
383    /// crate does no windowing.
384    #[cfg(feature = "trace")]
385    exec_ticks: AtomicU32,
386    /// Number of executor polls of this node, wrapping.
387    #[cfg(feature = "trace")]
388    polls: AtomicU32,
389    /// Longest single poll ever observed, in ticks — the "never yields" watermark.
390    /// A large value names the node that hogged the executor even after the fact,
391    /// which a live check cannot do from the blocked executor itself.
392    #[cfg(feature = "trace")]
393    max_poll_ticks: AtomicU32,
394}
395
396impl TaskHandle {
397    const fn new(disabled_at_boot: bool) -> Self {
398        Self {
399            shutdown: AtomicBool::new(false),
400            shutdown_wake: Signal::new(),
401            dropped: AtomicBool::new(false),
402            dropped_wake: Signal::new(),
403            running: AtomicBool::new(false),
404            busy: AtomicBool::new(false),
405            resume_wake: Signal::new(),
406            disabled: AtomicBool::new(disabled_at_boot),
407            detached: AtomicBool::new(false),
408            #[cfg(feature = "trace")]
409            task_id: AtomicU32::new(0),
410            #[cfg(feature = "trace")]
411            exec_ticks: AtomicU32::new(0),
412            #[cfg(feature = "trace")]
413            polls: AtomicU32::new(0),
414            #[cfg(feature = "trace")]
415            max_poll_ticks: AtomicU32::new(0),
416        }
417    }
418}
419
420// ─── Executor spawner slots ──────────────────────────────────────────────
421
422/// A runtime-filled slot holding the [`SendSpawner`] of an executor other than
423/// the one the supervisor runs on — an `InterruptExecutor` tier, the second
424/// core's executor, any foreign thread executor (via `Spawner::make_send()`).
425///
426/// Declared by the `executor NAME;` item of [`supervisor_graph!`]; nodes carrying
427/// `executor: NAME` are spawned through the slot instead of the supervisor's own
428/// `Spawner`. The application fills it once at startup — before, or concurrently
429/// with, [`Supervisor::start`] (e.g. from the second core's bring-up):
430///
431/// ```ignore
432/// static EXECUTOR_HIGH: InterruptExecutor = InterruptExecutor::new();
433/// HIGH.set(EXECUTOR_HIGH.start(interrupt::SWI_IRQ_0));
434/// sup.start(spawner).await?;   // nodes declared `executor: HIGH` spawn on that tier
435/// ```
436///
437/// The supervisor's bring-up (`start` / `start_node` / `respawn_terminate`) awaits
438/// [`ready`](Self::ready) for a node's slot before spawning it, so a tier filled
439/// late — or from another core — is handled without a race; a slot still empty after
440/// the supervisor's bounded wait fails the spawn with [`SpawnError::Busy`] rather
441/// than silently dropping the task. Spawned futures must be `Send` (a non-`Send`
442/// `executor:` task is a compile error at the glue).
443pub struct SpawnerSlot {
444    slot: BlockingMutex<CriticalSectionRawMutex, Cell<Option<SendSpawner>>>,
445    /// Wakes a `ready()` waiter when `set` fills the slot (cross-core safe:
446    /// `Signal` is critical-section based and latches).
447    filled: Signal<CriticalSectionRawMutex, ()>,
448}
449
450impl SpawnerSlot {
451    /// An empty slot (`const` — it lives in a `static` the macro emits).
452    pub const fn new() -> Self {
453        Self {
454            slot: BlockingMutex::new(Cell::new(None)),
455            filled: Signal::new(),
456        }
457    }
458
459    /// Fill the slot (last set wins) and wake a [`ready`](Self::ready) waiter.
460    /// Call before [`Supervisor::start`] — or from the other core's bring-up,
461    /// with the supervisor awaiting `ready()`.
462    pub fn set(&self, spawner: SendSpawner) {
463        self.slot.lock(|c| c.set(Some(spawner)));
464        self.filled.signal(());
465    }
466
467    /// The registered spawner, or `None` while unfilled.
468    pub fn get(&self) -> Option<SendSpawner> {
469        self.slot.lock(Cell::get)
470    }
471
472    /// Await the slot and return the spawner. The rendezvous primitive: the
473    /// supervisor's bring-up awaits this for a node's `executor:` slot before
474    /// spawning it (bounded, see [`Supervisor::start`]), so a tier filled late — or
475    /// from another core — is handled without a race. Returns immediately once the
476    /// slot is filled, so any number of *late* callers are fine (an application can
477    /// gate work on the executor being up). While the slot is still empty, at most
478    /// one task should be parked here: the underlying `Signal` holds a single waker,
479    /// so a second pre-fill waiter would displace the first.
480    pub async fn ready(&self) -> SendSpawner {
481        loop {
482            if let Some(sp) = self.get() {
483                return sp;
484            }
485            // `Signal` latches: a `set()` racing between the check above and
486            // this wait still wakes us.
487            self.filled.wait().await;
488        }
489    }
490}
491
492impl Default for SpawnerSlot {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498// ─── ResourceSlot ────────────────────────────────────────────────────────
499
500/// Type-erased readiness view of a [`ResourceSlot`], for the supervisor's
501/// bring-up wait.
502///
503/// A `TaskNode` can gate on any number of slots of *different* `T`s, so the node
504/// stores `&'static [&'static dyn ResourceGate]` (object-safe: no `T` in the
505/// signatures). Same shape as embassy's `dyn` driver registries — see
506/// <https://doc.rust-lang.org/reference/items/traits.html#object-safety>.
507/// The supervisor only needs "is it filled?" plus the signal to park on; taking
508/// the value stays in the generated spawn glue, where the concrete `T` is known.
509pub trait ResourceGate: Sync {
510    /// Non-consuming "is the slot currently filled" check.
511    fn is_filled(&self) -> bool;
512    /// The latching [`Signal`] fired by `provide`/`restore`, for the supervisor's
513    /// bounded pre-spawn wait (see [`Supervisor::start`]).
514    fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()>;
515}
516
517/// A one-value handoff cell threading an owned resource from `main` into a
518/// supervised task — the safe replacement for `Peripherals::steal()` inside
519/// the task body.
520///
521/// Declared (as a `pub static`) by [`supervisor_graph!`] for each entry in a
522/// node's `resources:` clause. The protocol:
523///
524/// 1. `main` splits `Peripherals` and **moves** the resource in with
525///    [`provide`](Self::provide). This is where the compile-time guarantee
526///    lives: the singleton field is *consumed*, so no second owner — and no
527///    `unsafe` steal — can exist.
528/// 2. The generated spawn glue [`take`](Self::take)s it just before spawning
529///    the node. An empty slot fails the spawn with `SpawnError::Busy` — a
530///    fail-closed error out of [`Supervisor::start`], not a panic inside the
531///    task (compare `static_cell::StaticCell`, which panics on misuse).
532/// 3. The generated task shell hands the worker `&mut T` and
533///    [`restore`](Self::restore)s the value after the worker returns, so a
534///    `Terminate` respawn re-takes the *same instance* instead of stealing a
535///    fresh one. (A `Pause` worker never returns — it parks — so it simply
536///    retains the resource, exactly like a hand-written parked task.)
537///
538/// Same primitives as [`SpawnerSlot`]: a critical-section
539/// [`BlockingMutex`]`<`[`Cell`]`<Option<T>>>` for the value (`Sync` for
540/// `T: Send`, provided by embassy-sync — no `unsafe` here) plus a latching
541/// [`Signal`] so the supervisor can await late provisioning (bounded; see
542/// [`Supervisor::start`]).
543pub struct ResourceSlot<T> {
544    slot: BlockingMutex<CriticalSectionRawMutex, Cell<Option<T>>>,
545    /// Wakes the supervisor's pre-spawn wait when `provide`/`restore` fills the
546    /// slot (latching, so a fill racing the check-then-wait still wakes it).
547    filled: Signal<CriticalSectionRawMutex, ()>,
548}
549
550impl<T> ResourceSlot<T> {
551    /// An empty slot (`const` — it lives in a `static` the macro emits).
552    pub const fn new() -> Self {
553        Self {
554            slot: BlockingMutex::new(Cell::new(None)),
555            filled: Signal::new(),
556        }
557    }
558
559    /// Move the resource in (from `main`'s `Peripherals` split) and wake the
560    /// supervisor's pre-spawn wait. Call before [`Supervisor::start`]; a slot
561    /// still empty after the supervisor's bounded wait fails that node's spawn
562    /// with `SpawnError::Busy`. Filling an occupied slot replaces (drops) the
563    /// old value — don't: one resource, one slot, moved exactly once.
564    pub fn provide(&self, value: T) {
565        self.slot.lock(|c| c.set(Some(value)));
566        self.filled.signal(());
567    }
568
569    /// Take the resource out, leaving the slot empty. Called by the generated
570    /// spawn glue just before the spawn; `None` means "not provided yet" or
571    /// "currently held by a live task instance".
572    pub fn take(&self) -> Option<T> {
573        self.slot.lock(Cell::take)
574    }
575
576    /// Copy the resource out **without emptying the slot** — the `shared`
577    /// resource kind's read: any number of consumers (several nodes, a whole
578    /// pool) get the same `Copy` handle, and the slot stays filled for the
579    /// next one. Only for `T: Copy` (a `Stack`-like handle, a `&'static`
580    /// registry ref); an owned singleton uses [`take`](Self::take).
581    pub fn get(&self) -> Option<T>
582    where
583        T: Copy,
584    {
585        // Same peek shape as `is_filled`: `Cell` has no `&T` access, so
586        // take-copy-put-back under one critical section.
587        self.slot.lock(|c| {
588            let v = c.take();
589            c.set(v);
590            v
591        })
592    }
593
594    /// Put the resource back for the next spawn. Called by the generated task
595    /// shell after the worker returns (i.e. after its clean shutdown ack), so a
596    /// respawn re-takes the same instance.
597    pub fn restore(&self, value: T) {
598        self.provide(value);
599    }
600}
601
602// `T: Send` (not just any `T`): the gate is reachable from the supervisor task,
603// which may run on a different core than the provider — the same bound the
604// inner `BlockingMutex` requires for `Sync`, restated here so the `dyn` upcast
605// can't outrun it.
606impl<T: Send> ResourceGate for ResourceSlot<T> {
607    fn is_filled(&self) -> bool {
608        // Peek without consuming: `Cell` has no `&T` access (no `T: Copy`
609        // here), so take-and-put-back under the same critical section.
610        self.slot.lock(|c| {
611            let v = c.take();
612            let filled = v.is_some();
613            c.set(v);
614            filled
615        })
616    }
617
618    fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()> {
619        &self.filled
620    }
621}
622
623impl<T> Default for ResourceSlot<T> {
624    fn default() -> Self {
625        Self::new()
626    }
627}
628
629// ─── TaskNode ────────────────────────────────────────────────────────────
630
631/// A node in the supervisor's task graph.
632///
633/// Designed to live in `static` memory: every field is `Sync`, all constructors
634/// are `const`. Declared by [`supervisor_graph!`], which emits one per managed
635/// task along with the [`Graph`] (`GRAPH`) that [`Supervisor::new`] consumes.
636pub struct TaskNode {
637    /// Human-readable name. Used in defmt logs and panic messages.
638    pub name: &'static str,
639    /// Lifecycle policy. See [`Mode`].
640    pub mode: Mode,
641    /// App-provided spawn function (typically an inline closure at the node's
642    /// declaration). Called once at boot from `Supervisor::start`, again from
643    /// `respawn_terminate` for Terminate nodes, and at runtime from `start_node`
644    /// for `OnDemand` nodes. `None` for a **parked** node the application spawns
645    /// itself (e.g. a `Pause` sensor holding a peripheral handle): the supervisor
646    /// tracks its lifecycle but never spawns it.
647    pub spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
648    /// The executor [`SpawnerSlot`] this node spawns through (`executor: NAME` in
649    /// the graph), or `None` to spawn on the supervisor's own `Spawner`. When
650    /// `Some`, the supervisor awaits the slot's [`ready`](SpawnerSlot::ready)
651    /// (bounded by [`SLOT_READY_TIMEOUT`]) *before* invoking `spawn`, so the
652    /// generated glue's own non-blocking `SpawnerSlot::get` is already filled. Set
653    /// by the macro via [`with_executor`](Self::with_executor); `const`, zero-cost.
654    spawn_slot: Option<&'static SpawnerSlot>,
655    /// The [`ResourceSlot`]s this node's spawn takes from (`resources:` in the
656    /// graph), type-erased to their [`ResourceGate`] readiness view. The
657    /// supervisor awaits every gate being filled (bounded by
658    /// [`SLOT_READY_TIMEOUT`]) *before* invoking `spawn`, so (a) a `main` that
659    /// provides late is tolerated and (b) a respawn cannot race the previous
660    /// instance's shell restoring the value (the restore happens after the
661    /// worker's shutdown ack). Empty for nodes without `resources:`. Set by the
662    /// macro via [`with_resources`](Self::with_resources); `const`, zero-cost.
663    resource_gates: &'static [&'static dyn ResourceGate],
664    /// Bound on the pre-spawn waits for this node's `executor:` slot and
665    /// `resources:` gates. Defaults to [`SLOT_READY_TIMEOUT`] (100 ms — sized
666    /// for "main provided before start"); raise it (`slot_timeout:` in the
667    /// graph) for a node whose slots are filled by a **provider node** at
668    /// runtime — e.g. an async radio bring-up worth hundreds of milliseconds.
669    /// Set by the macro via [`with_slot_timeout`](Self::with_slot_timeout).
670    slot_timeout: embassy_time::Duration,
671    handle: TaskHandle,
672}
673
674impl TaskNode {
675    /// A single-instance node started at boot (`Terminate`/`Pause`) or on demand
676    /// (`Mode::OnDemand`). Every node is single-instance; an elastic service is
677    /// modelled as several `OnDemand` nodes of the same pooled task fn.
678    ///
679    /// A `TaskNode` carries only its own identity and behaviour; the graph's
680    /// dependency edges live in the compile-time index table that
681    /// [`supervisor_graph!`] emits and [`Supervisor::new`] consumes.
682    /// `disabled_at_boot` seeds the node's disabled flag so a control-started node
683    /// (e.g. an OTA task) can be declared down and started later via a control op.
684    /// `spawn` is `None` for a parked node the application spawns itself.
685    pub const fn new(
686        name: &'static str,
687        mode: Mode,
688        spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
689        disabled_at_boot: bool,
690    ) -> Self {
691        Self {
692            name,
693            mode,
694            spawn,
695            spawn_slot: None,
696            resource_gates: &[],
697            slot_timeout: SLOT_READY_TIMEOUT,
698            handle: TaskHandle::new(disabled_at_boot),
699        }
700    }
701
702    /// Route this node's spawn through the given executor [`SpawnerSlot`] (the
703    /// `executor: NAME` graph annotation). The supervisor awaits the slot before
704    /// spawning the node, so a tier filled late — or from another core — is handled
705    /// without a race, and the generated glue's non-blocking `get` is already filled.
706    /// `const` and chainable in a `static` initializer; emitted by [`supervisor_graph!`].
707    pub const fn with_executor(mut self, slot: &'static SpawnerSlot) -> Self {
708        self.spawn_slot = Some(slot);
709        self
710    }
711
712    /// Declare the [`ResourceSlot`]s this node's spawn takes from (the
713    /// `resources:` graph clause). The supervisor awaits every gate being
714    /// filled before spawning the node, so the generated glue's non-blocking
715    /// `take()` finds the value. `const` and chainable in a `static`
716    /// initializer; emitted by [`supervisor_graph!`].
717    pub const fn with_resources(mut self, gates: &'static [&'static dyn ResourceGate]) -> Self {
718        self.resource_gates = gates;
719        self
720    }
721
722    /// Override the pre-spawn slot/gate wait bound for this node (the
723    /// `slot_timeout: <millis>` graph clause). The default
724    /// (`SLOT_READY_TIMEOUT`, 100 ms) assumes slots are provided *before*
725    /// `start()`; a node consuming a **provider node's** outputs must cover the
726    /// provider's async build time (the failure mode stays a loud
727    /// `SpawnError::Busy`, just later). `const` and chainable in a `static`
728    /// initializer; emitted by [`supervisor_graph!`].
729    pub const fn with_slot_timeout(mut self, timeout: embassy_time::Duration) -> Self {
730        self.slot_timeout = timeout;
731        self
732    }
733
734    // ── Task-side API ────────────────────────────────────────────────────
735    //
736    // Called from inside the `#[embassy_executor::task] async fn` body. The
737    // whole task-side protocol is four rules (the README's "Writing supervised
738    // tasks" section has per-mode skeletons):
739    //   1. select long-lived work against `wait_shutdown()`;
740    //   2. `ack_dropped()` exactly once per stop — on exit (Terminate/OnDemand)
741    //      or on each pause (Pause), before parking on `wait_resume()`;
742    //   3. an autonomous exit acks too;
743    //   4. resources follow the mode: Terminate re-acquires on respawn, Pause
744    //      retains across park.
745
746    /// True iff the supervisor has requested shutdown. Checked at the loop top
747    /// alongside `wait_shutdown()` in a `select`.
748    pub fn shutdown_requested(&self) -> bool {
749        self.handle.shutdown.load(Ordering::Acquire)
750    }
751
752    /// Park until shutdown is requested. Returns immediately if shutdown has
753    /// already been requested. Use this for single-instance tasks in a `select`
754    /// against the task's main work future.
755    pub async fn wait_shutdown(&self) {
756        // Fast path — already requested. (Important because the signal is
757        // edge-triggered: if `signal()` fired before we got here, the bare
758        // `wait()` below would block forever.)
759        if self.handle.shutdown.load(Ordering::Acquire) {
760            return;
761        }
762        self.handle.shutdown_wake.wait().await;
763    }
764
765    /// Mark this instance as having shut down: clears the running flag and acks
766    /// the teardown handshake (so the supervisor's `wait_dropped` completes).
767    /// Every instance must call this exactly once on exit (Terminate/OnDemand
768    /// mode) or on each pause (Pause mode). It also covers an **autonomous** exit
769    /// the supervisor didn't request — e.g. a pool worker backing off — so the
770    /// pool sees the instance as down and can re-grow it under later demand.
771    pub fn ack_dropped(&self) {
772        self.handle.running.store(false, Ordering::Release);
773        self.handle.dropped.store(true, Ordering::Release);
774        self.handle.dropped_wake.signal(());
775    }
776
777    /// Pause-mode only: park until the supervisor signals resume. Call *after*
778    /// [`ack_dropped`](Self::ack_dropped) — ack the pause, then park; held
779    /// resources stay owned across the park.
780    pub async fn wait_resume(&self) {
781        self.handle.resume_wake.wait().await;
782    }
783
784    /// Report that this task started serving a request (active). Fires the
785    /// scale-request signal on a real idle→busy transition so the scaling policy
786    /// can react (e.g. grow the pool); a redundant call doesn't re-signal.
787    pub fn mark_busy(&self) {
788        if !self.handle.busy.swap(true, Ordering::Release) {
789            request_scale();
790        }
791    }
792
793    /// Report that this task finished serving and is idle again. Fires the
794    /// scale-request signal on a real busy→idle transition so the scaling policy
795    /// can react (e.g. shrink the pool); a redundant call doesn't re-signal.
796    pub fn mark_idle(&self) {
797        if self.handle.busy.swap(false, Ordering::Release) {
798            request_scale();
799        }
800    }
801
802    /// True while this task is actively serving. Read by the scaling policy.
803    pub fn is_busy(&self) -> bool {
804        self.handle.busy.load(Ordering::Acquire)
805    }
806
807    /// True while the supervisor has this node spawned (and it hasn't exited).
808    /// Read by the scaling policy to count live instances, and by a task-state
809    /// view.
810    pub fn is_running(&self) -> bool {
811        self.handle.running.load(Ordering::Acquire)
812    }
813
814    /// True while the node is disabled: declared `disabled` in the graph
815    /// (stopped-at-boot, up on an explicit `Activate`), or manually deactivated
816    /// via the control interface and not yet re-activated. Read by a task-state
817    /// view and by the automatic bring-up paths (which skip a disabled node).
818    pub fn is_disabled(&self) -> bool {
819        self.handle.disabled.load(Ordering::Acquire)
820    }
821
822    /// Mark/clear this node as **detached**: a self-managing node the supervisor
823    /// brings up once (via [`start`](Supervisor::start)) and then stops managing
824    /// **entirely**. Every runtime lifecycle operation skips a detached node: full
825    /// [`teardown`](Supervisor::teardown), the control deactivate/activate cascades,
826    /// [`stop_node`](Supervisor::stop_node), [`respawn_terminate`](Supervisor::respawn_terminate),
827    /// and pause-resume. It keeps running (or, for a one-shot, stays exited) across a
828    /// teardown/wake cycle instead of being stopped, re-enabled, or re-spawned. Use it
829    /// for a task that must outlive the teardown it participates in — e.g. a sleep/power
830    /// coordinator that tears the graph down, sleeps, then wakes it — or a self-managed
831    /// one-shot whose `deps:` exist only for start-ordering. The node owns its own
832    /// shutdown; the supervisor will not drive it.
833    pub fn set_detached(&self, detached: bool) {
834        self.handle.detached.store(detached, Ordering::Release);
835    }
836
837    /// True while this node is [detached](Self::set_detached): self-managed, skipped by
838    /// every runtime lifecycle operation (teardown, deactivate/activate, `stop_node`,
839    /// respawn, pause-resume). Only the initial `start` brings it up.
840    pub fn is_detached(&self) -> bool {
841        self.handle.detached.load(Ordering::Acquire)
842    }
843
844    // ── Trace/observability API (features `trace`/`trace-names`) ───────────
845
846    /// Record the executor task id (`SpawnToken::id()` / `TaskRef::id()`) currently
847    /// backing this node, so the [`trace`] recorders can attribute executor polls to
848    /// it. Called automatically by the spawn glue `supervisor_graph!` generates;
849    /// call it manually only for a **parked** node (no `spawn:`) or a verbatim-closure
850    /// `spawn:`, where the macro cannot see the token. Overwrites on every (re)spawn.
851    #[cfg(feature = "trace")]
852    pub fn set_task_id(&self, id: u32) {
853        self.handle.task_id.store(id, Ordering::Release);
854    }
855
856    /// Register an externally-spawned token as this node's live task: records
857    /// the task id for the [`trace`] recorders and (feature `metadata-names`)
858    /// stamps the node name into the task Metadata. One call replaces the
859    /// manual [`set_task_id`](Self::set_task_id) dance wherever the macro can't
860    /// see the token — parked nodes and verbatim-closure `spawn:` forms:
861    ///
862    /// ```ignore
863    /// let t = environment_task(i2c_dev)?;
864    /// BME280.adopt(&t);
865    /// high_spawner.spawn(t);
866    /// ```
867    #[cfg(feature = "trace")]
868    pub fn adopt<S>(&self, token: &embassy_executor::SpawnToken<S>) {
869        self.set_task_id(token.id());
870        #[cfg(feature = "metadata-names")]
871        self.stamp_name(token);
872    }
873
874    /// Stamp this node's name into the task's embassy `Metadata` (feature
875    /// `metadata-names`), so external consumers — rtos-trace/SystemView, debuggers —
876    /// show the graph node name instead of an opaque task id. Unlike
877    /// [`adopt`](Self::adopt) this does **not** capture the task id or touch the
878    /// supervisor's [`trace`] recorders, so it needs neither the `trace` feature nor
879    /// the `_embassy_trace_*` hook symbols: it is the name-only spawn path emitted
880    /// when `metadata-names` is on but `trace` is off (pair it with embassy's
881    /// `rtos-trace`). Called automatically by the spawn glue; call it manually only
882    /// for a parked or verbatim-closure node the macro can't see.
883    ///
884    /// Requires `embassy-executor`'s `metadata-name` feature, which `metadata-names`
885    /// pulls in; without a registered name the task keeps embassy's default.
886    #[cfg(feature = "metadata-names")]
887    pub fn stamp_name<S>(&self, token: &embassy_executor::SpawnToken<S>) {
888        token.metadata().set_name(self.name);
889    }
890
891    /// The executor task id last recorded by [`set_task_id`](Self::set_task_id)
892    /// (`0` = never spawned / not registered).
893    #[cfg(feature = "trace")]
894    pub fn task_id(&self) -> u32 {
895        self.handle.task_id.load(Ordering::Acquire)
896    }
897
898    /// Accumulated executor-poll time of this node, in embassy-time ticks. Wrapping:
899    /// sample twice and `wrapping_sub` the readings to get a rate over a window.
900    #[cfg(feature = "trace")]
901    pub fn exec_ticks(&self) -> u32 {
902        self.handle.exec_ticks.load(Ordering::Relaxed)
903    }
904
905    /// Number of executor polls of this node (wrapping counter).
906    #[cfg(feature = "trace")]
907    pub fn poll_count(&self) -> u32 {
908        self.handle.polls.load(Ordering::Relaxed)
909    }
910
911    /// Longest single executor poll of this node ever observed, in ticks — the
912    /// "never yields" watermark. A poll is expected to be microseconds; a large
913    /// value names the node that hogged its executor, even after the fact.
914    #[cfg(feature = "trace")]
915    pub fn max_poll_ticks(&self) -> u32 {
916        self.handle.max_poll_ticks.load(Ordering::Relaxed)
917    }
918
919    // ── Supervisor-side API ──────────────────────────────────────────────
920    //
921    // Driven by the `Supervisor` struct. Kept `pub(crate)` so app code doesn't
922    // accidentally bypass the supervisor's orchestration.
923
924    pub(crate) fn signal_shutdown(&self) {
925        self.handle.shutdown.store(true, Ordering::Release);
926        self.handle.shutdown_wake.signal(());
927    }
928
929    pub(crate) fn signal_resume(&self) {
930        self.handle.resume_wake.signal(());
931    }
932
933    pub(crate) fn set_running(&self, running: bool) {
934        self.handle.running.store(running, Ordering::Release);
935    }
936
937    /// Set/clear the manual-deactivation flag. Set by `Supervisor::deactivate`,
938    /// cleared by `Supervisor::activate`. Deliberately *not* touched by
939    /// `reset()`, so a manual stop survives respawn cycles and RAM-retaining
940    /// power-state transitions.
941    ///
942    /// Public so an application can pre-disable a `Terminate` node *before*
943    /// `Supervisor::start`, making it a stopped-at-boot task that only comes up on
944    /// an explicit `Activate` control (a node started by control rather than at boot).
945    pub fn set_disabled(&self, disabled: bool) {
946        self.handle.disabled.store(disabled, Ordering::Release);
947    }
948
949    /// Wait until the instance has called `ack_dropped()`. Single-instance, so
950    /// one ack ends the wait. The fast-path flag check handles the ack landing
951    /// before this await (the `dropped_wake` signal is edge-triggered).
952    pub(crate) async fn wait_dropped(&self) {
953        if self.handle.dropped.load(Ordering::Acquire) {
954            return;
955        }
956        self.handle.dropped_wake.wait().await;
957    }
958
959    /// Clear the shutdown flag, dropped flag, busy flag, and the shutdown /
960    /// dropped wake-signals so the next cycle starts clean. Doesn't touch
961    /// `running` (managed around spawn/stop), `resume_wake` (`resume_pausable`
962    /// fires that for Pause nodes), or `disabled` (lifecycle-spanning).
963    pub(crate) fn reset(&self) {
964        self.handle.shutdown.store(false, Ordering::Release);
965        self.handle.dropped.store(false, Ordering::Release);
966        self.handle.busy.store(false, Ordering::Release);
967        self.handle.shutdown_wake.reset();
968        self.handle.dropped_wake.reset();
969    }
970}
971
972/// Manual impl: the private `TaskHandle` (Signals + atomics) has no `Debug`, and a
973/// snapshot of the *live* flags is more useful than raw handle internals anyway.
974/// `finish_non_exhaustive` marks the elided fields (`spawn`, the handle).
975impl core::fmt::Debug for TaskNode {
976    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
977        f.debug_struct("TaskNode")
978            .field("name", &self.name)
979            .field("mode", &self.mode)
980            .field("running", &self.is_running())
981            .field("busy", &self.is_busy())
982            .field("disabled", &self.is_disabled())
983            .field("detached", &self.is_detached())
984            .finish_non_exhaustive()
985    }
986}
987
988// ─── Graph ───────────────────────────────────────────────────────────────
989
990/// The compile-time task graph produced by [`supervisor_graph!`]: the node slots,
991/// the dependency-index table, the topological order, and the elastic pools — the
992/// single value [`Supervisor::new`] consumes. The macro emits one `pub static GRAPH`
993/// of this type. The fields are public so the application can read them directly
994/// (e.g. a status endpoint iterating `GRAPH.nodes` / `GRAPH.deps`).
995///
996/// `N` is capped at 256 (graph indices are `u8`); the macro enforces this at
997/// expansion time.
998pub struct Graph<const N: usize> {
999    /// Node slots, one per declared node. `None` marks a `#[cfg]`-ed-out node.
1000    pub nodes: &'static [Option<&'static TaskNode>; N],
1001    /// Per-node dependency indices into `nodes` (`deps[i]` lists node `i`'s deps).
1002    pub deps: &'static [&'static [u8]; N],
1003    /// Topologically sorted indices into `nodes` (dependencies before dependents;
1004    /// reverse iteration is the teardown order). A dependency cycle is a compile error.
1005    pub order: [u8; N],
1006    /// Elastic worker pools to register with the supervisor (empty when unused).
1007    #[cfg(feature = "pool")]
1008    pub pools: &'static [&'static dyn Pool],
1009}
1010
1011// ─── Supervisor ──────────────────────────────────────────────────────────
1012
1013/// Orchestrates a set of managed tasks across spawn / teardown / bring-up.
1014///
1015/// Owned by a single supervisor task. Concurrent access from other tasks goes
1016/// through each [`TaskNode`]'s own atomic state, not the `Supervisor` struct.
1017pub struct Supervisor<const N: usize> {
1018    /// Node slots, one per declared node. `None` marks a slot whose node was
1019    /// `#[cfg]`-ed out of the build (feature-gated); every method skips those.
1020    nodes: &'static [Option<&'static TaskNode>],
1021    /// Per-node dependency indices into `nodes` (`deps[i]` lists the indices of
1022    /// the nodes that node `i` depends on). The single runtime source of graph
1023    /// topology, generated alongside `order` by the `supervisor_graph!` macro.
1024    deps: &'static [&'static [u8]],
1025    /// Topologically sorted indices into `nodes`: dependencies before their
1026    /// dependents; reverse iteration is the teardown order. Precomputed at
1027    /// compile time (a cycle is a compile error), so construction does no work.
1028    /// Borrowed from the `static` [`Graph`] rather than copied: a `Supervisor`
1029    /// usually lives inside a task future (i.e. in that task's `static`
1030    /// storage), so an inline `[u8; N]` would cost N bytes of RAM per
1031    /// supervisor plus the copy code for no benefit.
1032    order: &'static [u8; N],
1033    /// Elastic pools, so the control interface can co-control a whole pool from
1034    /// any one member (`apply_control` expands the target through
1035    /// [`Pool::members`]) — the same registry `run_pools` drives. Taken from
1036    /// `GRAPH.pools` at construction (empty when no pool is declared).
1037    #[cfg(feature = "pool")]
1038    pools: &'static [&'static dyn Pool],
1039}
1040
1041/// Await a node's `executor:` [`SpawnerSlot`] (if it has one), bounded by the
1042/// node's [`slot_timeout`](TaskNode::with_slot_timeout) (default
1043/// [`SLOT_READY_TIMEOUT`]). A slot still empty after the wait yields
1044/// [`SpawnError::Busy`] — a loud misconfiguration, not a silent hang. A node with no
1045/// slot returns immediately, so a same-executor bring-up never touches the timer.
1046async fn await_spawn_slot(node: &'static TaskNode) -> Result<(), SpawnError> {
1047    if let Some(slot) = node.spawn_slot {
1048        with_timeout(node.slot_timeout, slot.ready())
1049            .await
1050            .map_err(|_| SpawnError::Busy)?;
1051    }
1052    Ok(())
1053}
1054
1055/// Await every [`ResourceSlot`] a node's `resources:` clause takes from being
1056/// filled, bounded by the node's
1057/// [`slot_timeout`](TaskNode::with_slot_timeout) (default
1058/// [`SLOT_READY_TIMEOUT`]) per gate. Covers three windows: `main` providing
1059/// after `start` was entered; — on respawn — the previous instance's shell
1060/// still between the shutdown ack and its `restore()` call (on another core
1061/// the two can genuinely overlap); and a **provider node** still building the
1062/// values this node consumes (size `slot_timeout:` to the build time). A gate
1063/// still empty at the deadline yields [`SpawnError::Busy`] — an unprovided
1064/// slot is a loud misconfiguration, not a silent hang. Nodes without
1065/// `resources:` have an empty gate list and never touch the timer. Same
1066/// check-then-park loop as [`SpawnerSlot::ready`]; the `filled` signal
1067/// latches, so a fill racing the check still wakes the wait (and the same
1068/// single-pre-fill-waiter caveat applies — the supervisor task is the only
1069/// intended waiter).
1070async fn await_resources(node: &'static TaskNode) -> Result<(), SpawnError> {
1071    for gate in node.resource_gates {
1072        let wait = async {
1073            loop {
1074                if gate.is_filled() {
1075                    break;
1076                }
1077                gate.filled_signal().wait().await;
1078            }
1079        };
1080        with_timeout(node.slot_timeout, wait)
1081            .await
1082            .map_err(|_| SpawnError::Busy)?;
1083    }
1084    Ok(())
1085}
1086
1087impl<const N: usize> Supervisor<N> {
1088    /// Build a supervisor from a precomputed [`Graph`] — the `GRAPH` that
1089    /// `supervisor_graph!` emits (node slots, dependency-index table, compile-time
1090    /// topological `order`, and the elastic pools). A dependency cycle is a
1091    /// *compile* error, so construction is infallible and does no work —
1092    /// `start` / `teardown` / `respawn_terminate` just iterate.
1093    pub const fn new(graph: &'static Graph<N>) -> Self {
1094        Self {
1095            nodes: graph.nodes,
1096            deps: graph.deps,
1097            order: &graph.order,
1098            #[cfg(feature = "pool")]
1099            pools: graph.pools,
1100        }
1101    }
1102
1103    /// Spawn every boot node in dependency order. Called once at boot.
1104    /// `Mode::OnDemand` nodes are skipped — they're brought up at runtime by
1105    /// `start_node`. A **parked** node (no `spawn` fn) is spawned externally by
1106    /// `main()` (with hardware handles main owns); it's still marked `running`
1107    /// here. Disabled nodes, and `#[cfg]`-ed-out slots, are skipped.
1108    ///
1109    /// Async because an `executor: NAME` node first awaits its [`SpawnerSlot::ready`]
1110    /// (bounded by `SLOT_READY_TIMEOUT` — the rendezvous with a tier or second core
1111    /// that comes up asynchronously); a slot still empty at the deadline fails the
1112    /// bring-up with [`SpawnError::Busy`]. A node with no `executor:` slot never
1113    /// touches the timer.
1114    pub async fn start(&self, spawner: Spawner) -> Result<(), SpawnError> {
1115        // Register the node slots with the trace recorders.
1116        #[cfg(feature = "trace")]
1117        trace::register_graph(self.nodes);
1118
1119        for i in self.order.iter() {
1120            let Some(node) = self.nodes[*i as usize] else {
1121                continue;
1122            };
1123            if matches!(node.mode, Mode::OnDemand) || node.is_disabled() {
1124                continue;
1125            }
1126            info!("supervisor: spawning {} ({})", node.name, node.mode);
1127            if let Some(spawn) = node.spawn {
1128                // For an `executor:` node, wait (bounded) for its slot to be filled
1129                // before spawning; a same-executor node has no slot, so this is an
1130                // immediate no-op and the bring-up loop stays tight. Then wait for
1131                // the node's `resources:` slots (if any) so the glue's take() finds
1132                // the value even if main provides late.
1133                await_spawn_slot(node).await?;
1134                await_resources(node).await?;
1135                spawn(spawner)?;
1136            }
1137            node.set_running(true);
1138        }
1139        Ok(())
1140    }
1141
1142    /// Start a single node at runtime — e.g. growing an elastic pool. Resets the
1143    /// handle, spawns one instance via the node's `spawn` fn (which must launch
1144    /// exactly one), and marks it `running`. Returns `SpawnError::Busy` if the
1145    /// underlying embassy task pool is exhausted (the ceiling), which the caller
1146    /// treats as "can't grow".
1147    pub async fn start_node(
1148        &self,
1149        node: &'static TaskNode,
1150        spawner: Spawner,
1151    ) -> Result<(), SpawnError> {
1152        node.reset();
1153        if let Some(spawn) = node.spawn {
1154            await_spawn_slot(node).await?;
1155            await_resources(node).await?;
1156            spawn(spawner)?;
1157        }
1158        node.set_running(true);
1159        info!("supervisor: started {}", node.name);
1160        Ok(())
1161    }
1162
1163    /// Signal `node` to shut down, wait for its ack (panicking on timeout — a
1164    /// missing `ack_dropped()` somewhere), then clear `running`. Shared by
1165    /// `stop_node` and `teardown`; the caller must have checked `is_running`.
1166    async fn shutdown_and_wait(&self, node: &'static TaskNode) {
1167        node.signal_shutdown();
1168        if let Either::Second(()) = select(
1169            node.wait_dropped(),
1170            Timer::after_millis(SHUTDOWN_ACK_TIMEOUT_MS),
1171        )
1172        .await
1173        {
1174            panic!(
1175                "supervisor: task {} did not ack shutdown within {}ms",
1176                node.name, SHUTDOWN_ACK_TIMEOUT_MS,
1177            );
1178        }
1179        node.set_running(false);
1180    }
1181
1182    /// Stop a single running node at runtime — e.g. shrinking an elastic pool.
1183    /// Signals shutdown, waits for the ack, clears `running`. No-op if the node
1184    /// isn't running, or is [detached](TaskNode::set_detached) (self-managed — the
1185    /// supervisor never stops it). Panics if it doesn't ack within the timeout.
1186    pub async fn stop_node(&self, node: &'static TaskNode) {
1187        if !node.is_running() || node.is_detached() {
1188            return;
1189        }
1190        self.shutdown_and_wait(node).await;
1191        info!("supervisor: stopped {}", node.name);
1192    }
1193
1194    /// Signal every **running** node to shut down in **reverse** topological
1195    /// order, awaiting each node's ack before moving to its dependency. Down
1196    /// `OnDemand` nodes are skipped (no instance to ack). Pause-mode nodes ack
1197    /// and park on `wait_resume()`; Terminate/OnDemand nodes exit. Panics if a
1198    /// running node fails to ack within `SHUTDOWN_ACK_TIMEOUT_MS`.
1199    pub async fn teardown(&self) {
1200        for i in self.order.iter().rev() {
1201            let Some(node) = self.nodes[*i as usize] else {
1202                continue;
1203            };
1204            if !node.is_running() {
1205                continue;
1206            }
1207            // A detached node is self-managed; never tear it down. See
1208            // [`TaskNode::set_detached`].
1209            if node.is_detached() {
1210                continue;
1211            }
1212            info!("supervisor: tearing down {}", node.name);
1213            self.shutdown_and_wait(node).await;
1214        }
1215    }
1216
1217    /// Signal every Pause-mode node to resume. Cheap and synchronous — the tasks
1218    /// were parked on `wait_resume()` and pick up immediately. Called separately
1219    /// from `respawn_terminate` so the application can fire resume independently
1220    /// of the respawn step. Disabled (manually-paused) nodes are skipped so a
1221    /// manual pause sticks, and detached (self-managed) Pause nodes are left
1222    /// parked; there is intentionally no dependency gate here.
1223    pub fn resume_pausable(&self) {
1224        for i in self.order.iter() {
1225            let Some(node) = self.nodes[*i as usize] else {
1226                continue;
1227            };
1228            if matches!(node.mode, Mode::Pause) && !node.is_disabled() && !node.is_detached() {
1229                node.reset();
1230                info!("supervisor: resuming {}", node.name);
1231                node.signal_resume();
1232                node.set_running(true);
1233            }
1234        }
1235    }
1236
1237    /// Reset and re-spawn every Terminate-mode node in dependency order.
1238    /// Pause-mode nodes are untouched (use `resume_pausable`); `OnDemand` nodes
1239    /// are left down — they re-grow under load via `start_node`. Disabled nodes
1240    /// are skipped so a manual stop sticks across the bring-up. Detached nodes are
1241    /// skipped too: `teardown` never brought them down, so they are still running
1242    /// and re-spawning would double-spawn them (see [`TaskNode::set_detached`]). The
1243    /// reset happens before the spawn so newly-running tasks see a clean handle.
1244    pub async fn respawn_terminate(&self, spawner: Spawner) -> Result<(), SpawnError> {
1245        for i in self.order.iter() {
1246            let Some(node) = self.nodes[*i as usize] else {
1247                continue;
1248            };
1249            if matches!(node.mode, Mode::Terminate) && !node.is_disabled() && !node.is_detached() {
1250                node.reset();
1251                info!("supervisor: respawning {}", node.name);
1252                if let Some(spawn) = node.spawn {
1253                    await_spawn_slot(node).await?;
1254                    // A `resources:` node's previous instance restores its slot
1255                    // value only after the shutdown ack, so wait (bounded) for
1256                    // the restore before the glue's take().
1257                    await_resources(node).await?;
1258                    spawn(spawner)?;
1259                }
1260                node.set_running(true);
1261            }
1262        }
1263        Ok(())
1264    }
1265}
1266
1267// ─── Runtime control (dependency- and pool-honoring start/stop) ────────────
1268//
1269// The `apply_control` entry point drives one `ControlCommand` from the
1270// application's control surface. Unlike the pool's bare `start_node`/`stop_node`,
1271// these honor the graph: a stop cascades through dependents (so nothing is left
1272// running without a dependency), a start cascades through deps (so nothing comes
1273// up before what it needs), and either expands across a whole `ElasticPool` so
1274// the pool is controlled as a unit. A manual stop/pause also sets the
1275// lifecycle-spanning `disabled` flag, so it sticks against the elastic policy and
1276// the wake respawn.
1277
1278// Graph-index helpers used by BOTH the control plane and the pool driver, so they
1279// are gated on either feature — `pool` alone (no `control`) must still compile.
1280#[cfg(any(feature = "control", feature = "pool"))]
1281impl<const N: usize> Supervisor<N> {
1282    /// Position of `node` in `self.nodes` (pointer identity — every node is a
1283    /// `&'static`). `None` only if the node isn't in this graph (impossible for
1284    /// targets sourced from `GRAPH.nodes`; treated as a no-op by callers).
1285    fn index_of(&self, node: &'static TaskNode) -> Option<usize> {
1286        self.nodes
1287            .iter()
1288            .position(|n| n.is_some_and(|x| core::ptr::eq(x, node)))
1289    }
1290
1291    /// Whether every dependency of `node` is currently running, resolved through
1292    /// the graph's index table. The pool driver checks this before growing a
1293    /// worker, so a pool member is never spawned while one of its dependencies is
1294    /// down.
1295    #[cfg(feature = "pool")]
1296    pub(crate) fn deps_running(&self, node: &'static TaskNode) -> bool {
1297        match self.index_of(node) {
1298            Some(i) => self.deps[i]
1299                .iter()
1300                .all(|&di| self.nodes[di as usize].is_some_and(|n| n.is_running())),
1301            None => false,
1302        }
1303    }
1304}
1305
1306#[cfg(feature = "control")]
1307impl<const N: usize> Supervisor<N> {
1308    /// Seed a membership set with `target` plus — if `target` belongs to an
1309    /// elastic pool — every member of that pool, so control is applied to the
1310    /// whole pool atomically. Pool membership is read from `GRAPH.pools`; with no
1311    /// pools (the `pool` feature off, or none declared) this is just `{target}`.
1312    fn seed(&self, target: &'static TaskNode, set: &mut [bool; N]) {
1313        if let Some(i) = self.index_of(target) {
1314            set[i] = true;
1315        }
1316        #[cfg(feature = "pool")]
1317        for pool in self.pools {
1318            let members = pool.members();
1319            if members.iter().any(|m| core::ptr::eq(*m, target)) {
1320                for m in members {
1321                    if let Some(i) = self.index_of(m) {
1322                        set[i] = true;
1323                    }
1324                }
1325            }
1326        }
1327    }
1328
1329    /// Apply one control command, honoring pool membership and the dependency
1330    /// graph. Run from the supervisor's driver loop (never concurrently with
1331    /// itself), so the cascade is atomic from the application's perspective.
1332    pub async fn apply_control(&self, cmd: ControlCommand, spawner: Spawner) {
1333        match cmd.op {
1334            ControlOp::Deactivate => self.deactivate(cmd.node).await,
1335            ControlOp::Activate => self.activate(cmd.node, spawner).await,
1336        }
1337    }
1338
1339    /// Bring `target` (and its pool, and every transitive dependent) down, in
1340    /// reverse-topological order so each dependent stops before the dependency it
1341    /// relies on. Marks the whole set `disabled` so the stop sticks against the
1342    /// elastic policy and the wake respawn until a matching `activate`.
1343    async fn deactivate(&self, target: &'static TaskNode) {
1344        let mut set = [false; N];
1345        self.seed(target, &mut set);
1346
1347        // Grow the set to include transitive dependents. `order` is
1348        // dependency-first, so when we reach a node its deps are already decided;
1349        // a node joins if any dep it declares is already in the set.
1350        for i in self.order.iter() {
1351            let j = *i as usize;
1352            if set[j] {
1353                continue;
1354            }
1355            let Some(node) = self.nodes[j] else {
1356                continue;
1357            };
1358            // A detached node declares its dep only for start ordering and intends
1359            // to outlive it, so it's never pulled into the cascade.
1360            if node.is_detached() {
1361                continue;
1362            }
1363            if self.deps[j].iter().any(|&di| set[di as usize]) {
1364                set[j] = true;
1365            }
1366        }
1367
1368        // Tear down in reverse topo order (dependents before their deps).
1369        for i in self.order.iter().rev() {
1370            let j = *i as usize;
1371            if !set[j] {
1372                continue;
1373            }
1374            let Some(node) = self.nodes[j] else {
1375                continue;
1376            };
1377            // A detached node is self-managed — never control-stop it. The growth loop
1378            // keeps detached *dependents* out of the set; this also covers a detached
1379            // node that was seeded directly (or a detached pool member). Without it a
1380            // detached one-shot that already exited (stale `is_running`, no ack path)
1381            // would be signalled a shutdown it can never acknowledge, panicking here.
1382            if node.is_detached() {
1383                continue;
1384            }
1385            node.set_disabled(true);
1386            if node.is_running() {
1387                info!("supervisor: control-stop {}", node.name);
1388                self.shutdown_and_wait(node).await;
1389            }
1390        }
1391    }
1392
1393    /// Bring `target` (and its pool, and every transitive dependency) up, in
1394    /// topological order so each dependency starts before its dependent. Clears
1395    /// `disabled` across the set. `OnDemand` (pool) members are only re-enabled,
1396    /// not force-spawned — the elastic policy re-grows them under load, which is
1397    /// the whole point of the pool.
1398    async fn activate(&self, target: &'static TaskNode, spawner: Spawner) {
1399        let mut set = [false; N];
1400        self.seed(target, &mut set);
1401
1402        // Grow the set to include transitive deps. Walk dependents-first
1403        // (reverse topo); when a set member is seen, pull in its direct deps.
1404        // A detached member's `deps:` are start-ordering only (the node is
1405        // self-managed), so don't expand from it — mirrors deactivate's guard;
1406        // otherwise activating a detached target would un-disable deps that
1407        // were independently disabled.
1408        for i in self.order.iter().rev() {
1409            let j = *i as usize;
1410            if set[j] && !self.nodes[j].is_some_and(|n| n.is_detached()) {
1411                for &di in self.deps[j] {
1412                    set[di as usize] = true;
1413                }
1414            }
1415        }
1416
1417        // Bring up in topo order (deps before dependents).
1418        for i in self.order.iter() {
1419            let j = *i as usize;
1420            if !set[j] {
1421                continue;
1422            }
1423            let Some(node) = self.nodes[j] else {
1424                continue;
1425            };
1426            // A detached node is self-managed — the supervisor never re-enables or
1427            // re-starts it, even when it is a dependency of an activated target.
1428            if node.is_detached() {
1429                continue;
1430            }
1431            node.set_disabled(false);
1432            if node.is_running() {
1433                continue;
1434            }
1435            match node.mode {
1436                Mode::Terminate => {
1437                    info!("supervisor: control-start {}", node.name);
1438                    // SpawnError::Busy (pool exhausted) → can't start, skip.
1439                    let _ = self.start_node(node, spawner).await;
1440                }
1441                Mode::Pause => {
1442                    info!("supervisor: control-resume {}", node.name);
1443                    node.reset();
1444                    node.signal_resume();
1445                    node.set_running(true);
1446                }
1447                // Pool worker — leave it down; the elastic policy regrows it on
1448                // demand now that `disabled` is cleared.
1449                Mode::OnDemand => {}
1450            }
1451        }
1452    }
1453}
1454
1455// ─── Topological sort (Kahn's algorithm, const) ───────────────────────────
1456//
1457// Computes the topological order at *compile time* over a per-node
1458// dependency-index table; a dependency cycle is a compile error.
1459
1460/// Topologically sort a graph given as a per-node dependency-index table.
1461///
1462/// `deps[i]` lists the indices of the nodes that node `i` depends on; the result
1463/// lists node indices in dependency-first order (a dependency appears before its
1464/// dependents). The supervisor iterates it forward for `start` /
1465/// `respawn_terminate` and in reverse for `teardown`.
1466///
1467/// Evaluated at compile time by the code `supervisor_graph!` generates — a
1468/// dependency **cycle is a compile error** (the `panic!` fires during const
1469/// evaluation). `#[doc(hidden)]`: an engine for the macro, not a user-facing API.
1470///
1471/// Supports at most 256 nodes: indices are `u8`, so a larger `N` would truncate.
1472/// The macro rejects bigger graphs at expansion; the assert below is defense in
1473/// depth for a manual caller (a const-eval panic, i.e. a compile error).
1474#[doc(hidden)]
1475#[must_use]
1476pub const fn topo_sort_const<const N: usize>(deps: &[&'static [u8]; N]) -> [u8; N] {
1477    assert!(
1478        N <= 256,
1479        "supervisor graph exceeds 256 node slots (indices are u8)"
1480    );
1481    // in_degree[i] = number of deps of node i not yet resolved.
1482    let mut in_degree = [0u8; N];
1483    let mut i = 0;
1484    while i < N {
1485        in_degree[i] = deps[i].len() as u8;
1486        i += 1;
1487    }
1488
1489    // Queue (fixed array, head/tail indices) seeded with the dependency-free nodes.
1490    let mut queue = [0u8; N];
1491    let mut tail = 0;
1492    i = 0;
1493    while i < N {
1494        if in_degree[i] == 0 {
1495            queue[tail] = i as u8;
1496            tail += 1;
1497        }
1498        i += 1;
1499    }
1500
1501    let mut order = [0u8; N];
1502    let mut produced = 0;
1503    let mut head = 0;
1504    while head < tail {
1505        let node = queue[head] as usize;
1506        head += 1;
1507        order[produced] = node as u8;
1508        produced += 1;
1509
1510        // Decrement the in-degree of every node that depends on `node`.
1511        let mut j = 0;
1512        while j < N {
1513            if in_degree[j] != 0 {
1514                let mut depends = false;
1515                let mut k = 0;
1516                while k < deps[j].len() {
1517                    if deps[j][k] as usize == node {
1518                        depends = true;
1519                    }
1520                    k += 1;
1521                }
1522                if depends {
1523                    in_degree[j] -= 1;
1524                    if in_degree[j] == 0 {
1525                        queue[tail] = j as u8;
1526                        tail += 1;
1527                    }
1528                }
1529            }
1530            j += 1;
1531        }
1532    }
1533
1534    // A cycle leaves some nodes unproduced. During const eval this panic is a
1535    // compile error, so cyclic graphs are rejected at build time. `core::panic!`
1536    // (not the crate's defmt-shimmed `panic!`) keeps this const-evaluable.
1537    if produced != N {
1538        core::panic!("supervisor_graph!: dependency cycle");
1539    }
1540    order
1541}
1542
1543#[cfg(feature = "pool")]
1544mod pool;
1545#[cfg(feature = "pool")]
1546pub use pool::*;
1547
1548#[cfg(feature = "trace")]
1549pub mod trace;
1550
1551/// Declare a supervised task graph and compute its topological order at compile
1552/// time (single source of nodes, deps, pool, and order). See the
1553/// `embassy-supervisor-macros` crate for the surface syntax.
1554#[cfg(feature = "macros")]
1555pub use embassy_supervisor_macros::supervisor_graph;
1556
1557/// Building blocks for `supervisor_graph!`-generated code — NOT public API.
1558///
1559/// The macro's `local`-marked `resources:` entries emit a slot *type* at the
1560/// graph declaration site (it needs an `unsafe impl Sync`, — same reason the
1561/// `trace-hooks` symbols are emitted there). That generated type must name
1562/// the exact `Signal`/mutex types in [`ResourceGate`]'s signature; re-exporting
1563/// them here keeps the macro's contract that a consumer only needs
1564/// `embassy-supervisor` itself as a real-named dependency (not `embassy-sync`).
1565#[doc(hidden)]
1566pub mod _export {
1567    pub use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
1568    pub use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
1569    pub use embassy_sync::signal::Signal;
1570    // For the `slot_timeout:` clause's emitted `with_slot_timeout(..)` call.
1571    pub use embassy_time::Duration;
1572}
1573
1574// ─── Tests (host-only) ─────────────────────────────────────────────────────
1575//
1576// Run on the host: `cargo test -p embassy-supervisor --target x86_64-unknown-linux-gnu`
1577// (the workspace `.cargo/config.toml` pins the embedded target, so `--target` is
1578// required to override it). These exercise the compile-time `topo_sort_const`
1579// over index adjacency tables — exactly what `supervisor_graph!` generates.
1580#[cfg(test)]
1581mod tests {
1582    use super::topo_sort_const;
1583
1584    /// Position of index `x` within `order`.
1585    fn pos<const N: usize>(order: &[u8; N], x: u8) -> usize {
1586        order.iter().position(|&y| y == x).expect("index present")
1587    }
1588
1589    #[test]
1590    fn linear_chain_orders_deps_before_dependents() {
1591        // A=0, B=1 dep A, C=2 dep B.
1592        const DEPS: [&[u8]; 3] = [&[], &[0], &[1]];
1593        const ORDER: [u8; 3] = topo_sort_const(&DEPS);
1594        assert_eq!(ORDER, [0, 1, 2]);
1595    }
1596
1597    #[test]
1598    fn diamond_puts_root_first_and_join_last() {
1599        // A=0; B=1 dep A; C=2 dep A; D=3 dep B,C.
1600        const DEPS: [&[u8]; 4] = [&[], &[0], &[0], &[1, 2]];
1601        const ORDER: [u8; 4] = topo_sort_const(&DEPS);
1602        assert_eq!(ORDER[0], 0, "root first");
1603        assert_eq!(ORDER[3], 3, "join last");
1604        assert!(pos(&ORDER, 1) < pos(&ORDER, 3), "B before D");
1605        assert!(pos(&ORDER, 2) < pos(&ORDER, 3), "C before D");
1606    }
1607
1608    #[test]
1609    fn independent_nodes_all_present() {
1610        const DEPS: [&[u8]; 2] = [&[], &[]];
1611        const ORDER: [u8; 2] = topo_sort_const(&DEPS);
1612        assert!(ORDER.contains(&0) && ORDER.contains(&1));
1613    }
1614
1615    #[test]
1616    fn unsorted_input_is_sorted() {
1617        // Declared out of dependency order: node 0 depends on 1 and 2, node 2 on 1.
1618        const DEPS: [&[u8]; 3] = [&[1, 2], &[], &[1]];
1619        const ORDER: [u8; 3] = topo_sort_const(&DEPS);
1620        assert!(pos(&ORDER, 1) < pos(&ORDER, 2), "1 before 2");
1621        assert!(pos(&ORDER, 2) < pos(&ORDER, 0), "2 before 0");
1622    }
1623
1624    #[test]
1625    fn evaluates_at_compile_time() {
1626        // The sort runs in a `const` context, proving it is const-evaluable.
1627        // (A cyclic table here would be a *compile* error, not a test failure.)
1628        const DEPS: [&[u8]; 3] = [&[], &[0], &[1]];
1629        const _: () = {
1630            let order = topo_sort_const(&DEPS);
1631            assert!(order[0] == 0 && order[1] == 1 && order[2] == 2);
1632        };
1633    }
1634
1635    // Uncommenting this must fail to compile ("dependency cycle"):
1636    //   const CYCLE: [&[u8]; 2] = [&[1], &[0]];
1637    //   const _BAD: [u8; 2] = topo_sort_const(&CYCLE);
1638}