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. race long-lived work against the stop request — that's how a stop reaches
90//!      you. [`TaskNode::run_cancellable_acked`] is the everyday body (it owns the
91//!      `select` and acks for you; `Err(`[`Aborted`]`)` means a stop won),
92//!      [`TaskNode::run_cancellable`] the variant with cleanup between the two, and
93//!      [`TaskNode::wait_shutdown`] the raw signal when you write the `select`
94//!      yourself;
95//!   2. ack exactly once per stop with [`TaskNode::ack_dropped`]: on exit
96//!      (`Terminate`/`OnDemand`), or on each pause (`Pause`) *before* parking on
97//!      [`TaskNode::wait_resume`];
98//!   3. an autonomous exit calls [`TaskNode::mark_exited`] instead — it acks *and*
99//!      records completion, so the supervisor sees the node as down and
100//!      [`TaskNode::has_exited`] tells a body that returned on its own from one
101//!      that was stopped (a `task:` shell does it for you);
102//!   4. resources follow the mode: a `Terminate` task re-acquires everything on
103//!      respawn (drop-on-exit is the cleanup), a `Pause` task keeps what it holds
104//!      across the park.
105//!
106//! Pool workers additionally report load with [`TaskNode::mark_busy`] /
107//! [`TaskNode::mark_idle`] (a real transition fires the scale signal itself), and
108//! a self-managed daemon or run-once job opts out of supervision with
109//! [`TaskNode::set_detached`]. The README's *Writing supervised tasks* section has
110//! per-mode skeletons.
111//!
112//! ## Beyond bring-up
113//!
114//!   * [`Supervisor::run`] is bring-up plus the driver loop (pool scaling and the
115//!     control mailbox) in one call; it returns only on a [`RunError`], which the
116//!     application escalates. Drive the pieces yourself when the loop must watch
117//!     extra wake sources.
118//!   * Every shutdown path is fallible, never a library panic: [`Supervisor::teardown`]
119//!     aborts at the first node that misses its ack and returns a [`ShutdownTimeout`]
120//!     naming it, [`Supervisor::teardown_continue`] presses on through the rest and
121//!     reports the first failure at the end (the "hardware reset next anyway" path).
122//!   * `exit: Type` on a node adds a typed exit-value slot the application awaits
123//!     with [`ResourceSlot::wait_take`] — a run-once job hands its result back.
124//!     `state: Type = expr` (feature `heap-state`) boxes per-activation state that
125//!     is freed when the task exits, so a stopped subsystem costs no RAM.
126//!   * Feature `readiness` separates *spawned* from *serving*: a task asserts
127//!     `set_ready()` and a `deps: [NET ready]` edge makes bring-up (and pool growth)
128//!     wait for it. Feature `liveness` adds a per-node heartbeat (`beat()` /
129//!     `is_stale()`) for alive-but-wedged detection.
130//!   * A graph can span crates: `supervisor_fragment!` declares a module's nodes and
131//!     [`compose_graph!`] assembles the fragments into one graph. `name: IDENT;`
132//!     gives a second graph in the same binary its own statics and [`Supervisor`],
133//!     for a subordinate sub-graph an application starts and tears down as a unit.
134//!
135//! ## What the supervisor does *not* do
136//!
137//!   * It does not model any power-state transition (sleep/wake): it reacts to
138//!     "teardown" and "bring-up" requests; the application drives them.
139//!   * It does not allocate, and does no work at construction: the topological
140//!     sort runs at compile time (see the `supervisor_graph!` macro).
141//!   * It does not observe task internals. Tasks self-report their drop state via
142//!     `ack_dropped()` / `mark_exited()`; a task that misses the ack window comes
143//!     back as a [`ShutdownTimeout`] naming the node, for the application to act on.
144//!   * It does not catch panics: a panicking task is not captured or restarted.
145//!     Pair the supervisor with a hardware watchdog for crashes, and the `liveness`
146//!     heartbeat for tasks that are alive but wedged.
147//!
148//! ## Cargo features
149//!
150//!   * `control` *(default)* — the runtime control plane: [`ControlOp`],
151//!     [`request_control`], [`Supervisor::apply_control`].
152//!   * `pool` *(default)* — elastic worker pools: [`ElasticPool`],
153//!     [`Supervisor::run_pools`], and the `pools` field of [`Graph`].
154//!   * `macros` *(default)* — the [`supervisor_graph!`] graph-declaration macro (and
155//!     `supervisor_fragment!` / [`compose_graph!`]).
156//!   * `local-resources` — permit the `local` resource kind; ⚠ opting in to the macro
157//!     emitting a documented `unsafe impl Sync` into your crate.
158//!   * `readiness` — `set_ready`/`clear_ready`/`wait_ready` plus the `ready` dep
159//!     marker, gating bring-up and pool growth on *serving*, not merely spawned.
160//!   * `liveness` — a per-node heartbeat: `beat()`, `ticks_since_beat()`,
161//!     `is_stale(max_age)`. A fresh spawn counts as a beat.
162//!   * `heap-state` — the `state: Type = expr` clause: per-activation boxed state,
163//!     reclaimed on task exit; ⚠ emits a small `unsafe` fallible-boxing helper and
164//!     needs a `#[global_allocator]`.
165//!   * `defmt` — route the supervisor's logs through `defmt`; without it the log
166//!     macros are no-ops.
167//!   * `trace` family (all opt-in) — `trace`: the `trace` module's recorders consuming
168//!     embassy-executor's `_embassy_trace_*` hooks; `trace-hooks`:
169//!     `supervisor_graph!` also *defines* the hook symbols; `metadata-names`: node
170//!     names stamped into task Metadata for external consumers (rtos-trace/
171//!     SystemView) — independent of `trace`, so it needs no hook symbols and pairs
172//!     with embassy's own `rtos-trace`; `trace-names`: shorthand for `trace` +
173//!     `metadata-names`; `trace-nested`: preemption-exact accounting (a nested
174//!     higher-tier poll credits its time back to the window it interrupted).
175//!
176//! Build with `default-features = false` for a minimal core that only does
177//! dependency-ordered bring-up/teardown (drops the control plane and pools,
178//! trimming flash and a couple of statics).
179//!
180//! ## Example
181//!
182//! [`supervisor_graph!`] declares the whole graph once — it generates the node
183//! `static`s and a single [`Graph`] value `GRAPH` bundling the node slots, dep
184//! table, and compile-time topological order (a dependency cycle is a compile
185//! error), which [`Supervisor::new`] consumes.
186//!
187//! ```ignore
188//! use embassy_executor::Spawner;
189//! use embassy_supervisor::{supervisor_graph, RunError, Supervisor, TaskNode};
190//!
191//! // `app` depends on `net`; `task:` names a plain async worker fn the macro wraps
192//! // in its `#[embassy_executor::task]` shell (`spawn:` takes one you wrote yourself).
193//! supervisor_graph! {
194//!     node NET = Terminate, deps: [], task: net_task;
195//!     node APP = Terminate, deps: [NET], task: app_task;
196//! }
197//!
198//! // Plain async fns taking the node first — no embassy attribute needed. The
199//! // combinator owns the shutdown `select` and the ack.
200//! async fn net_task(node: &'static TaskNode) {
201//!     let _ = node.run_cancellable_acked(async { /* serve forever */ }).await;
202//! }
203//! async fn app_task(node: &'static TaskNode) {
204//!     let _ = node.run_cancellable_acked(async { /* serve forever */ }).await;
205//! }
206//!
207//! #[embassy_executor::task]
208//! async fn supervisor_task(spawner: Spawner) {
209//!     // Infallible: the order is precomputed, so a dependency cycle is a compile error.
210//!     let sup = Supervisor::new(&GRAPH);
211//!     // Brings up `net`, then `app`, then drives pool scaling and runtime control
212//!     // requests (start/stop/pause/resume, applied in dependency order) forever;
213//!     // returns only on error, which the application escalates — typically a panic
214//!     // into a hardware-watchdog reset.
215//!     match sup.run(spawner).await {
216//!         RunError::Spawn(_) => panic!("bring-up failed"),
217//!         RunError::Shutdown(e) => panic!("{} missed its shutdown ack", e.node.name),
218//!     }
219//!     // Call the pieces yourself (`start`, then a `select(run_pools, wait_control)`
220//!     // loop) when the driver must watch extra wake sources.
221//! }
222//! ```
223//!
224//! The `firmware` crate in the [repository](https://github.com/cedrivard/embassy-supervisor)
225//! is a complete working example (USB-net, an HTTP control plane, an elastic pool,
226//! and OTA).
227
228#[macro_use]
229mod fmt;
230
231use core::cell::Cell;
232use core::future::Future;
233use core::pin::Pin;
234use core::sync::atomic::Ordering;
235use core::task::{Context, Poll};
236
237use embassy_executor::{SendSpawner, SpawnError, Spawner};
238use embassy_futures::select::{Either, select};
239use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
240use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
241#[cfg(feature = "control")]
242use embassy_sync::channel::Channel;
243use embassy_sync::signal::Signal;
244use embassy_time::{Timer, with_timeout};
245use portable_atomic::AtomicBool;
246#[cfg(any(feature = "trace", feature = "liveness"))]
247use portable_atomic::AtomicU32;
248
249// ─── Scale-request signal (task → supervisor) ──────────────────────────────
250//
251// Elastic pool workers fire this when their busy/idle status changes; the
252// supervisor's `run_pools` loop awaits it and re-runs the pool policies
253// (`ElasticPool`). Single-consumer `Signal`: many tasks may `signal()`, only the
254// supervisor `wait()`s. This is the *only* path by which task status reaches the
255// supervisor — it never polls.
256#[cfg(feature = "pool")]
257static SCALE_REQ: Signal<CriticalSectionRawMutex, ()> = Signal::new();
258
259/// Fire the scale-request signal. Called by a task on a busy/idle transition.
260/// A no-op when the `pool` feature is disabled (no pools to re-evaluate).
261pub fn request_scale() {
262    #[cfg(feature = "pool")]
263    SCALE_REQ.signal(());
264}
265
266/// Await the next scale request. The supervisor's driver loop selects this
267/// against its other wake sources and runs the scaling policy on each wake.
268#[cfg(feature = "pool")]
269pub async fn wait_scale() {
270    SCALE_REQ.wait().await;
271}
272
273// ─── Runtime control commands (app → supervisor) ───────────────────────────
274//
275// An application's control surface (e.g. a network endpoint) usually can't drive
276// the supervisor directly: the `Supervisor` and the `Spawner` live on the
277// supervisor task's stack, not in a `static`. So control is decoupled via this
278// channel — the caller `request_control()`s a (node, op) pair; the supervisor's
279// driver loop `wait_control()`s it and runs the dependency-honoring
280// `apply_control`. A `Channel` (not a `Signal`) so back-to-back requests aren't
281// coalesced; capacity 4 is ample for hand-driven control. Delivery is lossless:
282// `request_control` awaits free capacity, and the sync `try_request_control`
283// surfaces a full mailbox as an error instead of dropping the request — a
284// silently vanished emergency stop is the one failure mode this mailbox is not
285// allowed to have.
286
287/// Which way to drive a node. Higher-level verbs fold onto these two:
288/// `start`/`resume` → `Activate`, `stop`/`pause` → `Deactivate`. The concrete
289/// mechanism (respawn vs resume vs leave-to-pool) is then chosen per node `Mode`
290/// by the supervisor when it applies the command ([`Supervisor::apply_control`]).
291#[cfg(feature = "control")]
292#[derive(Clone, Copy, PartialEq, Eq, Debug)]
293pub enum ControlOp {
294    /// Bring the node up (start a stopped `Terminate` node, resume a `Pause` node).
295    Activate,
296    /// Take the node down (and its dependents, per the graph).
297    Deactivate,
298}
299
300/// A runtime control request: drive `node` (and, per the dependency graph and
301/// pool membership, the nodes it implies) in the `op` direction.
302#[cfg(feature = "control")]
303#[derive(Clone, Copy, Debug)]
304pub struct ControlCommand {
305    /// The node to drive.
306    pub node: &'static TaskNode,
307    /// The direction to drive it.
308    pub op: ControlOp,
309}
310
311/// App → supervisor control mailbox. `&'static TaskNode` is `Copy + Sync`, so
312/// the target rides the channel directly — no name lookup needed supervisor-side.
313#[cfg(feature = "control")]
314static CONTROL_REQ: Channel<CriticalSectionRawMutex, ControlCommand, 4> = Channel::new();
315
316/// The control mailbox was full (4 outstanding requests) and the request was
317/// not enqueued. Returned by [`try_request_control`]; retry after the
318/// supervisor's driver loop has drained a command, or use the awaiting
319/// [`request_control`] from async contexts.
320#[cfg(feature = "control")]
321#[derive(Clone, Copy, PartialEq, Eq, Debug)]
322pub struct ControlQueueFull;
323
324#[cfg(all(feature = "control", feature = "defmt"))]
325impl defmt::Format for ControlQueueFull {
326    fn format(&self, fmt: defmt::Formatter) {
327        defmt::write!(fmt, "control queue full");
328    }
329}
330
331/// Enqueue a control request, waiting for mailbox capacity if it is full.
332/// Lossless — the request is delivered once the supervisor's driver loop drains
333/// an earlier command. Called by the application's control surface.
334#[cfg(feature = "control")]
335pub async fn request_control(node: &'static TaskNode, op: ControlOp) {
336    CONTROL_REQ.send(ControlCommand { node, op }).await;
337}
338
339/// Non-blocking variant of [`request_control`] for sync contexts (ISRs,
340/// callbacks). Fails with [`ControlQueueFull`] instead of dropping the request
341/// when the mailbox is full — the caller decides whether to retry or surface it.
342#[cfg(feature = "control")]
343pub fn try_request_control(node: &'static TaskNode, op: ControlOp) -> Result<(), ControlQueueFull> {
344    CONTROL_REQ
345        .try_send(ControlCommand { node, op })
346        .map_err(|_| ControlQueueFull)
347}
348
349/// Await the next control request. Selected by the supervisor's driver loop
350/// against pool scaling and any other application wake sources.
351#[cfg(feature = "control")]
352pub async fn wait_control() -> ControlCommand {
353    CONTROL_REQ.receive().await
354}
355
356/// Per-node timeout for `wait_dropped`. A task that doesn't ack within this
357/// window is a bug (e.g. a missing `ack_dropped()` call) or a wedge; the
358/// shutdown paths surface it as a [`ShutdownTimeout`] naming the node, and the
359/// application decides the escalation. 2 s comfortably exceeds a typical task's
360/// poll period and peripheral settle time.
361const SHUTDOWN_ACK_TIMEOUT_MS: u64 = 2_000;
362
363/// A node failed to ack a requested shutdown within `SHUTDOWN_ACK_TIMEOUT_MS`.
364/// Returned by [`Supervisor::stop_node`], [`Supervisor::teardown`],
365/// [`Supervisor::teardown_continue`] and (feature `control`)
366/// [`Supervisor::apply_control`]. The node is still marked running; the sane
367/// escalations are app-level — a hardware watchdog reset, `panic!`, or a retry.
368#[derive(Clone, Copy, Debug)]
369pub struct ShutdownTimeout {
370    /// The node that missed its ack window.
371    pub node: &'static TaskNode,
372}
373
374#[cfg(feature = "defmt")]
375impl defmt::Format for ShutdownTimeout {
376    fn format(&self, fmt: defmt::Formatter) {
377        defmt::write!(fmt, "{} missed shutdown ack", self.node.name);
378    }
379}
380
381/// Why [`Supervisor::run`] stopped — it only returns on error, and every arm is
382/// an app-level escalation (typically `panic!` into a hardware-watchdog reset).
383#[cfg(any(feature = "pool", feature = "control"))]
384#[derive(Clone, Copy, Debug)]
385pub enum RunError {
386    /// Bring-up failed: a spawn error out of the initial [`Supervisor::start`]
387    /// (task-pool exhaustion, or a gate/slot wait that timed out as `Busy`).
388    Spawn(SpawnError),
389    /// A node missed its shutdown ack during a control cascade or pool shrink.
390    Shutdown(ShutdownTimeout),
391}
392
393#[cfg(all(any(feature = "pool", feature = "control"), feature = "defmt"))]
394impl defmt::Format for RunError {
395    fn format(&self, fmt: defmt::Formatter) {
396        match self {
397            RunError::Spawn(_) => defmt::write!(fmt, "bring-up spawn failed"),
398            RunError::Shutdown(e) => defmt::write!(fmt, "{}", e),
399        }
400    }
401}
402
403/// The shutdown side of [`TaskNode::run_cancellable`]'s result: the raced work
404/// future was cancelled at its await point because a stop/pause request won the
405/// select. Pairs naturally with the `exit:` slot — a worker returning
406/// `Result<R, Aborted>` records completed-vs-cancelled for whoever reads the
407/// exit value.
408#[derive(Clone, Copy, PartialEq, Eq, Debug)]
409pub struct Aborted;
410
411#[cfg(feature = "defmt")]
412impl defmt::Format for Aborted {
413    fn format(&self, fmt: defmt::Formatter) {
414        defmt::write!(fmt, "aborted by shutdown");
415    }
416}
417
418pin_project_lite::pin_project! {
419    /// The future behind [`TaskNode::run_cancellable`] and
420    /// [`run_cancellable_acked`](TaskNode::run_cancellable_acked): races the
421    /// worker against the node's shutdown signal, holding the worker's state
422    /// machine **once**.
423    ///
424    /// Written by hand rather than as `select(fut, wait_shutdown()).await` inside
425    /// an `async fn`, because that shape stores the worker both as the function's
426    /// argument and inside the select (rust-lang/rust#62958) — a doubling paid in
427    /// every caller's static task storage, which for a graph of `cancel` nodes is
428    /// the sum of every worker future in the binary.
429    ///
430    /// `fut` is an `Option` so the abort path can drop the worker in place, via
431    /// the safe `Pin::set`, *before* acking: the ack is what releases the
432    /// supervisor's teardown wait, and a runner whose `Drop` frees hardware must
433    /// have run by then.
434    struct RunCancellable<'a, F, S> {
435        #[pin]
436        fut: Option<F>,
437        #[pin]
438        shutdown: S,
439        // `Some` only for the `_acked` variant.
440        ack: Option<&'a TaskNode>,
441    }
442}
443
444impl<F: Future, S: Future<Output = ()>> Future for RunCancellable<'_, F, S> {
445    type Output = Result<F::Output, Aborted>;
446
447    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
448        let mut this = self.project();
449        // Worker first, shutdown second — the polling order `select` gave this
450        // before, so a worker that completes in the same wake as a stop request
451        // still reports completion rather than abort.
452        if let Some(fut) = this.fut.as_mut().as_pin_mut()
453            && let Poll::Ready(out) = fut.poll(cx)
454        {
455            return Poll::Ready(Ok(out));
456        }
457        match this.shutdown.poll(cx) {
458            Poll::Ready(()) => {
459                this.fut.set(None);
460                if let Some(node) = this.ack {
461                    node.ack_dropped();
462                }
463                Poll::Ready(Err(Aborted))
464            }
465            Poll::Pending => Poll::Pending,
466        }
467    }
468}
469
470/// How long the supervisor's bring-up waits for a node's `executor:`
471/// [`SpawnerSlot`] to be filled before failing the spawn with
472/// [`SpawnError::Busy`]. A genuine cross-core rendezvous resolves in microseconds;
473/// a slot empty this long is a misconfiguration (the app never registered that
474/// executor's spawner). Bounded, so a misconfigured graph fails loudly instead of
475/// hanging bring-up forever.
476const SLOT_READY_TIMEOUT: embassy_time::Duration = embassy_time::Duration::from_millis(100);
477
478// ─── Mode ────────────────────────────────────────────────────────────────
479
480/// Lifecycle policy for a managed task: what the task does on shutdown and what
481/// the supervisor does to bring it back.
482#[derive(Clone, Copy, PartialEq, Eq, Debug)]
483pub enum Mode {
484    /// Task exits its loop on shutdown. The supervisor respawns it via the
485    /// node's `spawn` fn from `respawn_terminate`.
486    Terminate,
487    /// Task acks shutdown and parks on `wait_resume()`. The supervisor resumes
488    /// it from `resume_pausable`; the task is never respawned, so it keeps any
489    /// resource it holds (a peripheral handle, a socket) across the pause.
490    Pause,
491    /// Like `Terminate` (exits on shutdown), but **not** started at boot and
492    /// **not** auto-respawned. The supervisor brings it up and down at runtime
493    /// via `start_node` / `stop_node` in response to load — see [`ElasticPool`].
494    /// `start()` skips it; `respawn_terminate()` leaves it down (it
495    /// re-grows under demand); `teardown()` only acts on it while it is running.
496    OnDemand,
497}
498
499impl Mode {
500    /// Stable lower-case wire name, used both for serialization (e.g. a JSON
501    /// task-state view) and for `defmt` logging — the single source of these
502    /// strings.
503    pub fn as_str(&self) -> &'static str {
504        match self {
505            Mode::Terminate => "terminate",
506            Mode::Pause => "pause",
507            Mode::OnDemand => "ondemand",
508        }
509    }
510}
511
512#[cfg(feature = "defmt")]
513impl defmt::Format for Mode {
514    fn format(&self, f: defmt::Formatter) {
515        defmt::write!(f, "{}", self.as_str());
516    }
517}
518
519// ─── TaskHandle ──────────────────────────────────────────────────────────
520
521/// Coordination state for one task. Embedded inside [`TaskNode`].
522///
523/// Every node is single-instance, so each field is a per-node atomic flag or a
524/// single-consumer signal — no counts, no fan-out. Written by one side (task or
525/// supervisor) and read by the other:
526///   * `shutdown` / `shutdown_wake` — supervisor requests exit; the task parks
527///     on the signal and reads the flag.
528///   * `dropped` / `dropped_wake` — the task acks its exit; the supervisor
529///     parks on the signal (with a timeout) and reads the flag.
530///   * `resume_wake` — supervisor resumes a parked Pause-mode task.
531///   * `running` — supervisor's record that the node is spawned; `busy` — the
532///     task's active/idle status. Both read by the elastic scaling policy.
533///   * `disabled` — the node has been manually deactivated; see below.
534pub struct TaskHandle {
535    /// Set true by the supervisor when shutdown is requested.
536    /// Cleared by `reset()` before the next spawn.
537    shutdown: AtomicBool,
538    /// Wake source for `wait_shutdown()`. Fired by `signal_shutdown()`.
539    shutdown_wake: Signal<CriticalSectionRawMutex, ()>,
540    /// Set true by the instance when it acks the shutdown (a bool, not a count,
541    /// since every node is single-instance). Cleared by `reset()`.
542    dropped: AtomicBool,
543    /// Wake source for `wait_dropped()`. Fired by `ack_dropped()`.
544    dropped_wake: Signal<CriticalSectionRawMutex, ()>,
545    /// True while the supervisor has the node spawned and it hasn't exited.
546    /// Always-on nodes are set true by `start()`; `OnDemand` nodes are set
547    /// true/false by `start_node()` / `stop_node()`. `teardown()` only acts on
548    /// `running` nodes, so a down `OnDemand` node doesn't stall it.
549    running: AtomicBool,
550    /// True while the task is actively serving (its active/idle status). Set by
551    /// `mark_busy()` / `mark_idle()`; read by the scaling policy.
552    busy: AtomicBool,
553    /// Set true by `mark_exited()` when the task body has returned — by the
554    /// generated `task:` shell automatically, or by a hand-written `spawn:` task
555    /// on its way out. Cleared by `reset()` before the next spawn. Together with
556    /// the lifecycle-spanning `shutdown` flag this distinguishes an autonomous
557    /// completion (`completed && !shutdown`) from an acked stop.
558    completed: AtomicBool,
559    /// Wake source for `wait_resume()` on Pause-mode tasks. Fired by
560    /// `signal_resume()`.
561    resume_wake: Signal<CriticalSectionRawMutex, ()>,
562    /// Task-asserted readiness ("initialized and serving", e.g. DHCP bound) —
563    /// distinct from `running` (spawned). Set by `set_ready()`, cleared by
564    /// `clear_ready()` and by `reset()` so a respawned provider re-asserts.
565    #[cfg(feature = "readiness")]
566    ready: AtomicBool,
567    /// Wake source for `wait_ready()`. Latching; the supervisor's bring-up is
568    /// the only pre-fill waiter (single-waiter Signal semantics).
569    #[cfg(feature = "readiness")]
570    ready_wake: Signal<CriticalSectionRawMutex, ()>,
571    /// Instant ticks (truncated) of the last `beat()`; also stamped by
572    /// `set_running(true)` so a freshly spawned node is never instantly stale.
573    #[cfg(feature = "liveness")]
574    last_beat: AtomicU32,
575    /// True while the node has been manually deactivated (stopped/paused) via the
576    /// runtime control interface (`Supervisor::deactivate`). Unlike the other
577    /// flags this one is **lifecycle-spanning**: it is *not* cleared by
578    /// `reset()`, so a manual stop "sticks" — the automatic bring-up paths
579    /// (`start`, `respawn_terminate`, `resume_pausable`, and the elastic pool's
580    /// grow) skip a node while it is set. Cleared only by `Supervisor::activate`.
581    /// Because it lives in a `static`, it also survives a power-state transition
582    /// that retains RAM (e.g. a warm-resume from deep sleep).
583    disabled: AtomicBool,
584    /// Self-managed: while set, the supervisor never drives this node — teardown,
585    /// deactivate/activate, `stop_node`, respawn, and pause-resume all skip it. Not
586    /// cleared by `reset()`. Full rationale on [`TaskNode::set_detached`].
587    detached: AtomicBool,
588    /// The executor task id currently running this node (`TaskRef::id()`, captured
589    /// from the `SpawnToken` by the macro's spawn glue). `0` = unknown (not yet
590    /// spawned, or a parked/closure-spawned node that never registered). Overwritten
591    /// on every (re)spawn, so — unlike an external tracker — it stays correct across
592    /// respawns without any unlinking.
593    #[cfg(feature = "trace")]
594    task_id: AtomicU32,
595    /// Accumulated executor-poll time for this node, in embassy-time ticks,
596    /// wrapping. Consumers sample twice and `wrapping_sub` to get a rate; the
597    /// crate does no windowing.
598    #[cfg(feature = "trace")]
599    exec_ticks: AtomicU32,
600    /// Number of executor polls of this node, wrapping.
601    #[cfg(feature = "trace")]
602    polls: AtomicU32,
603    /// Longest single poll ever observed, in ticks — the "never yields" watermark.
604    /// A large value names the node that hogged the executor even after the fact,
605    /// which a live check cannot do from the blocked executor itself.
606    #[cfg(feature = "trace")]
607    max_poll_ticks: AtomicU32,
608}
609
610impl TaskHandle {
611    const fn new(disabled_at_boot: bool) -> Self {
612        Self {
613            shutdown: AtomicBool::new(false),
614            shutdown_wake: Signal::new(),
615            dropped: AtomicBool::new(false),
616            dropped_wake: Signal::new(),
617            running: AtomicBool::new(false),
618            busy: AtomicBool::new(false),
619            completed: AtomicBool::new(false),
620            resume_wake: Signal::new(),
621            #[cfg(feature = "readiness")]
622            ready: AtomicBool::new(false),
623            #[cfg(feature = "readiness")]
624            ready_wake: Signal::new(),
625            #[cfg(feature = "liveness")]
626            last_beat: AtomicU32::new(0),
627            disabled: AtomicBool::new(disabled_at_boot),
628            detached: AtomicBool::new(false),
629            #[cfg(feature = "trace")]
630            task_id: AtomicU32::new(0),
631            #[cfg(feature = "trace")]
632            exec_ticks: AtomicU32::new(0),
633            #[cfg(feature = "trace")]
634            polls: AtomicU32::new(0),
635            #[cfg(feature = "trace")]
636            max_poll_ticks: AtomicU32::new(0),
637        }
638    }
639}
640
641// ─── Executor spawner slots ──────────────────────────────────────────────
642
643/// A runtime-filled slot holding the [`SendSpawner`] of an executor other than
644/// the one the supervisor runs on — an `InterruptExecutor` tier, the second
645/// core's executor, any foreign thread executor (via `Spawner::make_send()`).
646///
647/// Declared by the `executor NAME;` item of [`supervisor_graph!`]; nodes carrying
648/// `executor: NAME` are spawned through the slot instead of the supervisor's own
649/// `Spawner`. The application fills it once at startup — before, or concurrently
650/// with, [`Supervisor::start`] (e.g. from the second core's bring-up):
651///
652/// ```ignore
653/// static EXECUTOR_HIGH: InterruptExecutor = InterruptExecutor::new();
654/// HIGH.set(EXECUTOR_HIGH.start(interrupt::SWI_IRQ_0));
655/// sup.start(spawner).await?;   // nodes declared `executor: HIGH` spawn on that tier
656/// ```
657///
658/// The supervisor's bring-up (`start` / `start_node` / `respawn_terminate`) awaits
659/// [`ready`](Self::ready) for a node's slot before spawning it, so a tier filled
660/// late — or from another core — is handled without a race; a slot still empty after
661/// the supervisor's bounded wait fails the spawn with [`SpawnError::Busy`] rather
662/// than silently dropping the task. Spawned futures must be `Send` (a non-`Send`
663/// `executor:` task is a compile error at the glue).
664pub struct SpawnerSlot {
665    slot: BlockingMutex<CriticalSectionRawMutex, Cell<Option<SendSpawner>>>,
666    /// Wakes a `ready()` waiter when `set` fills the slot (cross-core safe:
667    /// `Signal` is critical-section based and latches).
668    filled: Signal<CriticalSectionRawMutex, ()>,
669}
670
671impl SpawnerSlot {
672    /// An empty slot (`const` — it lives in a `static` the macro emits).
673    pub const fn new() -> Self {
674        Self {
675            slot: BlockingMutex::new(Cell::new(None)),
676            filled: Signal::new(),
677        }
678    }
679
680    /// Fill the slot (last set wins) and wake a [`ready`](Self::ready) waiter.
681    /// Call before [`Supervisor::start`] — or from the other core's bring-up,
682    /// with the supervisor awaiting `ready()`.
683    pub fn set(&self, spawner: SendSpawner) {
684        self.slot.lock(|c| c.set(Some(spawner)));
685        self.filled.signal(());
686    }
687
688    /// The registered spawner, or `None` while unfilled.
689    pub fn get(&self) -> Option<SendSpawner> {
690        self.slot.lock(Cell::get)
691    }
692
693    /// Await the slot and return the spawner. The rendezvous primitive: the
694    /// supervisor's bring-up awaits this for a node's `executor:` slot before
695    /// spawning it (bounded, see [`Supervisor::start`]), so a tier filled late — or
696    /// from another core — is handled without a race. Returns immediately once the
697    /// slot is filled, so any number of *late* callers are fine (an application can
698    /// gate work on the executor being up). While the slot is still empty, at most
699    /// one task should be parked here: the underlying `Signal` holds a single waker,
700    /// so a second pre-fill waiter would displace the first.
701    pub async fn ready(&self) -> SendSpawner {
702        loop {
703            if let Some(sp) = self.get() {
704                return sp;
705            }
706            // `Signal` latches: a `set()` racing between the check above and
707            // this wait still wakes us.
708            self.filled.wait().await;
709        }
710    }
711}
712
713impl Default for SpawnerSlot {
714    fn default() -> Self {
715        Self::new()
716    }
717}
718
719// ─── ResourceSlot ────────────────────────────────────────────────────────
720
721/// Type-erased readiness view of a [`ResourceSlot`], for the supervisor's
722/// bring-up wait.
723///
724/// A `TaskNode` can gate on any number of slots of *different* `T`s, so the node
725/// stores `&'static [&'static dyn ResourceGate]` (object-safe: no `T` in the
726/// signatures). Same shape as embassy's `dyn` driver registries — see
727/// <https://doc.rust-lang.org/reference/items/traits.html#object-safety>.
728/// The supervisor only needs "is it filled?" plus the signal to park on; taking
729/// the value stays in the generated spawn glue, where the concrete `T` is known.
730pub trait ResourceGate: Sync {
731    /// Non-consuming "is the slot currently filled" check.
732    fn is_filled(&self) -> bool;
733    /// The latching [`Signal`] fired by `provide`/`restore`, for the supervisor's
734    /// bounded pre-spawn wait (see [`Supervisor::start`]).
735    fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()>;
736}
737
738/// A one-value handoff cell threading an owned resource from `main` into a
739/// supervised task — the safe replacement for `Peripherals::steal()` inside
740/// the task body.
741///
742/// Declared (as a `pub static`) by [`supervisor_graph!`] for each entry in a
743/// node's `resources:` clause. The protocol:
744///
745/// 1. `main` splits `Peripherals` and **moves** the resource in with
746///    [`provide`](Self::provide). This is where the compile-time guarantee
747///    lives: the singleton field is *consumed*, so no second owner — and no
748///    `unsafe` steal — can exist.
749/// 2. The generated spawn glue [`take`](Self::take)s it just before spawning
750///    the node. An empty slot fails the spawn with `SpawnError::Busy` — a
751///    fail-closed error out of [`Supervisor::start`], not a panic inside the
752///    task (compare `static_cell::StaticCell`, which panics on misuse).
753/// 3. The generated task shell hands the worker `&mut T` and
754///    [`restore`](Self::restore)s the value after the worker returns, so a
755///    `Terminate` respawn re-takes the *same instance* instead of stealing a
756///    fresh one. (A `Pause` worker never returns — it parks — so it simply
757///    retains the resource, exactly like a hand-written parked task.)
758///
759/// Same primitives as [`SpawnerSlot`]: a critical-section
760/// [`BlockingMutex`]`<`[`Cell`]`<Option<T>>>` for the value (`Sync` for
761/// `T: Send`, provided by embassy-sync — no `unsafe` here) plus a latching
762/// [`Signal`] so the supervisor can await late provisioning (bounded; see
763/// [`Supervisor::start`]).
764pub struct ResourceSlot<T> {
765    slot: BlockingMutex<CriticalSectionRawMutex, Cell<Option<T>>>,
766    /// Wakes the supervisor's pre-spawn wait when `provide`/`restore` fills the
767    /// slot (latching, so a fill racing the check-then-wait still wakes it).
768    filled: Signal<CriticalSectionRawMutex, ()>,
769}
770
771impl<T> ResourceSlot<T> {
772    /// An empty slot (`const` — it lives in a `static` the macro emits).
773    pub const fn new() -> Self {
774        Self {
775            slot: BlockingMutex::new(Cell::new(None)),
776            filled: Signal::new(),
777        }
778    }
779
780    /// Move the resource in (from `main`'s `Peripherals` split) and wake the
781    /// supervisor's pre-spawn wait. Call before [`Supervisor::start`]; a slot
782    /// still empty after the supervisor's bounded wait fails that node's spawn
783    /// with `SpawnError::Busy`. Filling an occupied slot replaces (drops) the
784    /// old value — don't: one resource, one slot, moved exactly once.
785    pub fn provide(&self, value: T) {
786        self.slot.lock(|c| c.set(Some(value)));
787        self.filled.signal(());
788    }
789
790    /// Take the resource out, leaving the slot empty. Called by the generated
791    /// spawn glue just before the spawn; `None` means "not provided yet" or
792    /// "currently held by a live task instance".
793    pub fn take(&self) -> Option<T> {
794        self.slot.lock(Cell::take)
795    }
796
797    /// Copy the resource out **without emptying the slot** — the `shared`
798    /// resource kind's read: any number of consumers (several nodes, a whole
799    /// pool) get the same `Copy` handle, and the slot stays filled for the
800    /// next one. Only for `T: Copy` (a `Stack`-like handle, a `&'static`
801    /// registry ref); an owned singleton uses [`take`](Self::take).
802    pub fn get(&self) -> Option<T>
803    where
804        T: Copy,
805    {
806        // Same peek shape as `is_filled`: `Cell` has no `&T` access, so
807        // take-copy-put-back under one critical section.
808        self.slot.lock(|c| {
809            let v = c.take();
810            c.set(v);
811            v
812        })
813    }
814
815    /// Put the resource back for the next spawn. Called by the generated task
816    /// shell after the worker returns (i.e. after its clean shutdown ack), so a
817    /// respawn re-takes the same instance.
818    pub fn restore(&self, value: T) {
819        self.provide(value);
820    }
821
822    /// Await the slot being filled, then take the value — how an application
823    /// reads a node's `exit:` slot (the shell `provide()`s the worker's return
824    /// value there just before recording the exit). Check-then-park, so a value
825    /// provided earlier is returned immediately; the latching signal carries
826    /// the same single-pre-fill-waiter caveat as [`SpawnerSlot::ready`] — for
827    /// N concurrent readers fan out through an app-owned `Watch` instead.
828    pub async fn wait_take(&self) -> T {
829        loop {
830            if let Some(v) = self.take() {
831                return v;
832            }
833            self.filled.wait().await;
834        }
835    }
836}
837
838// `T: Send` (not just any `T`): the gate is reachable from the supervisor task,
839// which may run on a different core than the provider — the same bound the
840// inner `BlockingMutex` requires for `Sync`, restated here so the `dyn` upcast
841// can't outrun it.
842impl<T: Send> ResourceGate for ResourceSlot<T> {
843    fn is_filled(&self) -> bool {
844        // Peek without consuming: `Cell` has no `&T` access (no `T: Copy`
845        // here), so take-and-put-back under the same critical section.
846        self.slot.lock(|c| {
847            let v = c.take();
848            let filled = v.is_some();
849            c.set(v);
850            filled
851        })
852    }
853
854    fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()> {
855        &self.filled
856    }
857}
858
859impl<T> Default for ResourceSlot<T> {
860    fn default() -> Self {
861        Self::new()
862    }
863}
864
865// ─── TaskNode ────────────────────────────────────────────────────────────
866
867/// A node in the supervisor's task graph.
868///
869/// Designed to live in `static` memory: every field is `Sync`, all constructors
870/// are `const`. Declared by [`supervisor_graph!`], which emits one per managed
871/// task along with the [`Graph`] (`GRAPH`) that [`Supervisor::new`] consumes.
872pub struct TaskNode {
873    /// Human-readable name. Used in defmt logs and panic messages.
874    pub name: &'static str,
875    /// Lifecycle policy. See [`Mode`].
876    pub mode: Mode,
877    /// App-provided spawn function (typically an inline closure at the node's
878    /// declaration). Called once at boot from `Supervisor::start`, again from
879    /// `respawn_terminate` for Terminate nodes, and at runtime from `start_node`
880    /// for `OnDemand` nodes. `None` for a **parked** node the application spawns
881    /// itself (e.g. a `Pause` sensor holding a peripheral handle): the supervisor
882    /// tracks its lifecycle but never spawns it.
883    pub spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
884    /// The executor [`SpawnerSlot`] this node spawns through (`executor: NAME` in
885    /// the graph), or `None` to spawn on the supervisor's own `Spawner`. When
886    /// `Some`, the supervisor awaits the slot's [`ready`](SpawnerSlot::ready)
887    /// (bounded by [`SLOT_READY_TIMEOUT`]) *before* invoking `spawn`, so the
888    /// generated glue's own non-blocking `SpawnerSlot::get` is already filled. Set
889    /// by the macro via [`with_executor`](Self::with_executor); `const`, zero-cost.
890    spawn_slot: Option<&'static SpawnerSlot>,
891    /// The [`ResourceSlot`]s this node's spawn takes from (`resources:` in the
892    /// graph), type-erased to their [`ResourceGate`] readiness view. The
893    /// supervisor awaits every gate being filled (bounded by
894    /// [`SLOT_READY_TIMEOUT`]) *before* invoking `spawn`, so (a) a `main` that
895    /// provides late is tolerated and (b) a respawn cannot race the previous
896    /// instance's shell restoring the value (the restore happens after the
897    /// worker's shutdown ack). Empty for nodes without `resources:`. Set by the
898    /// macro via [`with_resources`](Self::with_resources); `const`, zero-cost.
899    resource_gates: &'static [&'static dyn ResourceGate],
900    /// Deps whose task-asserted readiness (`set_ready`) bring-up awaits before
901    /// spawning this node — the `ready`-marked subset of `deps:`. Spawn-order
902    /// deps stay in the graph's dep table; this is the readiness overlay.
903    #[cfg(feature = "readiness")]
904    ready_deps: &'static [&'static TaskNode],
905    /// Bound on the pre-spawn waits for this node's `executor:` slot and
906    /// `resources:` gates. Defaults to [`SLOT_READY_TIMEOUT`] (100 ms — sized
907    /// for "main provided before start"); raise it (`slot_timeout:` in the
908    /// graph) for a node whose slots are filled by a **provider node** at
909    /// runtime — e.g. an async radio bring-up worth hundreds of milliseconds.
910    /// Set by the macro via [`with_slot_timeout`](Self::with_slot_timeout).
911    slot_timeout: embassy_time::Duration,
912    handle: TaskHandle,
913}
914
915impl TaskNode {
916    /// A single-instance node started at boot (`Terminate`/`Pause`) or on demand
917    /// (`Mode::OnDemand`). Every node is single-instance; an elastic service is
918    /// modelled as several `OnDemand` nodes of the same pooled task fn.
919    ///
920    /// A `TaskNode` carries only its own identity and behaviour; the graph's
921    /// dependency edges live in the compile-time index table that
922    /// [`supervisor_graph!`] emits and [`Supervisor::new`] consumes.
923    /// `disabled_at_boot` seeds the node's disabled flag so a control-started node
924    /// (e.g. an OTA task) can be declared down and started later via a control op.
925    /// `spawn` is `None` for a parked node the application spawns itself.
926    pub const fn new(
927        name: &'static str,
928        mode: Mode,
929        spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
930        disabled_at_boot: bool,
931    ) -> Self {
932        Self {
933            name,
934            mode,
935            spawn,
936            spawn_slot: None,
937            resource_gates: &[],
938            #[cfg(feature = "readiness")]
939            ready_deps: &[],
940            slot_timeout: SLOT_READY_TIMEOUT,
941            handle: TaskHandle::new(disabled_at_boot),
942        }
943    }
944
945    /// Route this node's spawn through the given executor [`SpawnerSlot`] (the
946    /// `executor: NAME` graph annotation). The supervisor awaits the slot before
947    /// spawning the node, so a tier filled late — or from another core — is handled
948    /// without a race, and the generated glue's non-blocking `get` is already filled.
949    /// `const` and chainable in a `static` initializer; emitted by [`supervisor_graph!`].
950    pub const fn with_executor(mut self, slot: &'static SpawnerSlot) -> Self {
951        self.spawn_slot = Some(slot);
952        self
953    }
954
955    /// Declare the [`ResourceSlot`]s this node's spawn takes from (the
956    /// `resources:` graph clause). The supervisor awaits every gate being
957    /// filled before spawning the node, so the generated glue's non-blocking
958    /// `take()` finds the value. `const` and chainable in a `static`
959    /// initializer; emitted by [`supervisor_graph!`].
960    pub const fn with_resources(mut self, gates: &'static [&'static dyn ResourceGate]) -> Self {
961        self.resource_gates = gates;
962        self
963    }
964
965    /// Declare the deps whose task-asserted readiness bring-up awaits before
966    /// spawning this node (the `ready`-marked subset of `deps:`). `const` and
967    /// chainable in a `static` initializer; emitted by [`supervisor_graph!`].
968    #[cfg(feature = "readiness")]
969    pub const fn with_ready_deps(mut self, deps: &'static [&'static TaskNode]) -> Self {
970        self.ready_deps = deps;
971        self
972    }
973
974    /// Override the pre-spawn slot/gate wait bound for this node (the
975    /// `slot_timeout: <millis>` graph clause). The default
976    /// (`SLOT_READY_TIMEOUT`, 100 ms) assumes slots are provided *before*
977    /// `start()`; a node consuming a **provider node's** outputs must cover the
978    /// provider's async build time (the failure mode stays a loud
979    /// `SpawnError::Busy`, just later). `const` and chainable in a `static`
980    /// initializer; emitted by [`supervisor_graph!`].
981    pub const fn with_slot_timeout(mut self, timeout: embassy_time::Duration) -> Self {
982        self.slot_timeout = timeout;
983        self
984    }
985
986    // ── Task-side API ────────────────────────────────────────────────────
987    //
988    // Called from inside the `#[embassy_executor::task] async fn` body. The
989    // whole task-side protocol is four rules (the README's "Writing supervised
990    // tasks" section has per-mode skeletons):
991    //   1. select long-lived work against `wait_shutdown()`;
992    //   2. `ack_dropped()` exactly once per stop — on exit (Terminate/OnDemand)
993    //      or on each pause (Pause), before parking on `wait_resume()`;
994    //   3. an autonomous exit calls `mark_exited()` (acks + records completion;
995    //      `task:` shells do it automatically);
996    //   4. resources follow the mode: Terminate re-acquires on respawn, Pause
997    //      retains across park.
998
999    /// True iff the supervisor has requested shutdown. Checked at the loop top
1000    /// alongside `wait_shutdown()` in a `select`.
1001    pub fn shutdown_requested(&self) -> bool {
1002        self.handle.shutdown.load(Ordering::Acquire)
1003    }
1004
1005    /// Park until shutdown is requested. Returns immediately if shutdown has
1006    /// already been requested. Use this for single-instance tasks in a `select`
1007    /// against the task's main work future.
1008    pub async fn wait_shutdown(&self) {
1009        // Fast path — already requested. (Important because the signal is
1010        // edge-triggered: if `signal()` fired before we got here, the bare
1011        // `wait()` below would block forever.)
1012        if self.handle.shutdown.load(Ordering::Acquire) {
1013            return;
1014        }
1015        self.handle.shutdown_wake.wait().await;
1016    }
1017
1018    /// Mark this instance as having shut down: clears the running flag and acks
1019    /// the teardown handshake (so the supervisor's `wait_dropped` completes).
1020    /// Every instance must call this exactly once on exit (Terminate/OnDemand
1021    /// mode) or on each pause (Pause mode). It also covers an **autonomous** exit
1022    /// the supervisor didn't request — e.g. a pool worker backing off — so the
1023    /// pool sees the instance as down and can re-grow it under later demand.
1024    pub fn ack_dropped(&self) {
1025        self.handle.running.store(false, Ordering::Release);
1026        self.handle.dropped.store(true, Ordering::Release);
1027        self.handle.dropped_wake.signal(());
1028    }
1029
1030    /// Record that this node's task body has **returned**. Called automatically
1031    /// by the generated `task:` shell after the worker returns (and after
1032    /// resource restores); call it manually at the end of a hand-written
1033    /// `spawn:` task that can exit, where you would previously have called
1034    /// [`ack_dropped`](Self::ack_dropped) alone. Idempotent, and subsumes
1035    /// `ack_dropped`: it acks the teardown handshake *and* records completion,
1036    /// so a body that returns on its own — the case the supervisor previously
1037    /// could not observe — reads as down ([`is_running`](Self::is_running) →
1038    /// `false`, [`has_exited`](Self::has_exited) → `true`) instead of running
1039    /// forever, and a control `Activate` can respawn it.
1040    pub fn mark_exited(&self) {
1041        self.handle.completed.store(true, Ordering::Release);
1042        self.ack_dropped();
1043    }
1044
1045    /// True once the last instance's body returned — set by
1046    /// [`mark_exited`](Self::mark_exited), cleared by the pre-spawn reset.
1047    /// `has_exited() && !shutdown_requested()` distinguishes an autonomous
1048    /// completion from an acked stop (the shutdown flag persists until the next
1049    /// reset).
1050    pub fn has_exited(&self) -> bool {
1051        self.handle.completed.load(Ordering::Acquire)
1052    }
1053
1054    /// Assert readiness: "initialized and serving" (DHCP bound, registration
1055    /// done, calibration finished) — the task-side half of a `ready`-marked
1056    /// dependency edge. Distinct from *running* (spawned): `deps:` orders
1057    /// spawns; a `deps: [THIS ready]` edge additionally awaits this call.
1058    /// Latching until [`clear_ready`](Self::clear_ready) or the pre-spawn
1059    /// reset (a respawned provider re-asserts).
1060    #[cfg(feature = "readiness")]
1061    pub fn set_ready(&self) {
1062        self.handle.ready.store(true, Ordering::Release);
1063        self.handle.ready_wake.signal(());
1064    }
1065
1066    /// Withdraw readiness — **status, not control**: dependents are NOT stopped
1067    /// or notified (pair with a control `Deactivate` for a cascade); it defers
1068    /// future bring-up (a ready-marked dependent's spawn, pool growth) until
1069    /// [`set_ready`](Self::set_ready) again. Use for "link lost, still
1070    /// reconnecting" style states.
1071    #[cfg(feature = "readiness")]
1072    pub fn clear_ready(&self) {
1073        self.handle.ready.store(false, Ordering::Release);
1074    }
1075
1076    /// True while the node asserts readiness. Pool growth checks this for
1077    /// `ready`-marked deps; also useful in app health views.
1078    #[cfg(feature = "readiness")]
1079    pub fn is_ready(&self) -> bool {
1080        self.handle.ready.load(Ordering::Acquire)
1081    }
1082
1083    /// Park until this node asserts readiness (immediately if it already has).
1084    /// The supervisor's bring-up is the intended pre-fill waiter; the latching
1085    /// signal has the same single-pre-fill-waiter caveat as
1086    /// [`SpawnerSlot::ready`] — for N concurrent app-side waiters fan out
1087    /// through an app-owned `embassy_sync::watch::Watch` fed by the ready task.
1088    #[cfg(feature = "readiness")]
1089    pub async fn wait_ready(&self) {
1090        loop {
1091            if self.is_ready() {
1092                return;
1093            }
1094            self.handle.ready_wake.wait().await;
1095        }
1096    }
1097
1098    /// True when every `ready`-marked dep currently asserts readiness — the
1099    /// sync form pool growth uses (no wait: a not-ready dep just defers the
1100    /// grow to the next evaluation).
1101    #[cfg(all(feature = "pool", feature = "readiness"))]
1102    pub(crate) fn ready_deps_ok(&self) -> bool {
1103        self.ready_deps.iter().all(|d| d.is_ready())
1104    }
1105    #[cfg(all(feature = "pool", not(feature = "readiness")))]
1106    pub(crate) fn ready_deps_ok(&self) -> bool {
1107        true
1108    }
1109
1110    /// Record a liveness heartbeat. Call once per work loop (or per served
1111    /// request); an app watchdog task reads [`is_stale`](Self::is_stale).
1112    #[cfg(feature = "liveness")]
1113    pub fn beat(&self) {
1114        self.handle.last_beat.store(
1115            embassy_time::Instant::now().as_ticks() as u32,
1116            Ordering::Release,
1117        );
1118    }
1119
1120    /// Ticks since the last [`beat`](Self::beat) (wrapping arithmetic; correct
1121    /// for gaps under the u32 tick wrap, ~71 min at 1 MHz — far above any sane
1122    /// `max_age`).
1123    #[cfg(feature = "liveness")]
1124    pub fn ticks_since_beat(&self) -> u32 {
1125        (embassy_time::Instant::now().as_ticks() as u32)
1126            .wrapping_sub(self.handle.last_beat.load(Ordering::Acquire))
1127    }
1128
1129    /// True when the node is running but hasn't beaten within `max_age` — the
1130    /// alive-but-wedged detector (a task hogging nothing, parked on an await
1131    /// that will never complete). Not-running nodes are never stale: a stopped
1132    /// or completed node is *down*, which `is_running`/`has_exited` already
1133    /// report. Complements the `trace` stall watermark, which catches the
1134    /// opposite failure (a poll that never yields).
1135    #[cfg(feature = "liveness")]
1136    pub fn is_stale(&self, max_age: embassy_time::Duration) -> bool {
1137        self.is_running() && u64::from(self.ticks_since_beat()) > max_age.as_ticks()
1138    }
1139
1140    /// Pause-mode only: park until the supervisor signals resume. Call *after*
1141    /// [`ack_dropped`](Self::ack_dropped) — ack the pause, then park; held
1142    /// resources stay owned across the park.
1143    pub async fn wait_resume(&self) {
1144        self.handle.resume_wake.wait().await;
1145    }
1146
1147    /// Race `fut` against this node's shutdown: `Ok(output)` when the work
1148    /// completes, `Err(Aborted)` when a stop/pause request wins. Owns the
1149    /// `select` that rule 1 of the task protocol otherwise has you write by
1150    /// hand. Does **not** ack — run your cleanup, then call
1151    /// [`ack_dropped`](Self::ack_dropped) (or return through
1152    /// [`run_cancellable_acked`](Self::run_cancellable_acked) when there is no
1153    /// cleanup between the select and the ack).
1154    ///
1155    /// ```ignore
1156    /// match node.run_cancellable(conn.serve()).await {
1157    ///     Ok(done) => handle(done),
1158    ///     Err(Aborted) => { flush().await; node.ack_dropped(); return; }
1159    /// }
1160    /// ```
1161    ///
1162    /// Returns a future rather than being an `async fn` on purpose: an `async fn`
1163    /// keeps `fut` in its own frame *and* in the `select` that lives across the
1164    /// await, and rustc does not overlap the two slots
1165    /// (rust-lang/rust#62958) — so the worker's state machine would be reserved
1166    /// twice in the caller's static storage. The hand-written future below holds
1167    /// it exactly once.
1168    pub fn run_cancellable<F: Future>(
1169        &self,
1170        fut: F,
1171    ) -> impl Future<Output = Result<F::Output, Aborted>> {
1172        RunCancellable {
1173            fut: Some(fut),
1174            shutdown: self.wait_shutdown(),
1175            ack: None,
1176        }
1177    }
1178
1179    /// [`run_cancellable`](Self::run_cancellable) that additionally calls
1180    /// [`ack_dropped`](Self::ack_dropped) before returning `Err(Aborted)` — for
1181    /// bodies with no teardown work between the select and the ack, e.g. a
1182    /// runner whose drop *is* the cleanup:
1183    ///
1184    /// ```ignore
1185    /// let _ = node.run_cancellable_acked(runner.run()).await; // drop releases the pins
1186    /// ```
1187    /// Single-copy for the same reason as
1188    /// [`run_cancellable`](Self::run_cancellable), and it keeps that method's
1189    /// ordering: on abort the worker future is dropped **before** the ack, so a
1190    /// runner whose `Drop` is the cleanup has released everything by the time the
1191    /// supervisor observes the handshake.
1192    pub fn run_cancellable_acked<F: Future>(
1193        &self,
1194        fut: F,
1195    ) -> impl Future<Output = Result<F::Output, Aborted>> {
1196        RunCancellable {
1197            fut: Some(fut),
1198            shutdown: self.wait_shutdown(),
1199            ack: Some(self),
1200        }
1201    }
1202
1203    /// Report that this task started serving a request (active). Fires the
1204    /// scale-request signal on a real idle→busy transition so the scaling policy
1205    /// can react (e.g. grow the pool); a redundant call doesn't re-signal.
1206    pub fn mark_busy(&self) {
1207        if !self.handle.busy.swap(true, Ordering::Release) {
1208            request_scale();
1209        }
1210    }
1211
1212    /// Report that this task finished serving and is idle again. Fires the
1213    /// scale-request signal on a real busy→idle transition so the scaling policy
1214    /// can react (e.g. shrink the pool); a redundant call doesn't re-signal.
1215    pub fn mark_idle(&self) {
1216        if self.handle.busy.swap(false, Ordering::Release) {
1217            request_scale();
1218        }
1219    }
1220
1221    /// True while this task is actively serving. Read by the scaling policy.
1222    pub fn is_busy(&self) -> bool {
1223        self.handle.busy.load(Ordering::Acquire)
1224    }
1225
1226    /// True while the supervisor has this node spawned (and it hasn't exited).
1227    /// Read by the scaling policy to count live instances, and by a task-state
1228    /// view.
1229    pub fn is_running(&self) -> bool {
1230        self.handle.running.load(Ordering::Acquire)
1231    }
1232
1233    /// True while the node is disabled: declared `disabled` in the graph
1234    /// (stopped-at-boot, up on an explicit `Activate`), or manually deactivated
1235    /// via the control interface and not yet re-activated. Read by a task-state
1236    /// view and by the automatic bring-up paths (which skip a disabled node).
1237    pub fn is_disabled(&self) -> bool {
1238        self.handle.disabled.load(Ordering::Acquire)
1239    }
1240
1241    /// Mark/clear this node as **detached**: a self-managing node the supervisor
1242    /// brings up once (via [`start`](Supervisor::start)) and then stops managing
1243    /// **entirely**. Every runtime lifecycle operation skips a detached node: full
1244    /// [`teardown`](Supervisor::teardown), the control deactivate/activate cascades,
1245    /// [`stop_node`](Supervisor::stop_node), [`respawn_terminate`](Supervisor::respawn_terminate),
1246    /// and pause-resume. It keeps running (or, for a one-shot, stays exited) across a
1247    /// teardown/wake cycle instead of being stopped, re-enabled, or re-spawned. Use it
1248    /// for a task that must outlive the teardown it participates in — e.g. a sleep/power
1249    /// coordinator that tears the graph down, sleeps, then wakes it — or a self-managed
1250    /// one-shot whose `deps:` exist only for start-ordering. The node owns its own
1251    /// shutdown; the supervisor will not drive it.
1252    pub fn set_detached(&self, detached: bool) {
1253        self.handle.detached.store(detached, Ordering::Release);
1254    }
1255
1256    /// True while this node is [detached](Self::set_detached): self-managed, skipped by
1257    /// every runtime lifecycle operation (teardown, deactivate/activate, `stop_node`,
1258    /// respawn, pause-resume). Only the initial `start` brings it up.
1259    pub fn is_detached(&self) -> bool {
1260        self.handle.detached.load(Ordering::Acquire)
1261    }
1262
1263    // ── Trace/observability API (features `trace`/`trace-names`) ───────────
1264
1265    /// Record the executor task id (`SpawnToken::id()` / `TaskRef::id()`) currently
1266    /// backing this node, so the [`trace`] recorders can attribute executor polls to
1267    /// it. Called automatically by the spawn glue `supervisor_graph!` generates;
1268    /// call it manually only for a **parked** node (no `spawn:`) or a verbatim-closure
1269    /// `spawn:`, where the macro cannot see the token. Overwrites on every (re)spawn.
1270    #[cfg(feature = "trace")]
1271    pub fn set_task_id(&self, id: u32) {
1272        self.handle.task_id.store(id, Ordering::Release);
1273    }
1274
1275    /// Register an externally-spawned token as this node's live task: records
1276    /// the task id for the [`trace`] recorders and (feature `metadata-names`)
1277    /// stamps the node name into the task Metadata. One call replaces the
1278    /// manual [`set_task_id`](Self::set_task_id) dance wherever the macro can't
1279    /// see the token — parked nodes and verbatim-closure `spawn:` forms:
1280    ///
1281    /// ```ignore
1282    /// let t = environment_task(i2c_dev)?;
1283    /// BME280.adopt(&t);
1284    /// high_spawner.spawn(t);
1285    /// ```
1286    #[cfg(feature = "trace")]
1287    pub fn adopt<S>(&self, token: &embassy_executor::SpawnToken<S>) {
1288        self.set_task_id(token.id());
1289        #[cfg(feature = "metadata-names")]
1290        self.stamp_name(token);
1291    }
1292
1293    /// Stamp this node's name into the task's embassy `Metadata` (feature
1294    /// `metadata-names`), so external consumers — rtos-trace/SystemView, debuggers —
1295    /// show the graph node name instead of an opaque task id. Unlike
1296    /// [`adopt`](Self::adopt) this does **not** capture the task id or touch the
1297    /// supervisor's [`trace`] recorders, so it needs neither the `trace` feature nor
1298    /// the `_embassy_trace_*` hook symbols: it is the name-only spawn path emitted
1299    /// when `metadata-names` is on but `trace` is off (pair it with embassy's
1300    /// `rtos-trace`). Called automatically by the spawn glue; call it manually only
1301    /// for a parked or verbatim-closure node the macro can't see.
1302    ///
1303    /// Requires `embassy-executor`'s `metadata-name` feature, which `metadata-names`
1304    /// pulls in; without a registered name the task keeps embassy's default.
1305    #[cfg(feature = "metadata-names")]
1306    pub fn stamp_name<S>(&self, token: &embassy_executor::SpawnToken<S>) {
1307        token.metadata().set_name(self.name);
1308    }
1309
1310    /// The executor task id last recorded by [`set_task_id`](Self::set_task_id)
1311    /// (`0` = never spawned / not registered).
1312    #[cfg(feature = "trace")]
1313    pub fn task_id(&self) -> u32 {
1314        self.handle.task_id.load(Ordering::Acquire)
1315    }
1316
1317    /// Accumulated executor-poll time of this node, in embassy-time ticks. Wrapping:
1318    /// sample twice and `wrapping_sub` the readings to get a rate over a window.
1319    #[cfg(feature = "trace")]
1320    pub fn exec_ticks(&self) -> u32 {
1321        self.handle.exec_ticks.load(Ordering::Relaxed)
1322    }
1323
1324    /// Number of executor polls of this node (wrapping counter).
1325    #[cfg(feature = "trace")]
1326    pub fn poll_count(&self) -> u32 {
1327        self.handle.polls.load(Ordering::Relaxed)
1328    }
1329
1330    /// Longest single executor poll of this node ever observed, in ticks — the
1331    /// "never yields" watermark. A poll is expected to be microseconds; a large
1332    /// value names the node that hogged its executor, even after the fact.
1333    #[cfg(feature = "trace")]
1334    pub fn max_poll_ticks(&self) -> u32 {
1335        self.handle.max_poll_ticks.load(Ordering::Relaxed)
1336    }
1337
1338    // ── Supervisor-side API ──────────────────────────────────────────────
1339    //
1340    // Driven by the `Supervisor` struct. Kept `pub(crate)` so app code doesn't
1341    // accidentally bypass the supervisor's orchestration.
1342
1343    pub(crate) fn signal_shutdown(&self) {
1344        self.handle.shutdown.store(true, Ordering::Release);
1345        self.handle.shutdown_wake.signal(());
1346    }
1347
1348    pub(crate) fn signal_resume(&self) {
1349        self.handle.resume_wake.signal(());
1350    }
1351
1352    pub(crate) fn set_running(&self, running: bool) {
1353        self.handle.running.store(running, Ordering::Release);
1354        // Stamp a beat at spawn so a freshly running node is never instantly
1355        // stale (its body may not reach its first beat() for a while).
1356        #[cfg(feature = "liveness")]
1357        if running {
1358            self.handle.last_beat.store(
1359                embassy_time::Instant::now().as_ticks() as u32,
1360                Ordering::Release,
1361            );
1362        }
1363    }
1364
1365    /// Set/clear the manual-deactivation flag. Set by `Supervisor::deactivate`,
1366    /// cleared by `Supervisor::activate`. Deliberately *not* touched by
1367    /// `reset()`, so a manual stop survives respawn cycles and RAM-retaining
1368    /// power-state transitions.
1369    ///
1370    /// Public so an application can pre-disable a `Terminate` node *before*
1371    /// `Supervisor::start`, making it a stopped-at-boot task that only comes up on
1372    /// an explicit `Activate` control (a node started by control rather than at boot).
1373    pub fn set_disabled(&self, disabled: bool) {
1374        self.handle.disabled.store(disabled, Ordering::Release);
1375    }
1376
1377    /// Wait until the instance has called `ack_dropped()`. Single-instance, so
1378    /// one ack ends the wait. The fast-path flag check handles the ack landing
1379    /// before this await (the `dropped_wake` signal is edge-triggered).
1380    /// True when an instance acked a stop WITHOUT exiting — for a `Pause` node
1381    /// that is exactly "parked on `wait_resume()`" (the protocol acks, then
1382    /// parks; a full exit would have set `completed` via `mark_exited`).
1383    /// Readable only before the pre-spawn `reset()` clears both flags.
1384    pub(crate) fn has_acked_stop(&self) -> bool {
1385        self.handle.dropped.load(Ordering::Acquire)
1386            && !self.handle.completed.load(Ordering::Acquire)
1387    }
1388
1389    pub(crate) async fn wait_dropped(&self) {
1390        if self.handle.dropped.load(Ordering::Acquire) {
1391            return;
1392        }
1393        self.handle.dropped_wake.wait().await;
1394    }
1395
1396    /// Clear the shutdown flag, dropped flag, busy flag, completed flag, and the
1397    /// shutdown / dropped wake-signals so the next cycle starts clean. Doesn't
1398    /// touch `running` (managed around spawn/stop), `resume_wake`
1399    /// (`resume_pausable` fires that for Pause nodes), or `disabled`
1400    /// (lifecycle-spanning).
1401    pub(crate) fn reset(&self) {
1402        self.handle.shutdown.store(false, Ordering::Release);
1403        self.handle.dropped.store(false, Ordering::Release);
1404        self.handle.busy.store(false, Ordering::Release);
1405        self.handle.completed.store(false, Ordering::Release);
1406        // A respawned provider must re-assert readiness for its new instance.
1407        #[cfg(feature = "readiness")]
1408        {
1409            self.handle.ready.store(false, Ordering::Release);
1410            self.handle.ready_wake.reset();
1411        }
1412        self.handle.shutdown_wake.reset();
1413        self.handle.dropped_wake.reset();
1414    }
1415}
1416
1417/// Manual impl: the private `TaskHandle` (Signals + atomics) has no `Debug`, and a
1418/// snapshot of the *live* flags is more useful than raw handle internals anyway.
1419/// `finish_non_exhaustive` marks the elided fields (`spawn`, the handle).
1420impl core::fmt::Debug for TaskNode {
1421    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1422        f.debug_struct("TaskNode")
1423            .field("name", &self.name)
1424            .field("mode", &self.mode)
1425            .field("running", &self.is_running())
1426            .field("busy", &self.is_busy())
1427            .field("disabled", &self.is_disabled())
1428            .field("detached", &self.is_detached())
1429            .finish_non_exhaustive()
1430    }
1431}
1432
1433// ─── Graph ───────────────────────────────────────────────────────────────
1434
1435/// The compile-time task graph produced by [`supervisor_graph!`]: the node slots,
1436/// the dependency-index table, the topological order, and the elastic pools — the
1437/// single value [`Supervisor::new`] consumes. The macro emits one `pub static GRAPH`
1438/// of this type. The fields are public so the application can read them directly
1439/// (e.g. a status endpoint iterating `GRAPH.nodes` / `GRAPH.deps`).
1440///
1441/// `N` is capped at 256 (graph indices are `u8`); the macro enforces this at
1442/// expansion time.
1443pub struct Graph<const N: usize> {
1444    /// Node slots, one per declared node. `None` marks a `#[cfg]`-ed-out node.
1445    pub nodes: &'static [Option<&'static TaskNode>; N],
1446    /// Per-node dependency indices into `nodes` (`deps[i]` lists node `i`'s deps).
1447    pub deps: &'static [&'static [u8]; N],
1448    /// Topologically sorted indices into `nodes` (dependencies before dependents;
1449    /// reverse iteration is the teardown order). A dependency cycle is a compile error.
1450    pub order: [u8; N],
1451    /// Elastic worker pools to register with the supervisor (empty when unused).
1452    #[cfg(feature = "pool")]
1453    pub pools: &'static [&'static dyn Pool],
1454}
1455
1456// ─── Supervisor ──────────────────────────────────────────────────────────
1457
1458/// Orchestrates a set of managed tasks across spawn / teardown / bring-up.
1459///
1460/// Owned by a single supervisor task. Concurrent access from other tasks goes
1461/// through each [`TaskNode`]'s own atomic state, not the `Supervisor` struct.
1462pub struct Supervisor<const N: usize> {
1463    /// Node slots, one per declared node. `None` marks a slot whose node was
1464    /// `#[cfg]`-ed out of the build (feature-gated); every method skips those.
1465    nodes: &'static [Option<&'static TaskNode>],
1466    /// Per-node dependency indices into `nodes` (`deps[i]` lists the indices of
1467    /// the nodes that node `i` depends on). The single runtime source of graph
1468    /// topology, generated alongside `order` by the `supervisor_graph!` macro.
1469    #[cfg(any(feature = "control", feature = "pool"))]
1470    deps: &'static [&'static [u8]],
1471    /// Topologically sorted indices into `nodes`: dependencies before their
1472    /// dependents; reverse iteration is the teardown order. Precomputed at
1473    /// compile time (a cycle is a compile error), so construction does no work.
1474    /// Borrowed from the `static` [`Graph`] rather than copied: a `Supervisor`
1475    /// usually lives inside a task future (i.e. in that task's `static`
1476    /// storage), so an inline `[u8; N]` would cost N bytes of RAM per
1477    /// supervisor plus the copy code for no benefit.
1478    order: &'static [u8; N],
1479    /// Elastic pools, so the control interface can co-control a whole pool from
1480    /// any one member (`apply_control` expands the target through
1481    /// [`Pool::members`]) — the same registry `run_pools` drives. Taken from
1482    /// `GRAPH.pools` at construction (empty when no pool is declared).
1483    #[cfg(feature = "pool")]
1484    pools: &'static [&'static dyn Pool],
1485}
1486
1487/// Await a node's `executor:` [`SpawnerSlot`] (if it has one), bounded by the
1488/// node's [`slot_timeout`](TaskNode::with_slot_timeout) (default
1489/// [`SLOT_READY_TIMEOUT`]). A slot still empty after the wait yields
1490/// [`SpawnError::Busy`] — a loud misconfiguration, not a silent hang. A node with no
1491/// slot returns immediately, so a same-executor bring-up never touches the timer.
1492async fn await_spawn_slot(node: &'static TaskNode) -> Result<(), SpawnError> {
1493    if let Some(slot) = node.spawn_slot {
1494        with_timeout(node.slot_timeout, slot.ready())
1495            .await
1496            .map_err(|_| SpawnError::Busy)?;
1497    }
1498    Ok(())
1499}
1500
1501/// Await every [`ResourceSlot`] a node's `resources:` clause takes from being
1502/// filled, bounded by the node's
1503/// [`slot_timeout`](TaskNode::with_slot_timeout) (default
1504/// [`SLOT_READY_TIMEOUT`]) per gate. Covers three windows: `main` providing
1505/// after `start` was entered; — on respawn — the previous instance's shell
1506/// still between the shutdown ack and its `restore()` call (on another core
1507/// the two can genuinely overlap); and a **provider node** still building the
1508/// values this node consumes (size `slot_timeout:` to the build time). A gate
1509/// still empty at the deadline yields [`SpawnError::Busy`] — an unprovided
1510/// slot is a loud misconfiguration, not a silent hang. Nodes without
1511/// `resources:` have an empty gate list and never touch the timer. Same
1512/// check-then-park loop as [`SpawnerSlot::ready`]; the `filled` signal
1513/// latches, so a fill racing the check still wakes the wait (and the same
1514/// single-pre-fill-waiter caveat applies — the supervisor task is the only
1515/// intended waiter).
1516async fn await_resources(node: &'static TaskNode) -> Result<(), SpawnError> {
1517    for gate in node.resource_gates {
1518        let wait = async {
1519            loop {
1520                if gate.is_filled() {
1521                    break;
1522                }
1523                gate.filled_signal().wait().await;
1524            }
1525        };
1526        with_timeout(node.slot_timeout, wait)
1527            .await
1528            .map_err(|_| SpawnError::Busy)?;
1529    }
1530    Ok(())
1531}
1532
1533/// Await every `ready`-marked dep's task-asserted readiness before spawning
1534/// `node`, each bounded by the node's `slot_timeout` (same budget as its
1535/// resource gates — both are "my inputs aren't there yet"). Timeout maps to
1536/// `SpawnError::Busy` like the other pre-spawn gates; the log line names the
1537/// dep so a readiness timeout is distinguishable from a slot timeout.
1538#[cfg(feature = "readiness")]
1539async fn await_ready_deps(node: &'static TaskNode) -> Result<(), SpawnError> {
1540    for dep in node.ready_deps {
1541        if with_timeout(node.slot_timeout, dep.wait_ready())
1542            .await
1543            .is_err()
1544        {
1545            warn!(
1546                "supervisor: ready-dep {} not ready within {}ms (spawning {})",
1547                dep.name,
1548                node.slot_timeout.as_millis(),
1549                node.name,
1550            );
1551            return Err(SpawnError::Busy);
1552        }
1553    }
1554    Ok(())
1555}
1556#[cfg(not(feature = "readiness"))]
1557async fn await_ready_deps(_node: &'static TaskNode) -> Result<(), SpawnError> {
1558    Ok(())
1559}
1560
1561impl<const N: usize> Supervisor<N> {
1562    /// Build a supervisor from a precomputed [`Graph`] — the `GRAPH` that
1563    /// `supervisor_graph!` emits (node slots, dependency-index table, compile-time
1564    /// topological `order`, and the elastic pools). A dependency cycle is a
1565    /// *compile* error, so construction is infallible and does no work —
1566    /// `start` / `teardown` / `respawn_terminate` just iterate.
1567    pub const fn new(graph: &'static Graph<N>) -> Self {
1568        Self {
1569            nodes: graph.nodes,
1570            #[cfg(any(feature = "control", feature = "pool"))]
1571            deps: graph.deps,
1572            order: &graph.order,
1573            #[cfg(feature = "pool")]
1574            pools: graph.pools,
1575        }
1576    }
1577
1578    /// Bring the graph from any quiescent state to running, in dependency
1579    /// order — cold boot AND re-entry (a sub-graph supervisor is legitimately
1580    /// `start()`/`teardown()`-cycled per app phase). Idempotent: running nodes
1581    /// are skipped; detached nodes are skipped on re-entry (their instance
1582    /// survived the teardown — the first start still spawns them, the flag is
1583    /// app-set afterwards); a `Pause` instance parked by an earlier teardown is
1584    /// **resumed in place** (never double-spawned; like
1585    /// [`resume_pausable`](Self::resume_pausable) this bypasses the gate waits,
1586    /// since the parked instance retains its resources and its slots are empty
1587    /// by design). `Mode::OnDemand` nodes are skipped — they're brought up at
1588    /// runtime by `start_node`. A **parked** node (no `spawn` fn) is spawned
1589    /// externally by `main()` (with hardware handles main owns); it's still
1590    /// marked `running` here. Disabled nodes, and `#[cfg]`-ed-out slots, are
1591    /// skipped.
1592    ///
1593    /// Async because an `executor: NAME` node first awaits its [`SpawnerSlot::ready`]
1594    /// (bounded by `SLOT_READY_TIMEOUT` — the rendezvous with a tier or second core
1595    /// that comes up asynchronously); a slot still empty at the deadline fails the
1596    /// bring-up with [`SpawnError::Busy`]. A node with no `executor:` slot never
1597    /// touches the timer.
1598    pub async fn start(&self, spawner: Spawner) -> Result<(), SpawnError> {
1599        // Register the node slots with the trace recorders.
1600        #[cfg(feature = "trace")]
1601        trace::register_graph(self.nodes);
1602
1603        for i in self.order.iter() {
1604            let Some(node) = self.nodes[*i as usize] else {
1605                continue;
1606            };
1607            if matches!(node.mode, Mode::OnDemand) || node.is_disabled() {
1608                continue;
1609            }
1610            // Re-entry guards, making start() the universal quiescent-to-running
1611            // op (cold boot, post-teardown cycle, partial states) — all three
1612            // are no-ops on a cold boot:
1613            // * already running -> skip (idempotent; trustworthy because a
1614            //   cleanly returned body clears `running` via mark_exited);
1615            // * detached -> skip (its instance survived the teardown that
1616            //   preceded this start; spawning again would double-spawn — the
1617            //   flag is app-set at runtime, so first-start still spawns it);
1618            // * a Pause instance parked by an earlier teardown -> resume it in
1619            //   place below, never spawn a second one.
1620            if node.is_running() || node.is_detached() {
1621                continue;
1622            }
1623            if matches!(node.mode, Mode::Pause) && node.has_acked_stop() {
1624                // Same sequence as resume_pausable, and like it deliberately
1625                // WITHOUT the spawn path's gate waits: the parked instance
1626                // retains its resources, so its slots are empty by design and
1627                // await_resources would time out Busy.
1628                node.reset();
1629                info!("supervisor: resuming {} in place", node.name);
1630                node.signal_resume();
1631                node.set_running(true);
1632                continue;
1633            }
1634            // Clean handle per cycle (like start_node): a sub-graph supervisor
1635            // is legitimately start()/teardown()-cycled per app phase, and the
1636            // teardown latches the shutdown flag — without this reset a second
1637            // start()'s workers would observe it instantly. No-op at boot.
1638            node.reset();
1639            info!("supervisor: spawning {} ({})", node.name, node.mode);
1640            if let Some(spawn) = node.spawn {
1641                // For an `executor:` node, wait (bounded) for its slot to be filled
1642                // before spawning; a same-executor node has no slot, so this is an
1643                // immediate no-op and the bring-up loop stays tight. Then wait for
1644                // the node's `resources:` slots (if any) so the glue's take() finds
1645                // the value even if main provides late.
1646                await_spawn_slot(node).await?;
1647                await_resources(node).await?;
1648                await_ready_deps(node).await?;
1649                spawn(spawner)?;
1650            }
1651            node.set_running(true);
1652        }
1653        Ok(())
1654    }
1655
1656    /// The canonical driver, as one call: [`start`](Self::start) the graph,
1657    /// then drive elastic-pool scaling and/or runtime control forever. Returns
1658    /// **only on error** — every arm is an app-level escalation (typically
1659    /// `panic!` into a hardware-watchdog reset):
1660    ///
1661    /// ```ignore
1662    /// match sup.run(spawner).await {
1663    ///     RunError::Spawn(_) => defmt::panic!("supervisor: bring-up failed"),
1664    ///     RunError::Shutdown(e) => defmt::panic!("supervisor: {} missed ack", e.node.name),
1665    /// }
1666    /// ```
1667    ///
1668    /// Apps that select extra wake sources into the driver loop (their own
1669    /// signals, a wake timer) keep writing the loop by hand:
1670    /// `select(sup.run_pools(spawner), wait_control())` + `apply_control`.
1671    #[cfg(any(feature = "pool", feature = "control"))]
1672    pub async fn run(&self, spawner: Spawner) -> RunError {
1673        if let Err(e) = self.start(spawner).await {
1674            return RunError::Spawn(e);
1675        }
1676        #[cfg(all(feature = "pool", feature = "control"))]
1677        loop {
1678            match select(self.run_pools(spawner), wait_control()).await {
1679                Either::First(e) => return RunError::Shutdown(e),
1680                Either::Second(cmd) => {
1681                    if let Err(e) = self.apply_control(cmd, spawner).await {
1682                        return RunError::Shutdown(e);
1683                    }
1684                }
1685            }
1686        }
1687        #[cfg(all(feature = "pool", not(feature = "control")))]
1688        return RunError::Shutdown(self.run_pools(spawner).await);
1689        #[cfg(all(feature = "control", not(feature = "pool")))]
1690        loop {
1691            let cmd = wait_control().await;
1692            if let Err(e) = self.apply_control(cmd, spawner).await {
1693                return RunError::Shutdown(e);
1694            }
1695        }
1696    }
1697
1698    /// Start a single node at runtime — e.g. growing an elastic pool. Resets the
1699    /// handle, spawns one instance via the node's `spawn` fn (which must launch
1700    /// exactly one), and marks it `running`. Returns `SpawnError::Busy` if the
1701    /// underlying embassy task pool is exhausted (the ceiling), which the caller
1702    /// treats as "can't grow".
1703    pub async fn start_node(
1704        &self,
1705        node: &'static TaskNode,
1706        spawner: Spawner,
1707    ) -> Result<(), SpawnError> {
1708        node.reset();
1709        if let Some(spawn) = node.spawn {
1710            await_spawn_slot(node).await?;
1711            await_resources(node).await?;
1712            await_ready_deps(node).await?;
1713            spawn(spawner)?;
1714        }
1715        node.set_running(true);
1716        info!("supervisor: started {}", node.name);
1717        Ok(())
1718    }
1719
1720    /// Signal `node` to shut down, wait for its ack, then clear `running`.
1721    /// A missed ack (a missing `ack_dropped()`/`mark_exited()` somewhere, or a
1722    /// wedged task) is returned as [`ShutdownTimeout`] — the node keeps running
1723    /// and the caller decides the escalation. Shared by `stop_node` and
1724    /// `teardown`; the caller must have checked `is_running`.
1725    async fn shutdown_and_wait(&self, node: &'static TaskNode) -> Result<(), ShutdownTimeout> {
1726        node.signal_shutdown();
1727        if let Either::Second(()) = select(
1728            node.wait_dropped(),
1729            Timer::after_millis(SHUTDOWN_ACK_TIMEOUT_MS),
1730        )
1731        .await
1732        {
1733            warn!(
1734                "supervisor: task {} did not ack shutdown within {}ms",
1735                node.name, SHUTDOWN_ACK_TIMEOUT_MS,
1736            );
1737            return Err(ShutdownTimeout { node });
1738        }
1739        node.set_running(false);
1740        Ok(())
1741    }
1742
1743    /// Stop a single running node at runtime — e.g. shrinking an elastic pool.
1744    /// For a `Pause` node this IS the single-node "pause": the worker acks and
1745    /// parks on `wait_resume()`, and [`resume_node`](Self::resume_node) is the
1746    /// symmetric other half. Signals shutdown, waits for the ack, clears
1747    /// `running`. No-op `Ok` if the node isn't running, or is
1748    /// [detached](TaskNode::set_detached) (self-managed — the supervisor never
1749    /// stops it). A node that misses the ack window is returned as
1750    /// [`ShutdownTimeout`] and stays marked running.
1751    pub async fn stop_node(&self, node: &'static TaskNode) -> Result<(), ShutdownTimeout> {
1752        if !node.is_running() || node.is_detached() {
1753            return Ok(());
1754        }
1755        self.shutdown_and_wait(node).await?;
1756        info!("supervisor: stopped {}", node.name);
1757        Ok(())
1758    }
1759
1760    /// Signal every **running** node to shut down in **reverse** topological
1761    /// order, awaiting each node's ack before moving to its dependency. Down
1762    /// `OnDemand` nodes are skipped (no instance to ack). Pause-mode nodes ack
1763    /// and park on `wait_resume()`; Terminate/OnDemand nodes exit.
1764    ///
1765    /// **Aborts on the first missed ack**, returning the offending node as
1766    /// [`ShutdownTimeout`]: continuing would stop dependencies out from under a
1767    /// still-live dependent. After `Err` the graph is partially down — the sane
1768    /// escalations are app-level (hardware watchdog reset, `panic!`, retry, or
1769    /// [`teardown_continue`](Self::teardown_continue) when quiescing the rest
1770    /// still matters before a reset).
1771    pub async fn teardown(&self) -> Result<(), ShutdownTimeout> {
1772        for i in self.order.iter().rev() {
1773            let Some(node) = self.nodes[*i as usize] else {
1774                continue;
1775            };
1776            if !node.is_running() {
1777                continue;
1778            }
1779            // A detached node is self-managed; never tear it down. See
1780            // [`TaskNode::set_detached`].
1781            if node.is_detached() {
1782                continue;
1783            }
1784            info!("supervisor: tearing down {}", node.name);
1785            self.shutdown_and_wait(node).await?;
1786        }
1787        Ok(())
1788    }
1789
1790    /// Best-effort variant of [`teardown`](Self::teardown) for the
1791    /// "hardware reset next" escalation path: presses on past a non-acking node
1792    /// (still in reverse topological order) so the remaining nodes get their
1793    /// chance to flush and park, and returns the **first** timeout after
1794    /// visiting every node. The wedged node's dependencies are stopped under it
1795    /// — acceptable only because the caller is about to reset anyway.
1796    pub async fn teardown_continue(&self) -> Result<(), ShutdownTimeout> {
1797        let mut first_err = Ok(());
1798        for i in self.order.iter().rev() {
1799            let Some(node) = self.nodes[*i as usize] else {
1800                continue;
1801            };
1802            if !node.is_running() || node.is_detached() {
1803                continue;
1804            }
1805            info!("supervisor: tearing down {}", node.name);
1806            if let Err(e) = self.shutdown_and_wait(node).await
1807                && first_err.is_ok()
1808            {
1809                first_err = Err(e);
1810            }
1811        }
1812        first_err
1813    }
1814
1815    /// Resume ONE `Pause` node parked by an earlier [`stop_node`](Self::stop_node)
1816    /// or [`teardown`](Self::teardown) — the single-node partner of
1817    /// [`resume_pausable`](Self::resume_pausable), same sequence and the same
1818    /// deliberate absence of dependency gating (the parked instance retains its
1819    /// resources). Cheap and synchronous. No-op unless the node is `Pause`
1820    /// mode, actually parked (an instance acked without exiting), and neither
1821    /// [disabled](TaskNode::is_disabled) (a control pause sticks — clear it
1822    /// with [`activate`](Self::activate)) nor
1823    /// [detached](TaskNode::set_detached).
1824    pub fn resume_node(&self, node: &'static TaskNode) {
1825        if !matches!(node.mode, Mode::Pause)
1826            || node.is_disabled()
1827            || node.is_detached()
1828            || !node.has_acked_stop()
1829        {
1830            return;
1831        }
1832        node.reset();
1833        info!("supervisor: resuming {}", node.name);
1834        node.signal_resume();
1835        node.set_running(true);
1836    }
1837
1838    /// Signal every Pause-mode node to resume. Cheap and synchronous — the tasks
1839    /// were parked on `wait_resume()` and pick up immediately. Called separately
1840    /// from `respawn_terminate` so the application can fire resume independently
1841    /// of the respawn step. Disabled (manually-paused) nodes are skipped so a
1842    /// manual pause sticks, and detached (self-managed) Pause nodes are left
1843    /// parked; there is intentionally no dependency gate here.
1844    pub fn resume_pausable(&self) {
1845        for i in self.order.iter() {
1846            let Some(node) = self.nodes[*i as usize] else {
1847                continue;
1848            };
1849            if matches!(node.mode, Mode::Pause) && !node.is_disabled() && !node.is_detached() {
1850                node.reset();
1851                info!("supervisor: resuming {}", node.name);
1852                node.signal_resume();
1853                node.set_running(true);
1854            }
1855        }
1856    }
1857
1858    /// Reset and re-spawn every Terminate-mode node in dependency order.
1859    /// Pause-mode nodes are untouched (use `resume_pausable`); `OnDemand` nodes
1860    /// are left down — they re-grow under load via `start_node`. Disabled nodes
1861    /// are skipped so a manual stop sticks across the bring-up. Detached nodes are
1862    /// skipped too: `teardown` never brought them down, so they are still running
1863    /// and re-spawning would double-spawn them (see [`TaskNode::set_detached`]). The
1864    /// reset happens before the spawn so newly-running tasks see a clean handle.
1865    pub async fn respawn_terminate(&self, spawner: Spawner) -> Result<(), SpawnError> {
1866        for i in self.order.iter() {
1867            let Some(node) = self.nodes[*i as usize] else {
1868                continue;
1869            };
1870            if matches!(node.mode, Mode::Terminate) && !node.is_disabled() && !node.is_detached() {
1871                node.reset();
1872                info!("supervisor: respawning {}", node.name);
1873                if let Some(spawn) = node.spawn {
1874                    await_spawn_slot(node).await?;
1875                    // A `resources:` node's previous instance restores its slot
1876                    // value only after the shutdown ack, so wait (bounded) for
1877                    // the restore before the glue's take().
1878                    await_resources(node).await?;
1879                    await_ready_deps(node).await?;
1880                    spawn(spawner)?;
1881                }
1882                node.set_running(true);
1883            }
1884        }
1885        Ok(())
1886    }
1887}
1888
1889// ─── Runtime control (dependency- and pool-honoring start/stop) ────────────
1890//
1891// The `apply_control` entry point drives one `ControlCommand` from the
1892// application's control surface. Unlike the pool's bare `start_node`/`stop_node`,
1893// these honor the graph: a stop cascades through dependents (so nothing is left
1894// running without a dependency), a start cascades through deps (so nothing comes
1895// up before what it needs), and either expands across a whole `ElasticPool` so
1896// the pool is controlled as a unit. A manual stop/pause also sets the
1897// lifecycle-spanning `disabled` flag, so it sticks against the elastic policy and
1898// the wake respawn.
1899
1900// Graph-index helpers used by BOTH the control plane and the pool driver, so they
1901// are gated on either feature — `pool` alone (no `control`) must still compile.
1902#[cfg(any(feature = "control", feature = "pool"))]
1903impl<const N: usize> Supervisor<N> {
1904    /// Position of `node` in `self.nodes` (pointer identity — every node is a
1905    /// `&'static`). `None` only if the node isn't in this graph (impossible for
1906    /// targets sourced from `GRAPH.nodes`; treated as a no-op by callers).
1907    fn index_of(&self, node: &'static TaskNode) -> Option<usize> {
1908        self.nodes
1909            .iter()
1910            .position(|n| n.is_some_and(|x| core::ptr::eq(x, node)))
1911    }
1912
1913    /// Whether every dependency of `node` is currently running, resolved through
1914    /// the graph's index table. The pool driver checks this before growing a
1915    /// worker, so a pool member is never spawned while one of its dependencies is
1916    /// down.
1917    #[cfg(feature = "pool")]
1918    pub(crate) fn deps_running(&self, node: &'static TaskNode) -> bool {
1919        match self.index_of(node) {
1920            Some(i) => self.deps[i]
1921                .iter()
1922                .all(|&di| self.nodes[di as usize].is_some_and(|n| n.is_running())),
1923            None => false,
1924        }
1925    }
1926}
1927
1928#[cfg(feature = "control")]
1929impl<const N: usize> Supervisor<N> {
1930    /// Seed a membership set with `target` plus — if `target` belongs to an
1931    /// elastic pool — every member of that pool, so control is applied to the
1932    /// whole pool atomically. Pool membership is read from `GRAPH.pools`; with no
1933    /// pools (the `pool` feature off, or none declared) this is just `{target}`.
1934    fn seed(&self, target: &'static TaskNode, set: &mut [bool; N]) {
1935        if let Some(i) = self.index_of(target) {
1936            set[i] = true;
1937        }
1938        #[cfg(feature = "pool")]
1939        for pool in self.pools {
1940            let members = pool.members();
1941            if members.iter().any(|m| core::ptr::eq(*m, target)) {
1942                for m in members {
1943                    if let Some(i) = self.index_of(m) {
1944                        set[i] = true;
1945                    }
1946                }
1947            }
1948        }
1949    }
1950
1951    /// Apply one control command, honoring pool membership and the dependency
1952    /// graph — the mailbox-dispatch form of [`activate`](Self::activate) /
1953    /// [`deactivate`](Self::deactivate) (call those directly when you hold the
1954    /// supervisor). Run from the supervisor's driver loop (never concurrently
1955    /// with itself), so the cascade is atomic from the application's
1956    /// perspective. A `Deactivate` cascade propagates a missed shutdown ack as
1957    /// [`ShutdownTimeout`] (the cascade aborts at the offending node, dependents
1958    /// already stopped); `Activate` cannot fail this way.
1959    pub async fn apply_control(
1960        &self,
1961        cmd: ControlCommand,
1962        spawner: Spawner,
1963    ) -> Result<(), ShutdownTimeout> {
1964        match cmd.op {
1965            ControlOp::Deactivate => self.deactivate(cmd.node).await,
1966            ControlOp::Activate => {
1967                self.activate(cmd.node, spawner).await;
1968                Ok(())
1969            }
1970        }
1971    }
1972
1973    /// Bring `target` (and its pool, and every transitive dependent) down, in
1974    /// reverse-topological order so each dependent stops before the dependency it
1975    /// relies on — the cascading "turn this subsystem off" verb, and the exit
1976    /// half of the subordinate sub-graph pattern's one-graph variant. Marks
1977    /// the whole set `disabled` so the stop sticks against the elastic policy
1978    /// and the wake respawn until a matching [`activate`](Self::activate).
1979    /// Aborts with [`ShutdownTimeout`] on a missed ack (the offending node stays
1980    /// running and disabled; dependents visited before it are already down).
1981    ///
1982    /// Contrast [`stop_node`](Self::stop_node): ONE node, no cascade, no
1983    /// `disabled` latch (the pool-shrink primitive). Call this directly when
1984    /// you hold the supervisor; [`request_control`] +
1985    /// [`apply_control`](Self::apply_control) is the same operation routed
1986    /// through the mailbox from code that doesn't.
1987    pub async fn deactivate(&self, target: &'static TaskNode) -> Result<(), ShutdownTimeout> {
1988        let mut set = [false; N];
1989        self.seed(target, &mut set);
1990
1991        // Grow the set to include transitive dependents. `order` is
1992        // dependency-first, so when we reach a node its deps are already decided;
1993        // a node joins if any dep it declares is already in the set.
1994        for i in self.order.iter() {
1995            let j = *i as usize;
1996            if set[j] {
1997                continue;
1998            }
1999            let Some(node) = self.nodes[j] else {
2000                continue;
2001            };
2002            // A detached node declares its dep only for start ordering and intends
2003            // to outlive it, so it's never pulled into the cascade.
2004            if node.is_detached() {
2005                continue;
2006            }
2007            if self.deps[j].iter().any(|&di| set[di as usize]) {
2008                set[j] = true;
2009            }
2010        }
2011
2012        // Tear down in reverse topo order (dependents before their deps).
2013        for i in self.order.iter().rev() {
2014            let j = *i as usize;
2015            if !set[j] {
2016                continue;
2017            }
2018            let Some(node) = self.nodes[j] else {
2019                continue;
2020            };
2021            // A detached node is self-managed — never control-stop it. The growth loop
2022            // keeps detached *dependents* out of the set; this also covers a detached
2023            // node that was seeded directly (or a detached pool member). Without it a
2024            // detached one-shot that already exited (stale `is_running`, no ack path)
2025            // would be signalled a shutdown it can never acknowledge, failing here
2026            // with a spurious `ShutdownTimeout`.
2027            if node.is_detached() {
2028                continue;
2029            }
2030            node.set_disabled(true);
2031            if node.is_running() {
2032                info!("supervisor: control-stop {}", node.name);
2033                self.shutdown_and_wait(node).await?;
2034            }
2035        }
2036        Ok(())
2037    }
2038
2039    /// Bring `target` (and its pool, and every transitive dependency) up, in
2040    /// topological order so each dependency starts before its dependent — the
2041    /// cascading "turn this subsystem on" verb, and the entry half of the
2042    /// subordinate sub-graph pattern's one-graph variant: `activate` on a
2043    /// subtree's LEAF pulls its whole dependency chain up, skipping
2044    /// already-running nodes. Per-node spawn errors are deliberately swallowed
2045    /// (a cascade is best-effort; a `Busy` member is re-driven by the pool
2046    /// policy or a later activate), so this returns `()` — asymmetric with
2047    /// [`deactivate`](Self::deactivate) on purpose. Clears
2048    /// `disabled` across the set. `OnDemand` (pool) members are only re-enabled,
2049    /// not force-spawned — the elastic policy re-grows them under load, which is
2050    /// the whole point of the pool.
2051    pub async fn activate(&self, target: &'static TaskNode, spawner: Spawner) {
2052        let mut set = [false; N];
2053        self.seed(target, &mut set);
2054
2055        // Grow the set to include transitive deps. Walk dependents-first
2056        // (reverse topo); when a set member is seen, pull in its direct deps.
2057        // A detached member's `deps:` are start-ordering only (the node is
2058        // self-managed), so don't expand from it — mirrors deactivate's guard;
2059        // otherwise activating a detached target would un-disable deps that
2060        // were independently disabled.
2061        for i in self.order.iter().rev() {
2062            let j = *i as usize;
2063            if set[j] && !self.nodes[j].is_some_and(|n| n.is_detached()) {
2064                for &di in self.deps[j] {
2065                    set[di as usize] = true;
2066                }
2067            }
2068        }
2069
2070        // Bring up in topo order (deps before dependents).
2071        for i in self.order.iter() {
2072            let j = *i as usize;
2073            if !set[j] {
2074                continue;
2075            }
2076            let Some(node) = self.nodes[j] else {
2077                continue;
2078            };
2079            // A detached node is self-managed — the supervisor never re-enables or
2080            // re-starts it, even when it is a dependency of an activated target.
2081            if node.is_detached() {
2082                continue;
2083            }
2084            node.set_disabled(false);
2085            if node.is_running() {
2086                continue;
2087            }
2088            match node.mode {
2089                Mode::Terminate => {
2090                    info!("supervisor: control-start {}", node.name);
2091                    // SpawnError::Busy (pool exhausted) → can't start, skip.
2092                    let _ = self.start_node(node, spawner).await;
2093                }
2094                Mode::Pause => {
2095                    info!("supervisor: control-resume {}", node.name);
2096                    node.reset();
2097                    node.signal_resume();
2098                    node.set_running(true);
2099                }
2100                // Pool worker — leave it down; the elastic policy regrows it on
2101                // demand now that `disabled` is cleared.
2102                Mode::OnDemand => {}
2103            }
2104        }
2105    }
2106}
2107
2108// ─── Topological sort (Kahn's algorithm, const) ───────────────────────────
2109//
2110// Computes the topological order at *compile time* over a per-node
2111// dependency-index table; a dependency cycle is a compile error.
2112
2113/// Topologically sort a graph given as a per-node dependency-index table.
2114///
2115/// `deps[i]` lists the indices of the nodes that node `i` depends on; the result
2116/// lists node indices in dependency-first order (a dependency appears before its
2117/// dependents). The supervisor iterates it forward for `start` /
2118/// `respawn_terminate` and in reverse for `teardown`.
2119///
2120/// Evaluated at compile time by the code `supervisor_graph!` generates — a
2121/// dependency **cycle is a compile error** (the `panic!` fires during const
2122/// evaluation). `#[doc(hidden)]`: an engine for the macro, not a user-facing API.
2123///
2124/// Supports at most 256 nodes: indices are `u8`, so a larger `N` would truncate.
2125/// The macro rejects bigger graphs at expansion; the assert below is defense in
2126/// depth for a manual caller (a const-eval panic, i.e. a compile error).
2127#[doc(hidden)]
2128#[must_use]
2129pub const fn topo_sort_const<const N: usize>(deps: &[&'static [u8]; N]) -> [u8; N] {
2130    assert!(
2131        N <= 256,
2132        "supervisor graph exceeds 256 node slots (indices are u8)"
2133    );
2134    // in_degree[i] = number of deps of node i not yet resolved.
2135    let mut in_degree = [0u8; N];
2136    let mut i = 0;
2137    while i < N {
2138        in_degree[i] = deps[i].len() as u8;
2139        i += 1;
2140    }
2141
2142    // Queue (fixed array, head/tail indices) seeded with the dependency-free nodes.
2143    let mut queue = [0u8; N];
2144    let mut tail = 0;
2145    i = 0;
2146    while i < N {
2147        if in_degree[i] == 0 {
2148            queue[tail] = i as u8;
2149            tail += 1;
2150        }
2151        i += 1;
2152    }
2153
2154    let mut order = [0u8; N];
2155    let mut produced = 0;
2156    let mut head = 0;
2157    while head < tail {
2158        let node = queue[head] as usize;
2159        head += 1;
2160        order[produced] = node as u8;
2161        produced += 1;
2162
2163        // Decrement the in-degree of every node that depends on `node`.
2164        let mut j = 0;
2165        while j < N {
2166            if in_degree[j] != 0 {
2167                let mut depends = false;
2168                let mut k = 0;
2169                while k < deps[j].len() {
2170                    if deps[j][k] as usize == node {
2171                        depends = true;
2172                    }
2173                    k += 1;
2174                }
2175                if depends {
2176                    in_degree[j] -= 1;
2177                    if in_degree[j] == 0 {
2178                        queue[tail] = j as u8;
2179                        tail += 1;
2180                    }
2181                }
2182            }
2183            j += 1;
2184        }
2185    }
2186
2187    // A cycle leaves some nodes unproduced. During const eval this panic is a
2188    // compile error, so cyclic graphs are rejected at build time. `core::panic!`
2189    // (not the crate's defmt-shimmed `panic!`) keeps this const-evaluable.
2190    if produced != N {
2191        core::panic!("supervisor_graph!: dependency cycle");
2192    }
2193    order
2194}
2195
2196#[cfg(feature = "pool")]
2197mod pool;
2198#[cfg(feature = "pool")]
2199pub use pool::*;
2200
2201#[cfg(feature = "trace")]
2202pub mod trace;
2203
2204#[cfg(feature = "macros")]
2205pub use embassy_supervisor_macros::supervisor_fragment;
2206/// Declare a supervised task graph and compute its topological order at compile
2207/// time (single source of nodes, deps, pool, and order). See the
2208/// `embassy-supervisor-macros` crate for the surface syntax.
2209#[cfg(feature = "macros")]
2210pub use embassy_supervisor_macros::supervisor_graph;
2211
2212/// Assemble one graph from `supervisor_fragment!` relays plus compose-site
2213/// items:
2214///
2215/// ```ignore
2216/// embassy_supervisor::compose_graph! {
2217///     fragments: [::net_stack::NET_FRAG, HTTP_FRAG],
2218///     graph: {
2219///         node APP = Terminate, deps: [NET], task: app_worker; // cross-fragment dep
2220///     }
2221/// }
2222/// ```
2223///
2224/// Fragments expand in listed order, then the `graph:` items; everything
2225/// reaches ONE `supervisor_graph!` expansion, so cross-fragment deps resolve by
2226/// name (forward references included) and every compile-time pass — name map,
2227/// u8 slot indices, topological order, shared-slot dedup, the 256-node cap —
2228/// checks the whole composed graph. One compose site per binary (it emits the
2229/// usual `GRAPH`/`NODES`/`DEPS` statics and, under `trace-hooks`, the hook
2230/// symbols). Name collisions across fragments hit the ordinary duplicate-name
2231/// errors, attributed to the owning fragment; prefix fragment-public names.
2232#[cfg(feature = "macros")]
2233#[macro_export]
2234macro_rules! compose_graph {
2235    // `name: X,` first renames the composed graph static (see
2236    // `supervisor_graph!`'s `name:`) — seeded into the accumulator ahead of
2237    // every fragment's items so it stays the expansion's first item.
2238    (name: $n:ident, fragments: [$f:path $(, $r:path)* $(,)?], graph: {$($g:tt)*}) => {
2239        $f! { @emit $crate::compose_graph, [$($r),*], {name: $n;}, {$($g)*} }
2240    };
2241    (fragments: [$f:path $(, $r:path)* $(,)?], graph: {$($g:tt)*}) => {
2242        $f! { @emit $crate::compose_graph, [$($r),*], {}, {$($g)*} }
2243    };
2244    (@next [], {$($acc:tt)*}, {$($g:tt)*}) => {
2245        $crate::supervisor_graph! { $($acc)* $($g)* }
2246    };
2247    (@next [$f:path $(, $r:path)*], {$($acc:tt)*}, $g:tt) => {
2248        $f! { @emit $crate::compose_graph, [$($r),*], {$($acc)*}, $g }
2249    };
2250}
2251
2252/// Building blocks for `supervisor_graph!`-generated code — NOT public API.
2253///
2254/// The macro's `local`-marked `resources:` entries emit a slot *type* at the
2255/// graph declaration site (it needs an `unsafe impl Sync`, — same reason the
2256/// `trace-hooks` symbols are emitted there). That generated type must name
2257/// the exact `Signal`/mutex types in [`ResourceGate`]'s signature; re-exporting
2258/// them here keeps the macro's contract that a consumer only needs
2259/// `embassy-supervisor` itself as a real-named dependency (not `embassy-sync`).
2260#[doc(hidden)]
2261pub mod _export {
2262    pub use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
2263    pub use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
2264    pub use embassy_sync::signal::Signal;
2265    // For the `slot_timeout:` clause's emitted `with_slot_timeout(..)` call.
2266    pub use embassy_time::Duration;
2267}