Skip to main content

embassy_supervisor/
lib.rs

1//! Dependency-ordered task-lifecycle supervision for [embassy](https://embassy.dev) firmware.
2//!
3//! `embassy-supervisor` brings tasks up in dependency order, supervises their
4//! lifecycle (`Terminate`, `Pause`, `OnDemand`), tears dependents down before
5//! the things they depend on, and verifies declared dataflow against live
6//! behaviour. The graph is declared through the [`supervisor_graph!`] macro
7//! (re-exported from `embassy-supervisor-macros`) and checked at compile time.
8//!
9//! The crate is HAL-agnostic, `no_std`, and has no allocator or board-specific
10//! dependencies.
11
12#![cfg_attr(not(test), no_std)]
13#![forbid(unsafe_code)]
14#![deny(missing_docs)]
15
16#[macro_use]
17mod fmt;
18
19#[cfg(all(
20    feature = "log",
21    not(target_os = "none"),
22    not(all(target_arch = "wasm32", target_os = "unknown"))
23))]
24mod host_log;
25#[cfg(all(
26    feature = "log",
27    not(target_os = "none"),
28    not(all(target_arch = "wasm32", target_os = "unknown"))
29))]
30pub use host_log::init_host_logging;
31
32use core::cell::Cell;
33use core::future::Future;
34use core::pin::Pin;
35use core::sync::atomic::Ordering;
36use core::task::{Context, Poll};
37
38use embassy_executor::{SendSpawner, SpawnError, Spawner};
39use embassy_futures::select::{Either, select};
40use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
41use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
42#[cfg(any(feature = "control", feature = "liveness-monitor"))]
43use embassy_sync::channel::Channel;
44use embassy_sync::signal::Signal;
45use embassy_time::{Timer, with_timeout};
46#[cfg(feature = "liveness")]
47use portable_atomic::AtomicBool;
48#[cfg(any(feature = "liveness-monitor", feature = "fault-inject"))]
49use portable_atomic::AtomicU8;
50use portable_atomic::AtomicU16;
51#[cfg(any(
52    feature = "trace",
53    feature = "liveness",
54    feature = "epochs",
55    feature = "fault-inject"
56))]
57use portable_atomic::AtomicU32;
58
59#[cfg(feature = "pool")]
60static SCALE_REQ: Signal<CriticalSectionRawMutex, ()> = Signal::new();
61
62/// Signal that an elastic pool should re-evaluate its scaling decision.
63///
64/// This is a no-op when the `pool` feature is disabled.
65pub fn request_scale() {
66    #[cfg(feature = "pool")]
67    SCALE_REQ.signal(());
68}
69
70#[cfg(feature = "pool")]
71/// Wait until something requests a pool scaling re-evaluation.
72pub async fn wait_scale() {
73    SCALE_REQ.wait().await;
74}
75
76static GATE_EVT: Signal<CriticalSectionRawMutex, ()> = Signal::new();
77
78static STOP_EVT: Signal<CriticalSectionRawMutex, ()> = Signal::new();
79
80#[doc(hidden)]
81#[inline]
82pub fn __sv_gate_event() {
83    GATE_EVT.signal(());
84}
85
86#[cfg(feature = "control")]
87#[non_exhaustive]
88#[derive(Clone, Copy, PartialEq, Eq, Debug)]
89/// A control request issued to the supervisor for a node.
90pub enum ControlOp {
91    /// Request that the node be activated.
92    Activate,
93    /// Request that the node be deactivated.
94    Deactivate,
95    /// Request that the node be restarted.
96    #[cfg(feature = "restart")]
97    Restart,
98}
99
100#[cfg(feature = "control")]
101#[derive(Clone, Copy, Debug)]
102/// A control request addressed to a specific node.
103pub struct ControlCommand {
104    /// The target node.
105    pub node: &'static TaskNode,
106    /// The direction to drive it.
107    pub op: ControlOp,
108}
109
110/// App → supervisor control mailbox. `&'static TaskNode` is `Copy + Sync`, so
111/// the target rides the channel directly — no name lookup needed supervisor-side.
112#[cfg(feature = "control")]
113static CONTROL_REQ: Channel<CriticalSectionRawMutex, ControlCommand, 4> = Channel::new();
114
115/// The control mailbox was full (4 outstanding requests) and the request was
116/// not enqueued. Returned by [`try_request_control`]; retry after the
117/// supervisor's driver loop has drained a command, or use the awaiting
118/// [`request_control`] from async contexts.
119#[cfg(feature = "control")]
120#[derive(Clone, Copy, PartialEq, Eq, Debug)]
121pub struct ControlQueueFull;
122
123#[cfg(all(feature = "control", feature = "defmt"))]
124impl defmt::Format for ControlQueueFull {
125    fn format(&self, fmt: defmt::Formatter) {
126        defmt::write!(fmt, "control queue full");
127    }
128}
129
130/// Enqueue a control request, waiting for mailbox capacity if it is full.
131/// Lossless — the request is delivered once the supervisor's driver loop drains
132/// an earlier command. Called by the application's control surface.
133#[cfg(feature = "control")]
134pub async fn request_control(node: &'static TaskNode, op: ControlOp) {
135    CONTROL_REQ.send(ControlCommand { node, op }).await;
136}
137
138/// Non-blocking variant of [`request_control`] for sync contexts (ISRs,
139/// callbacks). Fails with [`ControlQueueFull`] instead of dropping the request
140/// when the mailbox is full — the caller decides whether to retry or surface it.
141#[cfg(feature = "control")]
142pub fn try_request_control(node: &'static TaskNode, op: ControlOp) -> Result<(), ControlQueueFull> {
143    CONTROL_REQ
144        .try_send(ControlCommand { node, op })
145        .map_err(|_| ControlQueueFull)
146}
147
148/// Await the next control request. Selected by the supervisor's driver loop
149/// against pool scaling and any other application wake sources.
150#[cfg(feature = "control")]
151pub async fn wait_control() -> ControlCommand {
152    CONTROL_REQ.receive().await
153}
154
155// ─── Declared dataflow coupling ───────────────────────────────────────────
156//
157// `deps:` says "spawn me after that". It is consumed once and says nothing
158// about the relationship that holds for the rest of the program's life: the
159// signals a task reads and writes. `reads:`/`writes:` declare *that* relation,
160// which outlives the spawn the `deps:` edge described.
161
162/// A signal a node declares it reads or writes.
163///
164/// Implemented for every `Sync` type by a blanket impl, so no consumer ever
165/// writes one: the graph's `reads:`/`writes:` clauses coerce the named statics
166/// to `&'static dyn CouplingPoint` for you. The trait carries **no methods** —
167/// the supervisor neither knows nor cares what a signal is. Its only purpose is
168/// to type-erase heterogeneous statics into one slice, so identity is the
169/// static's address and nothing else.
170#[cfg(feature = "coupling")]
171pub trait CouplingPoint: Sync {}
172
173#[cfg(feature = "coupling")]
174impl<T: Sync + ?Sized> CouplingPoint for T {}
175
176#[cfg(feature = "heap-state")]
177pub use bytemuck::Zeroable;
178
179#[cfg(feature = "coupling-observe")]
180pub use embassy_supervisor_observe::Observable;
181
182#[cfg(feature = "coupling-observe")]
183#[derive(Clone, Copy)]
184/// A callable that returns a signal's change count for observation.
185pub struct Observer {
186    count: fn() -> u32,
187}
188
189#[cfg(feature = "coupling-observe")]
190impl Observer {
191    /// Wrap a function that returns the signal's current change count.
192    pub const fn new(count: fn() -> u32) -> Self {
193        Self { count }
194    }
195
196    /// Return the current change count.
197    pub fn count(&self) -> u32 {
198        (self.count)()
199    }
200}
201
202#[cfg(feature = "coupling")]
203#[derive(Clone, Copy)]
204/// A declared read/write coupling between a node and a signal.
205pub struct Coupling {
206    name: &'static str,
207    point: &'static dyn CouplingPoint,
208    #[cfg(feature = "coupling-observe")]
209    observe: Option<Observer>,
210    #[cfg(feature = "coupling-observe")]
211    beat: bool,
212    #[cfg(feature = "veto")]
213    veto: Option<u8>,
214}
215
216#[cfg(feature = "coupling")]
217impl Coupling {
218    /// Create a coupling with the given path name and wiring point.
219    pub const fn new(name: &'static str, point: &'static dyn CouplingPoint) -> Self {
220        Self {
221            name,
222            point,
223            #[cfg(feature = "coupling-observe")]
224            observe: None,
225            #[cfg(feature = "coupling-observe")]
226            beat: false,
227            #[cfg(feature = "veto")]
228            veto: None,
229        }
230    }
231
232    #[cfg(feature = "veto")]
233    /// Mark this coupling as a `veto` write holding contributor `slot`.
234    pub const fn veto(mut self, slot: u8) -> Self {
235        self.veto = Some(slot);
236        self
237    }
238
239    /// This writer's contributor slot, if the entry carries the `veto` marker.
240    #[cfg(feature = "veto")]
241    pub const fn veto_slot(&self) -> Option<u8> {
242        self.veto
243    }
244
245    #[cfg(feature = "coupling-observe")]
246    /// Mark this coupling as observed with the given observer.
247    pub const fn observed(mut self, observer: Observer) -> Self {
248        self.observe = Some(observer);
249        self
250    }
251
252    #[cfg(feature = "coupling-observe")]
253    /// Mark this coupling as feeding a heartbeat.
254    pub const fn beat(mut self) -> Self {
255        self.beat = true;
256        self
257    }
258
259    /// Return the path name of this coupling.
260    pub const fn name(&self) -> &'static str {
261        self.name
262    }
263
264    /// This entry's accessor, if it carries the `observed` marker.
265    #[cfg(feature = "coupling-observe")]
266    pub const fn observer(&self) -> Option<Observer> {
267        self.observe
268    }
269
270    /// Return whether this coupling feeds a heartbeat.
271    #[cfg(feature = "coupling-observe")]
272    pub const fn beats(&self) -> bool {
273        self.beat
274    }
275}
276
277#[cfg(feature = "coupling")]
278impl core::fmt::Debug for Coupling {
279    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280        f.write_str(self.name)
281    }
282}
283
284/// Do two declarations refer to the same static? Compares **data pointers
285/// only** (`ptr::addr_eq`): the same static reached through two different
286/// generic instantiations can carry different vtables, and a plain
287/// `ptr::eq` on trait objects would compare those too and miss the match.
288#[cfg(feature = "coupling")]
289fn same_signal(a: &Coupling, b: &Coupling) -> bool {
290    core::ptr::addr_eq(
291        a.point as *const dyn CouplingPoint,
292        b.point as *const dyn CouplingPoint,
293    )
294}
295
296/// Does `table` carry an entry whose declared path ends in the same segment as
297/// `name`? Emitted by `supervisor_graph!` as a const assertion behind a marked
298/// entry beside `discover`: the entry may only add a marker to a signal the
299/// task fn already accesses, and this is what checks it before anything runs.
300///
301/// **By name, and only the last segment**, because a const context cannot
302/// compare addresses: `ptr::addr_eq` is not `const`, and a pointer has no
303/// integer value at compile time. The declaration and the call site usually
304/// spell the same static differently (`crate::signals::EST` against an aliased
305/// `s::EST`), so the tail is the most that can be matched. Two consequences,
306/// both benign: a signal reached through a renaming re-export fails the check
307/// though it is legitimate, and two distinct signals sharing a final segment
308/// pass it. A marker that lands on the wrong static costs nothing silently —
309/// no verb write matches it, so the node simply never beats and the liveness
310/// monitor reports it stale.
311#[doc(hidden)]
312#[cfg(feature = "coupling")]
313pub const fn __sv_tail_declared(table: &[Coupling], name: &str) -> bool {
314    let mut i = 0;
315    while i < table.len() {
316        if tail_eq(table[i].name, name) {
317            return true;
318        }
319        i += 1;
320    }
321    false
322}
323
324/// Byte offset just past the last `::` in `s`, or 0.
325#[cfg(feature = "coupling")]
326const fn tail_start(s: &str) -> usize {
327    let b = s.as_bytes();
328    let mut i = b.len();
329    while i >= 2 {
330        if b[i - 1] == b':' && b[i - 2] == b':' {
331            return i;
332        }
333        i -= 1;
334    }
335    0
336}
337
338/// Do two paths end in the same segment, index and all (`ARR[1]`)?
339#[cfg(feature = "coupling")]
340const fn tail_eq(a: &str, b: &str) -> bool {
341    let (ab, bb) = (a.as_bytes(), b.as_bytes());
342    let (ai, bi) = (tail_start(a), tail_start(b));
343    if ab.len() - ai != bb.len() - bi {
344        return false;
345    }
346    let mut k = 0;
347    while ai + k < ab.len() {
348        if ab[ai + k] != bb[bi + k] {
349            return false;
350        }
351        k += 1;
352    }
353    true
354}
355
356// ─── Bound readiness edges (task → supervisor) ────────────────────────────
357//
358// A dedicated mailbox rather than a widening of CONTROL_REQ: a flapping link
359// can produce readiness transitions far faster than an operator produces
360// control commands, and the two must not be able to starve each other.
361
362/// "Some node's readiness changed." A coalescing `Signal`, not a queue, and
363/// deliberately so: the cascade handler re-reads live `is_ready` state rather
364/// than trusting a message, so several transitions collapsing into one wake is
365/// not a loss — it is the whole point. A flapping link cannot flood the
366/// supervisor, and no readiness change can ever be dropped for lack of queue
367/// space. Same single-consumer shape as `SCALE_REQ`: many tasks `signal()`,
368/// only the supervisor `wait()`s.
369#[cfg(feature = "bound-deps")]
370static BIND_REQ: Signal<CriticalSectionRawMutex, ()> = Signal::new();
371
372/// Post a readiness transition. Sync and non-blocking — called from task
373/// context inside `set_ready`/`clear_ready`, which must never park.
374#[cfg(feature = "bound-deps")]
375fn notify_bind() {
376    BIND_REQ.signal(());
377}
378
379/// Await the next readiness transition of any node. The supervisor's driver
380/// loop selects this alongside pool scaling and control.
381#[cfg(feature = "bound-deps")]
382pub async fn wait_bind() {
383    BIND_REQ.wait().await
384}
385
386// ─── Fault injection (`fault-inject`) ─────────────────────────────────────
387
388/// A fault injected into a node through [`TaskNode::inject`]. The verbs act on
389/// the task, not the worker.
390///
391/// - **Stall**: stop polling the worker.
392/// - **Wedge**: hide shutdown and swallow the ack.
393/// - **Crash**: drop the worker future.
394/// - **Hog**: busy-spin the executor for the given bound.
395///
396/// Off by default. For benches, tests, and dashboards.
397#[cfg(feature = "fault-inject")]
398#[non_exhaustive]
399#[derive(Clone, Copy, PartialEq, Eq, Debug)]
400pub enum Fault {
401    /// Healthy.
402    None,
403    /// Stop polling the worker.
404    Stall,
405    /// Hide the shutdown request and swallow the ack.
406    Wedge,
407    /// Drop the worker future; the node reads as exited.
408    Crash,
409    /// Busy-spin the executor for the given bound.
410    Hog(embassy_time::Duration),
411}
412
413#[cfg(feature = "fault-inject")]
414impl Fault {
415    /// The verb as a short lowercase name (`"stall"`, `"wedge"`, `"crash"`,
416    /// `"hog"`; `None` is `"none"`), for logs and JSON.
417    pub const fn as_str(self) -> &'static str {
418        match self {
419            Fault::None => "none",
420            Fault::Stall => "stall",
421            Fault::Wedge => "wedge",
422            Fault::Crash => "crash",
423            Fault::Hog(_) => "hog",
424        }
425    }
426
427    fn encode(self) -> (u8, u32) {
428        match self {
429            Fault::None => (0, 0),
430            Fault::Stall => (1, 0),
431            Fault::Wedge => (2, 0),
432            Fault::Crash => (3, 0),
433            Fault::Hog(d) => (4, d.as_millis().min(u32::MAX as u64) as u32),
434        }
435    }
436
437    fn decode(code: u8, ms: u32) -> Self {
438        match code {
439            1 => Fault::Stall,
440            2 => Fault::Wedge,
441            3 => Fault::Crash,
442            4 => Fault::Hog(embassy_time::Duration::from_millis(ms as u64)),
443            _ => Fault::None,
444        }
445    }
446}
447
448#[cfg(all(feature = "fault-inject", feature = "defmt"))]
449impl defmt::Format for Fault {
450    fn format(&self, fmt: defmt::Formatter) {
451        defmt::write!(fmt, "{}", self.as_str());
452    }
453}
454
455/// Why [`TaskNode::inject`] refused.
456#[cfg(feature = "fault-inject")]
457#[non_exhaustive]
458#[derive(Clone, Copy, PartialEq, Eq, Debug)]
459pub enum InjectError {
460    /// The node is a hand-written `spawn:` fn; only wedge applies.
461    NoShell,
462}
463
464#[cfg(all(feature = "fault-inject", feature = "defmt"))]
465impl defmt::Format for InjectError {
466    fn format(&self, fmt: defmt::Formatter) {
467        defmt::write!(
468            fmt,
469            "no shell: only a `task:` node can be stalled, crashed or hogged"
470        );
471    }
472}
473
474/// State for an in-progress `Hog` fault.
475#[cfg(feature = "fault-inject")]
476struct HogState {
477    grace: Timer,
478    bound: embassy_time::Duration,
479}
480
481/// Wrapper for `task:` worker futures. Returns `Some(output)` on completion,
482/// or `None` if [`Fault::Crash`] dropped the worker.
483#[cfg(feature = "fault-inject")]
484#[doc(hidden)]
485pub struct Injected<'a, F> {
486    node: &'a TaskNode,
487    inner: Option<F>,
488    hog: Option<HogState>,
489}
490
491#[cfg(feature = "fault-inject")]
492impl<'a, F: Future + Unpin> Injected<'a, F> {
493    /// Grace period before a `Hog` spin begins.
494    pub const HOG_GRACE: embassy_time::Duration = embassy_time::Duration::from_millis(250);
495
496    #[doc(hidden)]
497    pub fn new(node: &'a TaskNode, inner: F) -> Self {
498        Self {
499            node,
500            inner: Some(inner),
501            hog: None,
502        }
503    }
504}
505
506#[cfg(feature = "fault-inject")]
507impl<F: Future + Unpin> Future for Injected<'_, F> {
508    type Output = Option<F::Output>;
509
510    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
511        let this = self.get_mut();
512        let node = this.node;
513        node.handle.fault_waker.register(cx.waker());
514        loop {
515            match node.fault() {
516                Fault::Stall if !node.handle.flag(flag::SHUTDOWN) => {
517                    if core::pin::pin!(node.handle.shutdown_wake.wait())
518                        .poll(cx)
519                        .is_ready()
520                    {
521                        continue;
522                    }
523                    return Poll::Pending;
524                }
525                Fault::Crash => {
526                    this.inner = None;
527                    let _ = node.handle.fault.compare_exchange(
528                        3,
529                        0,
530                        Ordering::AcqRel,
531                        Ordering::Relaxed,
532                    );
533                    warn!("supervisor: {} crashed (injected)", node.name());
534                    return Poll::Ready(None);
535                }
536                Fault::Hog(bound) => {
537                    if !matches!(&this.hog, Some(h) if h.bound == bound) {
538                        this.hog = Some(HogState {
539                            grace: Timer::after(Injected::<F>::HOG_GRACE),
540                            bound,
541                        });
542                    }
543                    let hog = this.hog.as_mut().unwrap();
544                    if Pin::new(&mut hog.grace).poll(cx).is_pending() {
545                        return Poll::Pending;
546                    }
547                    let bound = hog.bound;
548                    this.hog = None;
549                    warn!(
550                        "supervisor: {} hogging its executor for {} ms (injected)",
551                        node.name(),
552                        bound.as_millis()
553                    );
554                    let deadline = embassy_time::Instant::now() + bound;
555                    while embassy_time::Instant::now() < deadline {
556                        core::hint::spin_loop();
557                    }
558                    let _ = node.handle.fault.compare_exchange(
559                        4,
560                        0,
561                        Ordering::AcqRel,
562                        Ordering::Relaxed,
563                    );
564                }
565                _ => {}
566            }
567            break;
568        }
569        let Some(inner) = this.inner.as_mut() else {
570            return Poll::Pending;
571        };
572        match Pin::new(inner).poll(cx) {
573            Poll::Ready(out) => {
574                this.inner = None;
575                Poll::Ready(Some(out))
576            }
577            Poll::Pending => Poll::Pending,
578        }
579    }
580}
581
582// ─── Health events (supervisor → app) ─────────────────────────────────────
583//
584// The reporting half of `liveness-monitor`. Deliberately a mailbox rather than
585// a callback: the supervisor names what it saw, and the application decides —
586// on its own task, at its own priority — what that means. See the module docs
587// on why escalation is not built in.
588
589/// What the monitor observed about a node.
590///
591/// `#[non_exhaustive]`: further observations may be added without a breaking
592/// change, so match with a `_` arm.
593#[cfg(feature = "liveness-monitor")]
594#[non_exhaustive]
595#[derive(Clone, Copy, PartialEq, Eq, Debug)]
596pub enum HealthKind {
597    /// The node is still marked running but has not beaten within its
598    /// `beat_timeout:` for `beat_window:` consecutive sweeps — alive but
599    /// stalled, parked on an await that will never complete. Emitted **once**
600    /// per stall; the next event for this node is a [`Recovered`](Self::Recovered).
601    Stale {
602        /// Ticks since the node's last beat when the sweep tripped.
603        ticks: u32,
604    },
605    /// A node previously reported [`Stale`](Self::Stale) has beaten again.
606    Recovered,
607}
608
609/// One observation from [`Supervisor::monitor`], delivered through
610/// [`wait_health`].
611#[cfg(feature = "liveness-monitor")]
612#[derive(Clone, Copy, Debug)]
613pub struct HealthEvent {
614    /// The node the observation is about.
615    pub node: &'static TaskNode,
616    /// What was observed.
617    pub kind: HealthKind,
618}
619
620#[cfg(all(feature = "liveness-monitor", feature = "defmt"))]
621impl defmt::Format for HealthKind {
622    fn format(&self, fmt: defmt::Formatter) {
623        match self {
624            HealthKind::Stale { ticks } => defmt::write!(fmt, "stale for {} ticks", ticks),
625            HealthKind::Recovered => defmt::write!(fmt, "recovered"),
626        }
627    }
628}
629
630/// Supervisor → app health mailbox. Lossy on purpose (see
631/// [`Supervisor::monitor`]): the monitor must never block on a slow or absent
632/// consumer, and a still-stale node is re-reported by a later sweep anyway.
633#[cfg(feature = "liveness-monitor")]
634static HEALTH_EVT: Channel<CriticalSectionRawMutex, HealthEvent, 4> = Channel::new();
635
636/// Await the next health observation from [`Supervisor::monitor`].
637///
638/// The application's escalation point. What to do with a `Stale` report is
639/// domain-specific and therefore yours: log it, degrade to a safe mode, request
640/// a `Deactivate`, `clear_ready()` the node so future bring-up defers — the
641/// supervisor deliberately does none of these on its own.
642///
643/// ```ignore
644/// loop {
645///     let ev = embassy_supervisor::wait_health().await;
646///     match ev.kind {
647///         HealthKind::Stale { ticks } => warn!("{} stalled for {} ticks", ev.node.name, ticks),
648///         HealthKind::Recovered => info!("{} is beating", ev.node.name),
649///         _ => {}
650///     }
651/// }
652/// ```
653#[cfg(feature = "liveness-monitor")]
654pub async fn wait_health() -> HealthEvent {
655    HEALTH_EVT.receive().await
656}
657
658/// Non-blocking [`wait_health`], for a consumer that polls (a status endpoint
659/// draining pending events, an existing loop that must not park here).
660#[cfg(feature = "liveness-monitor")]
661pub fn try_wait_health() -> Option<HealthEvent> {
662    HEALTH_EVT.try_receive().ok()
663}
664
665/// Post one observation, dropping it if the app isn't keeping up. The monitor
666/// runs on the supervisor task, which must not park behind a health consumer —
667/// blocking here would stall pool scaling and control commands over a
668/// diagnostic.
669#[cfg(feature = "liveness-monitor")]
670fn emit_health(ev: HealthEvent) {
671    if HEALTH_EVT.try_send(ev).is_err() {
672        warn!(
673            "supervisor: health mailbox full, dropped an event for {}",
674            ev.node.name()
675        );
676    }
677}
678
679/// Default per-node timeout for `wait_dropped` (`ack_timeout:` in the graph
680/// overrides it per node). A task that doesn't ack within its window is a bug
681/// (a body that never notices the stop) or a wedge; the shutdown paths
682/// surface it as a [`NodeFault`] naming the node, and the application decides
683/// the escalation. 2 s comfortably exceeds a typical task's poll period and
684/// peripheral settle time.
685const SHUTDOWN_ACK_TIMEOUT_MS: u64 = 2_000;
686
687/// How much finer than its beat budget a `ready_on_write` node is probed while
688/// it has yet to assert. Eight keeps the added readiness latency well inside
689/// what a dependent's `slot_timeout` tolerates, without making bring-up busy.
690#[cfg(all(
691    feature = "liveness-monitor",
692    feature = "coupling-observe",
693    feature = "readiness"
694))]
695const READY_PROBE_DIVISOR: u64 = 8;
696
697/// A node-scoped lifecycle failure: which node, and what went wrong.
698///
699/// Every way bring-up or teardown can fail is about one node, so one type says
700/// so — the same `{ node, kind }` shape as [`HealthEvent`].
701/// It is what [`Supervisor::start`], [`Supervisor::teardown`],
702/// [`Supervisor::run`] and [`Supervisor::restart`] all return.
703///
704/// [`Display`](core::fmt::Display) is unconditional, so an application logging
705/// through anything other than `defmt` can render one without matching on
706/// [`FaultKind`].
707#[derive(Clone, Copy, Debug)]
708pub struct NodeFault {
709    /// The node the failure is about.
710    pub node: &'static TaskNode,
711    /// What went wrong.
712    pub kind: FaultKind,
713}
714
715/// What went wrong in a [`NodeFault`].
716///
717/// `#[non_exhaustive]`: new failure modes may be added without a breaking
718/// change.
719#[non_exhaustive]
720#[derive(Clone, Copy, Debug)]
721pub enum FaultKind {
722    /// A `ready`-marked dep did not assert readiness within the dependent's
723    /// `slot_timeout`. Either the dep is failing to reach its serving state, or
724    /// the budget is too tight for this build. Only plain `ready` edges fault
725    /// here: a `bound` edge parks the dependent instead.
726    ReadyDepTimeout {
727        /// The dep that never asserted.
728        dep: &'static TaskNode,
729    },
730    /// A `resources:` slot was still empty at the deadline — nothing called
731    /// `provide()` before the supervisor started, or a previous instance never
732    /// restored it.
733    ResourceMissing,
734    /// The node names an `executor:` slot that was never filled with a spawner.
735    ExecutorSlotEmpty,
736    /// The spawn itself was rejected: the task's pool is exhausted, or (with
737    /// `heap-state`) its `state:` allocation was refused. These are the only
738    /// cases embassy's own `SpawnError` describes.
739    Spawn(SpawnError),
740    /// The node did not acknowledge a requested shutdown within its ack window.
741    /// It is still marked running; the sane escalations are app-level. Its
742    /// `divisible` shares, if any, have been released on its behalf.
743    ShutdownTimeout,
744}
745
746impl core::fmt::Display for NodeFault {
747    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
748        match self.kind {
749            FaultKind::ReadyDepTimeout { dep } => write!(
750                f,
751                "{}: ready-dep {} did not assert within {}ms",
752                self.node.name(),
753                dep.name(),
754                self.node.slot_timeout().as_millis()
755            ),
756            FaultKind::ResourceMissing => {
757                write!(
758                    f,
759                    "{}: a resource slot was never provided",
760                    self.node.name()
761                )
762            }
763            FaultKind::ExecutorSlotEmpty => {
764                write!(
765                    f,
766                    "{}: its executor slot was never filled",
767                    self.node.name()
768                )
769            }
770            FaultKind::Spawn(_) => {
771                write!(
772                    f,
773                    "{}: spawn failed, its task pool is exhausted",
774                    self.node.name()
775                )
776            }
777            FaultKind::ShutdownTimeout => {
778                write!(f, "{}: missed its shutdown ack", self.node.name())
779            }
780        }
781    }
782}
783
784#[cfg(feature = "defmt")]
785impl defmt::Format for NodeFault {
786    fn format(&self, fmt: defmt::Formatter) {
787        match self.kind {
788            FaultKind::ReadyDepTimeout { dep } => defmt::write!(
789                fmt,
790                "{}: ready-dep {} did not assert within {}ms",
791                self.node.name(),
792                dep.name(),
793                self.node.slot_timeout().as_millis()
794            ),
795            FaultKind::ResourceMissing => {
796                defmt::write!(
797                    fmt,
798                    "{}: a resource slot was never provided",
799                    self.node.name()
800                )
801            }
802            FaultKind::ExecutorSlotEmpty => {
803                defmt::write!(
804                    fmt,
805                    "{}: its executor slot was never filled",
806                    self.node.name()
807                )
808            }
809            FaultKind::Spawn(_) => {
810                defmt::write!(
811                    fmt,
812                    "{}: spawn failed, its task pool is exhausted",
813                    self.node.name()
814                )
815            }
816            FaultKind::ShutdownTimeout => {
817                defmt::write!(fmt, "{}: missed its shutdown ack", self.node.name())
818            }
819        }
820    }
821}
822
823/// The shutdown side of [`TaskNode::run_cancellable`]'s result: the raced work
824/// future was cancelled at its await point because a stop/pause request won the
825/// select. Pairs naturally with the `exit:` slot — a worker returning
826/// `Result<R, Aborted>` records completed-vs-cancelled for whoever reads the
827/// exit value.
828#[derive(Clone, Copy, PartialEq, Eq, Debug)]
829pub struct Aborted;
830
831#[cfg(feature = "defmt")]
832impl defmt::Format for Aborted {
833    fn format(&self, fmt: defmt::Formatter) {
834        defmt::write!(fmt, "aborted by shutdown");
835    }
836}
837
838/// The pause side of [`TaskNode::run_pausable`]'s result: a stop/pause request
839/// won the race, the combinator acked and parked, and the supervisor has since
840/// resumed the node. By the time a body sees `Err(Resumed)` the park is already
841/// over — the next loop iteration is the fresh cycle.
842#[derive(Clone, Copy, PartialEq, Eq, Debug)]
843pub struct Resumed;
844
845#[cfg(feature = "defmt")]
846impl defmt::Format for Resumed {
847    fn format(&self, fmt: defmt::Formatter) {
848        defmt::write!(fmt, "resumed after pause");
849    }
850}
851
852pin_project_lite::pin_project! {
853    /// The future behind [`TaskNode::run_cancellable`] and
854    /// [`run_cancellable_acked`](TaskNode::run_cancellable_acked): races the
855    /// worker against the node's shutdown signal, holding the worker's state
856    /// machine **once**.
857    ///
858    /// Written by hand rather than as `select(fut, wait_shutdown()).await` inside
859    /// an `async fn`, because that shape stores the worker both as the function's
860    /// argument and inside the select (rust-lang/rust#62958) — a doubling paid in
861    /// every caller's static task storage, which for a graph of `cancel` nodes is
862    /// the sum of every worker future in the binary.
863    ///
864    /// `fut` is an `Option` so the abort path can drop the worker in place, via
865    /// the safe `Pin::set`, *before* acking: the ack is what releases the
866    /// supervisor's teardown wait, and a runner whose `Drop` frees hardware must
867    /// have run by then.
868    ///
869    /// Shutdown is polled inline off `node` rather than stored as a
870    /// `wait_shutdown()` future: the signal wait is stateless (register-on-poll),
871    /// so embedding its state machine here would spend ~8 bytes of every
872    /// caller's task storage carrying a second copy of the node reference.
873    struct RunCancellable<'a, F> {
874        #[pin]
875        fut: Option<F>,
876        node: &'a TaskNode,
877        // True only for the `_acked` variant.
878        ack: bool,
879    }
880}
881
882impl<F: Future> Future for RunCancellable<'_, F> {
883    type Output = Result<F::Output, Aborted>;
884
885    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
886        let mut this = self.project();
887        // Worker first, shutdown second — the polling order `select` gave this
888        // before, so a worker that completes in the same wake as a stop request
889        // still reports completion rather than abort.
890        if let Some(fut) = this.fut.as_mut().as_pin_mut()
891            && let Poll::Ready(out) = fut.poll(cx)
892        {
893            return Poll::Ready(Ok(out));
894        }
895        // The flag fast path covers a request that predates this future (the
896        // signal is edge-triggered); a fresh `wait()` per poll is sound because
897        // the `Signal` latches its value and re-registers the waker each poll.
898        if this.node.poll_shutdown(cx) {
899            this.fut.set(None);
900            if *this.ack {
901                this.node.ack_dropped();
902            }
903            return Poll::Ready(Err(Aborted));
904        }
905        Poll::Pending
906    }
907}
908
909pin_project_lite::pin_project! {
910    /// The future behind [`TaskNode::run_pausable`]: [`RunCancellable`] with the
911    /// `Pause` protocol's tail folded in. Racing, it behaves as the `_acked`
912    /// variant; on abort it drops the worker in place, acks, and becomes the
913    /// `wait_resume()` park — one state flag, and the worker's state machine is
914    /// still held exactly once (same layout rationale as [`RunCancellable`]).
915    struct RunPausable<'a, F> {
916        #[pin]
917        fut: Option<F>,
918        node: &'a TaskNode,
919        parked: bool,
920    }
921}
922
923impl<F: Future> Future for RunPausable<'_, F> {
924    type Output = Result<F::Output, Resumed>;
925
926    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
927        let mut this = self.project();
928        if !*this.parked {
929            // Same race, same ordering as `RunCancellable`: worker first, so a
930            // completion in the same wake as a stop request still reports
931            // completion; flag fast path, then the latched signal.
932            if let Some(fut) = this.fut.as_mut().as_pin_mut()
933                && let Poll::Ready(out) = fut.poll(cx)
934            {
935                return Poll::Ready(Ok(out));
936            }
937            if !this.node.poll_shutdown(cx) {
938                return Poll::Pending;
939            }
940            this.fut.set(None);
941            this.node.ack_dropped();
942            *this.parked = true;
943            // Fall through to the resume poll in this same call: `resume_node`
944            // is eligible the instant the ack lands, and a resume signaled
945            // before this poll returns would otherwise latch with no waker
946            // registered — a lost wakeup.
947        }
948        if core::pin::pin!(this.node.handle.resume_wake.wait())
949            .poll(cx)
950            .is_ready()
951        {
952            return Poll::Ready(Err(Resumed));
953        }
954        Poll::Pending
955    }
956}
957
958/// How long the supervisor's bring-up waits for a node's `executor:`
959/// [`SpawnerSlot`] to be filled before failing the spawn with
960/// [`FaultKind::ExecutorSlotEmpty`]. A genuine cross-core rendezvous resolves in microseconds;
961/// a slot empty this long is a misconfiguration (the app never registered that
962/// executor's spawner). Bounded, so a misconfigured graph fails loudly instead of
963/// hanging bring-up forever.
964const SLOT_READY_TIMEOUT: embassy_time::Duration = embassy_time::Duration::from_millis(100);
965
966// ─── Mode ────────────────────────────────────────────────────────────────
967
968/// Lifecycle policy for a managed task: what the task does on shutdown and what
969/// the supervisor does to bring it back.
970#[derive(Clone, Copy, PartialEq, Eq, Debug)]
971pub enum Mode {
972    /// Task exits its loop on shutdown. The supervisor respawns it via the
973    /// node's `spawn` fn from `respawn_terminate`.
974    Terminate,
975    /// Task acks shutdown and parks on `wait_resume()`. The supervisor resumes
976    /// it from `resume_pausable`; the task is never respawned, so it keeps any
977    /// resource it holds (a peripheral handle, a socket) across the pause.
978    Pause,
979    /// Like `Terminate` (exits on shutdown), but **not** started at boot and
980    /// **not** auto-respawned. The supervisor brings it up and down at runtime
981    /// via `start_node` / `stop_node` in response to load — see [`ElasticPool`].
982    /// `start()` skips it; `respawn_terminate()` leaves it down (it
983    /// re-grows under demand); `teardown()` only acts on it while it is running.
984    OnDemand,
985}
986
987impl Mode {
988    /// Stable lower-case wire name, used both for serialization (e.g. a JSON
989    /// task-state view) and for `defmt` logging — the single source of these
990    /// strings.
991    pub fn as_str(&self) -> &'static str {
992        match self {
993            Mode::Terminate => "terminate",
994            Mode::Pause => "pause",
995            Mode::OnDemand => "ondemand",
996        }
997    }
998}
999
1000#[cfg(feature = "defmt")]
1001impl defmt::Format for Mode {
1002    fn format(&self, f: defmt::Formatter) {
1003        defmt::write!(f, "{}", self.as_str());
1004    }
1005}
1006
1007/// Renders [`as_str`](Self::as_str), so a `{}` in this crate's log macros reads
1008/// the same whichever backend is compiled in.
1009impl core::fmt::Display for Mode {
1010    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1011        f.write_str(self.as_str())
1012    }
1013}
1014
1015// ─── TaskHandle ──────────────────────────────────────────────────────────
1016
1017/// Bit assignments of [`TaskHandle::flags`].
1018mod flag {
1019    /// Shutdown requested by the supervisor. Cleared by `reset()`.
1020    pub const SHUTDOWN: u16 = 1 << 0;
1021    /// The instance acked the shutdown (a flag, not a count, since every node
1022    /// is single-instance). Cleared by `reset()`.
1023    pub const DROPPED: u16 = 1 << 1;
1024    /// The supervisor has the node spawned and it hasn't exited. Always-on
1025    /// nodes are set by `start()`; `OnDemand` nodes by `start_node()` /
1026    /// `stop_node()`. `teardown()` only acts on running nodes, so a down
1027    /// `OnDemand` node doesn't stall it.
1028    pub const RUNNING: u16 = 1 << 2;
1029    /// Actively serving (`mark_busy()` / `mark_idle()`); read by the scaling
1030    /// policy.
1031    pub const BUSY: u16 = 1 << 3;
1032    /// The task body returned (`mark_exited()`). Cleared by `reset()`.
1033    /// Together with the lifecycle-spanning `SHUTDOWN` this distinguishes an
1034    /// autonomous completion (`COMPLETED && !SHUTDOWN`) from an acked stop.
1035    pub const COMPLETED: u16 = 1 << 4;
1036    /// Manually deactivated via the control interface, or declared `disabled`
1037    /// in the graph. **Lifecycle-spanning**: not cleared by `reset()`, so a
1038    /// manual stop sticks against the automatic bring-up paths until
1039    /// `Supervisor::activate`; living in a `static`, it also survives a
1040    /// RAM-retaining power-state transition.
1041    pub const DISABLED: u16 = 1 << 5;
1042    /// Self-managed: the supervisor never drives this node. Not cleared by
1043    /// `reset()`. Full rationale on `TaskNode::set_detached`.
1044    pub const DETACHED: u16 = 1 << 6;
1045    /// Task-asserted readiness ("initialized and serving") — distinct from
1046    /// `RUNNING` (spawned). Set by `set_ready()`, cleared by `clear_ready()`
1047    /// and by `reset()` so a respawned provider re-asserts.
1048    #[cfg(feature = "readiness")]
1049    pub const READY: u16 = 1 << 7;
1050    /// Stopped by a bound provider's `clear_ready` — or never started, when a
1051    /// bound dep was still un-ready at the end of the bring-up gate budget —
1052    /// and eligible to come back when readiness returns. Deliberately NOT
1053    /// `DISABLED`: a manual stop must survive a readiness flap, and a bound
1054    /// stop must not survive the provider's recovery. Not cleared by `reset()`
1055    /// — it spans the stopped instance's whole absence.
1056    #[cfg(feature = "bound-deps")]
1057    pub const BOUND_STOPPED: u16 = 1 << 8;
1058    /// Stopped as a dependent of a deactivated node. `deactivate` marks only
1059    /// its seed as `DISABLED`; dependents are marked `COLLATERAL` instead.
1060    /// Both block automatic bring-up, but `activate` clears `COLLATERAL` once
1061    /// no disabled node remains in the node's transitive dependencies. A manual
1062    /// `start_node` also clears it. Unlike `BOUND_STOPPED`, a readiness flap
1063    /// does not release a control stop. Lifecycle-spanning: not cleared by `reset()`.
1064    pub const COLLATERAL: u16 = 1 << 9;
1065    /// `fault-inject`: swallowed ack pending replay.
1066    #[cfg(feature = "fault-inject")]
1067    pub const PENDING_ACK: u16 = 1 << 10;
1068    /// `fault-inject`: swallowed exit pending replay.
1069    #[cfg(feature = "fault-inject")]
1070    pub const PENDING_EXIT: u16 = 1 << 11;
1071}
1072
1073/// Coordination state for one task. Embedded inside [`TaskNode`].
1074///
1075/// Every node is single-instance, so the state is one word of per-node flags
1076/// plus single-consumer signals — no counts, no fan-out. Written by one side
1077/// (task or supervisor) and read by the other: the supervisor requests exit
1078/// (`SHUTDOWN` + `shutdown_wake`), the task acks it (`DROPPED` +
1079/// `dropped_wake`), a parked Pause-mode task resumes on `resume_wake`, and
1080/// the scaling policy reads `RUNNING`/`BUSY`. See the private `flag` module
1081/// for each bit.
1082pub struct TaskHandle {
1083    /// The lifecycle flags, one bit each (see [`flag`]) — one atomic word
1084    /// where separate booleans would pad the handle. Writes pair `Release`
1085    /// with `Acquire` reads, per flag.
1086    flags: AtomicU16,
1087    /// Wake source for `wait_shutdown()`. Fired by `signal_shutdown()`.
1088    shutdown_wake: Signal<CriticalSectionRawMutex, ()>,
1089    /// Wake source for `wait_dropped()`. Fired by `ack_dropped()`.
1090    dropped_wake: Signal<CriticalSectionRawMutex, ()>,
1091    /// Wake source for `wait_resume()` on Pause-mode tasks. Fired by
1092    /// `signal_resume()`.
1093    resume_wake: Signal<CriticalSectionRawMutex, ()>,
1094    /// Wake source for `wait_ready()`. Latching; the supervisor's bring-up is
1095    /// the only pre-fill waiter (single-waiter Signal semantics).
1096    #[cfg(feature = "readiness")]
1097    ready_wake: Signal<CriticalSectionRawMutex, ()>,
1098    /// Instant ticks (truncated) of the last `beat()`; also stamped by
1099    /// `set_running(true)` so a freshly spawned node is never instantly stale.
1100    #[cfg(feature = "liveness")]
1101    last_beat: AtomicU32,
1102    /// Activation generation, bumped on every false→true `running` transition.
1103    /// Starts at 0 ("never activated"); the first spawn makes it 1. Wrapping —
1104    /// dependents compare for *inequality*, never ordering.
1105    #[cfg(feature = "epochs")]
1106    epoch: AtomicU32,
1107    /// Wake source for `wait_epoch_change()`. Single-waiter Signal, like
1108    /// `ready_wake`; the multi-consumer path is polling `epoch()`.
1109    #[cfg(feature = "epochs")]
1110    epoch_wake: Signal<CriticalSectionRawMutex, ()>,
1111    /// Consecutive monitor sweeps that found this node stale. Reset to 0 by any
1112    /// sweep that finds it beating. Saturates at `beat_window`: once the event
1113    /// has been emitted the count only needs to hold the threshold, so a node
1114    /// stale for hours cannot wrap back around and re-report.
1115    #[cfg(feature = "liveness-monitor")]
1116    stale_strikes: AtomicU8,
1117    /// Wrapping sum of this node's beat-feeding `observed` write counters as
1118    /// of the last sweep. One word per node rather than one per entry, which
1119    /// is why a `beat` entry's token must be a counter — a documented
1120    /// requirement on [`Observer`], since value-tokens could cancel in the sum.
1121    #[cfg(all(feature = "coupling-observe", feature = "liveness"))]
1122    write_mark: AtomicU32,
1123    /// The node beat since anyone last looked;
1124    /// [`TaskNode::ticks_since_beat`] converts it into a timestamp with the
1125    /// `now` it reads anyway. One relaxed store per beat instead of a timer
1126    /// read — the write-rate decimation, with the clock cost on the checker,
1127    /// and the reason [`TaskNode::beat`] is cheap enough to call per message.
1128    #[cfg(feature = "liveness")]
1129    pending_beat: AtomicBool,
1130    /// The node's self-description (`report_status`), shown when asked and
1131    /// never acted on. A mutexed cell because a `&'static str` is two words —
1132    /// too wide to swap atomically.
1133    #[cfg(feature = "node-status")]
1134    status: BlockingMutex<CriticalSectionRawMutex, Cell<Option<&'static str>>>,
1135    /// Encoded current fault.
1136    #[cfg(feature = "fault-inject")]
1137    fault: AtomicU8,
1138    /// Hog spin duration in milliseconds.
1139    #[cfg(feature = "fault-inject")]
1140    hog_ms: AtomicU32,
1141    /// Wakes the task on inject or clear.
1142    #[cfg(feature = "fault-inject")]
1143    fault_waker: embassy_sync::waitqueue::AtomicWaker,
1144    /// The executor task id currently running this node (`TaskRef::id()`, captured
1145    /// from the `SpawnToken` by the macro's spawn glue). `0` = unknown (not yet
1146    /// spawned, or a parked/closure-spawned node that never registered). Overwritten
1147    /// on every (re)spawn, so — unlike an external tracker — it stays correct across
1148    /// respawns without any unlinking.
1149    #[cfg(feature = "trace")]
1150    task_id: AtomicU32,
1151    /// Accumulated executor-poll time for this node, in embassy-time ticks,
1152    /// wrapping. Consumers sample twice and `wrapping_sub` to get a rate; the
1153    /// crate does no windowing.
1154    #[cfg(feature = "trace")]
1155    exec_ticks: AtomicU32,
1156    /// Number of executor polls of this node, wrapping.
1157    #[cfg(feature = "trace")]
1158    polls: AtomicU32,
1159    /// Longest single poll ever observed, in ticks — the "never yields" watermark.
1160    /// A large value names the node that hogged the executor even after the fact,
1161    /// which a live check cannot do from the blocked executor itself.
1162    #[cfg(feature = "trace")]
1163    max_poll_ticks: AtomicU32,
1164}
1165
1166impl TaskHandle {
1167    const fn new(disabled_at_boot: bool) -> Self {
1168        Self {
1169            flags: AtomicU16::new(if disabled_at_boot { flag::DISABLED } else { 0 }),
1170            shutdown_wake: Signal::new(),
1171            dropped_wake: Signal::new(),
1172            resume_wake: Signal::new(),
1173            #[cfg(feature = "readiness")]
1174            ready_wake: Signal::new(),
1175            #[cfg(feature = "liveness")]
1176            last_beat: AtomicU32::new(0),
1177            #[cfg(feature = "epochs")]
1178            epoch: AtomicU32::new(0),
1179            #[cfg(feature = "epochs")]
1180            epoch_wake: Signal::new(),
1181            #[cfg(feature = "liveness-monitor")]
1182            stale_strikes: AtomicU8::new(0),
1183            #[cfg(all(feature = "coupling-observe", feature = "liveness"))]
1184            write_mark: AtomicU32::new(0),
1185            #[cfg(feature = "liveness")]
1186            pending_beat: AtomicBool::new(false),
1187            #[cfg(feature = "node-status")]
1188            status: BlockingMutex::new(Cell::new(None)),
1189            #[cfg(feature = "fault-inject")]
1190            fault: AtomicU8::new(0),
1191            #[cfg(feature = "fault-inject")]
1192            hog_ms: AtomicU32::new(0),
1193            #[cfg(feature = "fault-inject")]
1194            fault_waker: embassy_sync::waitqueue::AtomicWaker::new(),
1195            #[cfg(feature = "trace")]
1196            task_id: AtomicU32::new(0),
1197            #[cfg(feature = "trace")]
1198            exec_ticks: AtomicU32::new(0),
1199            #[cfg(feature = "trace")]
1200            polls: AtomicU32::new(0),
1201            #[cfg(feature = "trace")]
1202            max_poll_ticks: AtomicU32::new(0),
1203        }
1204    }
1205
1206    fn flag(&self, bit: u16) -> bool {
1207        self.flags.load(Ordering::Acquire) & bit != 0
1208    }
1209
1210    fn flag_set(&self, bit: u16) {
1211        self.flags.fetch_or(bit, Ordering::Release);
1212    }
1213
1214    /// Clear `bits` — one bit or a whole mask, as `reset()` uses it.
1215    fn flag_clear(&self, bits: u16) {
1216        self.flags.fetch_and(!bits, Ordering::Release);
1217    }
1218
1219    fn flag_put(&self, bit: u16, on: bool) {
1220        if on {
1221            self.flag_set(bit);
1222        } else {
1223            self.flag_clear(bit);
1224        }
1225    }
1226
1227    /// Set or clear `bit` and report whether it was set before — the
1228    /// transition test `mark_busy` / `mark_idle` signal scaling on.
1229    fn flag_swap(&self, bit: u16, on: bool) -> bool {
1230        let prior = if on {
1231            self.flags.fetch_or(bit, Ordering::Release)
1232        } else {
1233            self.flags.fetch_and(!bit, Ordering::Release)
1234        };
1235        prior & bit != 0
1236    }
1237}
1238
1239// ─── Executor spawner slots ──────────────────────────────────────────────
1240
1241/// Runtime-filled slot holding a foreign executor's [`SendSpawner`].
1242///
1243/// Declared by `executor NAME;` and filled by the app before or during
1244/// [`Supervisor::start`]. The supervisor waits on [`ready`](Self::ready)
1245/// before spawning a node into it; if the slot is still empty after the
1246/// bounded wait, the spawn fails with [`FaultKind::ExecutorSlotEmpty`].
1247///
1248/// `default executor NAME;` makes `NAME` the default for nodes that do not
1249/// specify an executor. This lets a supervisor running on an interrupt tier
1250/// keep most of its graph in thread mode:
1251///
1252/// ```ignore
1253/// supervisor_graph! {
1254///     default executor THREAD;
1255///     executor HIGH;
1256///     node LOGGER  = Terminate, task: logger_worker;
1257///     node SAMPLER = Terminate, executor: HIGH, task: sampler_worker;
1258/// }
1259/// THREAD.set(spawner.make_send());
1260/// ```
1261///
1262/// `Send` is required on the spawn arguments, not the future. A `task:` shell's
1263/// arguments are always `Send`, so any `task:` worker can be routed to any tier.
1264/// A `spawn:` fn's own arguments must be `Send`, so this does not compile:
1265///
1266/// ```compile_fail
1267/// use std::rc::Rc;
1268/// use embassy_supervisor::{TaskNode, supervisor_graph};
1269///
1270/// #[embassy_executor::task]
1271/// async fn worker(_node: &'static TaskNode, _handle: Rc<u32>) {}
1272///
1273/// supervisor_graph! {
1274///     executor HIGH;
1275///     node A = Terminate, deps: [], executor: HIGH, spawn: worker(Rc::new(1));
1276/// }
1277/// ```
1278///
1279/// What the worker then touches from that tier is the author's contract.
1280pub struct SpawnerSlot {
1281    slot: BlockingMutex<CriticalSectionRawMutex, Cell<Option<SendSpawner>>>,
1282    /// Wakes a `ready()` waiter when `set` fills the slot (cross-core safe:
1283    /// `Signal` is critical-section based and latches).
1284    filled: Signal<CriticalSectionRawMutex, ()>,
1285}
1286
1287impl SpawnerSlot {
1288    /// An empty slot (`const` — it lives in a `static` the macro emits).
1289    pub const fn new() -> Self {
1290        Self {
1291            slot: BlockingMutex::new(Cell::new(None)),
1292            filled: Signal::new(),
1293        }
1294    }
1295
1296    /// Fill the slot (last set wins) and wake a [`ready`](Self::ready) waiter.
1297    /// Call before [`Supervisor::start`] — or from the other core's bring-up,
1298    /// with the supervisor awaiting `ready()`.
1299    pub fn set(&self, spawner: SendSpawner) {
1300        self.slot.lock(|c| c.set(Some(spawner)));
1301        self.filled.signal(());
1302        __sv_gate_event();
1303    }
1304
1305    /// The registered spawner, or `None` while unfilled.
1306    pub fn get(&self) -> Option<SendSpawner> {
1307        self.slot.lock(Cell::get)
1308    }
1309
1310    /// Await the slot and return the spawner. The rendezvous primitive: the
1311    /// supervisor's bring-up awaits this for a node's `executor:` slot before
1312    /// spawning it (bounded, see [`Supervisor::start`]), so a tier filled late — or
1313    /// from another core — is handled without a race. Returns immediately once the
1314    /// slot is filled, so any number of *late* callers are fine (an application can
1315    /// gate work on the executor being up). While the slot is still empty, at most
1316    /// one task should be parked here: the underlying `Signal` holds a single waker,
1317    /// so a second pre-fill waiter would displace the first.
1318    pub async fn ready(&self) -> SendSpawner {
1319        loop {
1320            if let Some(sp) = self.get() {
1321                return sp;
1322            }
1323            // `Signal` latches: a `set()` racing between the check above and
1324            // this wait still wakes us.
1325            self.filled.wait().await;
1326        }
1327    }
1328}
1329
1330impl Default for SpawnerSlot {
1331    fn default() -> Self {
1332        Self::new()
1333    }
1334}
1335
1336// ─── ResourceSlot ────────────────────────────────────────────────────────
1337
1338/// Type-erased readiness view of a [`ResourceSlot`], for the supervisor's
1339/// bring-up wait.
1340///
1341/// A `TaskNode` can gate on any number of slots of *different* `T`s, so the node
1342/// stores `&'static [&'static dyn ResourceGate]` (object-safe: no `T` in the
1343/// signatures). Same shape as embassy's `dyn` driver registries — see
1344/// <https://doc.rust-lang.org/reference/items/traits.html#object-safety>.
1345/// The supervisor only needs "is it filled?" plus the signal to park on; taking
1346/// the value stays in the generated spawn glue, where the concrete `T` is known.
1347pub trait ResourceGate: Sync {
1348    /// Non-consuming "is the slot currently filled" check.
1349    fn is_filled(&self) -> bool;
1350    /// The latching [`Signal`] fired by `provide`/`restore`, for the supervisor's
1351    /// bounded pre-spawn wait (see [`Supervisor::start`]).
1352    fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()>;
1353    /// Empty the slot, dropping any held value — how a stopping provider's
1354    /// `provides:` list is cleared, so a value that dies with its producer
1355    /// reads empty until the next activation re-provides. Default no-op, for a
1356    /// gate view with nothing to clear.
1357    fn clear(&self) {}
1358}
1359
1360/// A one-value handoff cell threading an owned resource from `main` into a
1361/// supervised task — the safe replacement for `Peripherals::steal()` inside
1362/// the task body.
1363///
1364/// Declared (as a `pub static`) by [`supervisor_graph!`] for each entry in a
1365/// node's `resources:` clause. The protocol:
1366///
1367/// 1. `main` splits `Peripherals` and **moves** the resource in with
1368///    [`provide`](Self::provide). This is where the compile-time guarantee
1369///    lives: the singleton field is *consumed*, so no second owner — and no
1370///    `unsafe` steal — can exist.
1371/// 2. The bring-up awaits the slot being filled, then the generated task
1372///    shell [`take`](Self::take)s it — inside the spawned task, so a value
1373///    never crosses the spawn call. A slot still empty at the gate deadline
1374///    fails the spawn with [`FaultKind::ResourceMissing`] — a fail-closed
1375///    error out of [`Supervisor::start`], not a panic inside the task
1376///    (compare `static_cell::StaticCell`, which panics on misuse).
1377/// 3. The generated task shell hands the worker `&mut T` and
1378///    [`restore`](Self::restore)s the value after the worker returns, so a
1379///    `Terminate` respawn re-takes the *same instance* instead of stealing a
1380///    fresh one. (A `Pause` worker never returns — it parks — so it simply
1381///    retains the resource, exactly like a hand-written parked task.)
1382///
1383/// Same primitives as [`SpawnerSlot`]: a critical-section
1384/// [`BlockingMutex`]`<`[`Cell`]`<Option<T>>>` for the value (`Sync` for
1385/// `T: Send`, provided by embassy-sync — no `unsafe` here) plus a latching
1386/// [`Signal`] so the supervisor can await late provisioning (bounded; see
1387/// [`Supervisor::start`]).
1388pub struct ResourceSlot<T> {
1389    slot: BlockingMutex<CriticalSectionRawMutex, Cell<Option<T>>>,
1390    /// Wakes the supervisor's pre-spawn wait when `provide`/`restore` fills the
1391    /// slot (latching, so a fill racing the check-then-wait still wakes it).
1392    filled: Signal<CriticalSectionRawMutex, ()>,
1393}
1394
1395impl<T> ResourceSlot<T> {
1396    /// An empty slot (`const` — it lives in a `static` the macro emits).
1397    pub const fn new() -> Self {
1398        Self {
1399            slot: BlockingMutex::new(Cell::new(None)),
1400            filled: Signal::new(),
1401        }
1402    }
1403
1404    /// Move the resource in (from `main`'s `Peripherals` split) and wake the
1405    /// supervisor's pre-spawn wait. Call before [`Supervisor::start`]; a slot
1406    /// still empty after the supervisor's bounded wait fails that node's spawn
1407    /// with `FaultKind::ExecutorSlotEmpty`. Filling an occupied slot replaces (drops) the
1408    /// old value — don't: one resource, one slot, moved exactly once.
1409    pub fn provide(&self, value: T) {
1410        self.slot.lock(|c| c.set(Some(value)));
1411        self.filled.signal(());
1412        __sv_gate_event();
1413    }
1414
1415    /// Take the resource out, leaving the slot empty. Called by the generated
1416    /// task shell at its first poll; `None` means "not provided yet" or
1417    /// "currently held by a live task instance".
1418    pub fn take(&self) -> Option<T> {
1419        self.slot.lock(Cell::take)
1420    }
1421
1422    /// Copy the resource out **without emptying the slot** — the `shared`
1423    /// resource kind's read: any number of consumers (several nodes, a whole
1424    /// pool) get the same `Copy` handle, and the slot stays filled for the
1425    /// next one. Only for `T: Copy` (a `Stack`-like handle, a `&'static`
1426    /// registry ref); an owned singleton uses [`take`](Self::take).
1427    pub fn get(&self) -> Option<T>
1428    where
1429        T: Copy,
1430    {
1431        // Same peek shape as `is_filled`: `Cell` has no `&T` access, so
1432        // take-copy-put-back under one critical section.
1433        self.slot.lock(|c| {
1434            let v = c.take();
1435            c.set(v);
1436            v
1437        })
1438    }
1439
1440    /// Put the resource back for the next spawn. Called by the generated task
1441    /// shell after the worker returns (i.e. after its clean shutdown ack), so a
1442    /// respawn re-takes the same instance.
1443    pub fn restore(&self, value: T) {
1444        self.provide(value);
1445    }
1446
1447    /// Empty the slot, dropping any held value, and reset the latched filled
1448    /// signal. The provider side of the freshness convention: a task that
1449    /// rebuilds this slot's value each activation clears it on the way down —
1450    /// the `provides:` clause does it from the shutdown ack — so a consumer's
1451    /// gate wait holds for the next activation's value instead of taking this
1452    /// one's leftover.
1453    pub fn clear(&self) {
1454        let stale = self.slot.lock(Cell::take);
1455        drop(stale);
1456        self.filled.reset();
1457    }
1458
1459    /// Await the slot being filled, then take the value — how an application
1460    /// reads a node's `exit:` slot (the shell `provide()`s the worker's return
1461    /// value there just before recording the exit). Check-then-park, so a value
1462    /// provided earlier is returned immediately; the latching signal carries
1463    /// the same single-pre-fill-waiter caveat as [`SpawnerSlot::ready`] — for
1464    /// N concurrent readers fan out through an app-owned `Watch` instead.
1465    pub async fn wait_take(&self) -> T {
1466        loop {
1467            if let Some(v) = self.take() {
1468                return v;
1469            }
1470            self.filled.wait().await;
1471        }
1472    }
1473}
1474
1475// `T: Send` (not just any `T`): the gate is reachable from the supervisor task,
1476// which may run on a different core than the provider — the same bound the
1477// inner `BlockingMutex` requires for `Sync`, restated here so the `dyn` upcast
1478// can't outrun it.
1479impl<T: Send> ResourceGate for ResourceSlot<T> {
1480    fn is_filled(&self) -> bool {
1481        // Peek without consuming: `Cell` has no `&T` access (no `T: Copy`
1482        // here), so take-and-put-back under the same critical section.
1483        self.slot.lock(|c| {
1484            let v = c.take();
1485            let filled = v.is_some();
1486            c.set(v);
1487            filled
1488        })
1489    }
1490
1491    fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()> {
1492        &self.filled
1493    }
1494
1495    fn clear(&self) {
1496        ResourceSlot::clear(self);
1497    }
1498}
1499
1500impl<T> Default for ResourceSlot<T> {
1501    fn default() -> Self {
1502        Self::new()
1503    }
1504}
1505
1506// ─── TaskNode ────────────────────────────────────────────────────────────
1507
1508/// A node in the supervisor's task graph.
1509///
1510/// Designed to live in `static` memory: every field is `Sync`, all constructors
1511/// are `const`. Declared by [`supervisor_graph!`], which emits one per managed
1512/// task along with the [`Graph`] (`GRAPH`) that [`Supervisor::new`] consumes.
1513///
1514/// Split in two on purpose. The handle's atomics force this static into RAM —
1515/// and would force everything beside them into RAM too — so the node holds
1516/// only its live state plus one reference to its [`NodeCfg`], the immutable
1517/// half (name, mode, spawn fn, gates, budgets, coupling tables), which has no
1518/// interior mutability and therefore stays in flash. Same lesson as the
1519/// graph's [`Topology`], one level down: keep the constant data out of reach
1520/// of the atomics.
1521pub struct TaskNode {
1522    /// The immutable half — flash-resident, emitted as its own `static` by
1523    /// [`supervisor_graph!`] beside the node.
1524    cfg: &'static NodeCfg,
1525    handle: TaskHandle,
1526}
1527
1528/// The immutable half of a [`TaskNode`]: everything the graph *declared* about
1529/// the node, none of what happens to it at runtime. No interior mutability, so
1530/// the `static` carrying it lives in flash (`.rodata`); the RAM-resident node
1531/// points at it. Built `const` with [`new`](Self::new) plus the chainable
1532/// `with_*` methods, exactly as [`supervisor_graph!`] emits it.
1533pub struct NodeCfg {
1534    /// Human-readable name. Used in defmt logs and panic messages.
1535    pub name: &'static str,
1536    /// Lifecycle policy. See [`Mode`].
1537    pub mode: Mode,
1538    /// App-provided spawn function (typically an inline closure at the node's
1539    /// declaration). Called once at boot from `Supervisor::start`, again from
1540    /// `respawn_terminate` for Terminate nodes, and at runtime from `start_node`
1541    /// for `OnDemand` nodes. `None` for a **parked** node the application spawns
1542    /// itself (e.g. a `Pause` sensor holding a peripheral handle): the supervisor
1543    /// tracks its lifecycle but never spawns it.
1544    pub spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
1545    /// The executor [`SpawnerSlot`] this node spawns through (`executor: NAME` in
1546    /// the graph), or `None` to spawn on the supervisor's own `Spawner`. When
1547    /// `Some`, the supervisor holds the spawn until the slot is filled
1548    /// (bounded by [`slot_timeout`](TaskNode::slot_timeout)), so the
1549    /// generated glue's own non-blocking `SpawnerSlot::get` is already filled. Set
1550    /// by the macro via [`with_executor`](Self::with_executor); `const`, zero-cost.
1551    spawn_slot: Option<&'static SpawnerSlot>,
1552    /// The [`ResourceSlot`]s this node's spawn takes from (`resources:` in the
1553    /// graph), type-erased to their [`ResourceGate`] readiness view. The
1554    /// supervisor holds the spawn until every gate is filled (bounded by
1555    /// [`slot_timeout`](TaskNode::slot_timeout)), so (a) a `main` that
1556    /// provides late is tolerated and (b) a respawn cannot race the previous
1557    /// instance's shell restoring the value (the restore happens after the
1558    /// worker's shutdown ack). Empty for nodes without `resources:`. Set by the
1559    /// macro via [`with_resources`](Self::with_resources); `const`, zero-cost.
1560    resource_gates: &'static [&'static dyn ResourceGate],
1561    /// The slots this node's task fills at runtime (`provides:` in the graph),
1562    /// cleared when the node acknowledges a stop so a consumer's gate wait
1563    /// sees the value's absence rather than a previous activation's leftover.
1564    /// A `Pause` ack is exempt: the parked task still backs what it published.
1565    provides: &'static [&'static dyn ResourceGate],
1566    /// `divisible` budget slots this node claims (`resources:`). Released on
1567    /// shutdown, or by the supervisor if the ack times out, so a dead holder
1568    /// does not strand its share. A `Pause` ack keeps the claim.
1569    #[cfg(feature = "budget")]
1570    claims: &'static [(&'static dyn Divisible, u8)],
1571    /// Deps whose task-asserted readiness (`set_ready`) bring-up awaits before
1572    /// spawning this node — the `ready`-marked subset of `deps:`. Spawn-order
1573    /// deps stay in the graph's dep table; this is the readiness overlay.
1574    #[cfg(feature = "readiness")]
1575    ready_deps: &'static [&'static TaskNode],
1576    /// Bound on the pre-spawn waits for this node's `executor:` slot and
1577    /// `resources:` gates. Defaults to [`SLOT_READY_TIMEOUT`] (100 ms — sized
1578    /// for "main provided before start"); raise it (`slot_timeout:` in the
1579    /// graph) for a node whose slots are filled by a **provider node** at
1580    /// runtime — e.g. an async radio bring-up worth hundreds of milliseconds.
1581    /// Set by the macro via [`with_slot_timeout`](Self::with_slot_timeout).
1582    slot_timeout: embassy_time::Duration,
1583    /// How long a stop waits for this node's shutdown ack before faulting it
1584    /// (`ack_timeout:` in the graph). Defaults to [`SHUTDOWN_ACK_TIMEOUT_MS`]
1585    /// (2 s); raise it for a node whose cleanup legitimately takes longer —
1586    /// a flash sync, a peripheral settle. Set by the macro via
1587    /// [`with_ack_timeout`](Self::with_ack_timeout).
1588    ack_timeout: embassy_time::Duration,
1589    /// How long this node may go without a `beat()` before the monitor calls it
1590    /// stale (`beat_timeout:` in the graph). `None` — the default — opts the
1591    /// node OUT of policing entirely, which is right for every node whose body
1592    /// does not beat: it would otherwise read permanently stale.
1593    #[cfg(feature = "liveness-monitor")]
1594    beat_timeout: Option<embassy_time::Duration>,
1595    /// How many *consecutive* stale sweeps are needed before the monitor emits
1596    /// (`beat_window:` in the graph). 1 (the default) reports the first miss;
1597    /// raise it to tolerate a node whose beat interval is legitimately jittery.
1598    #[cfg(feature = "liveness-monitor")]
1599    beat_window: u8,
1600    /// `ready_on_write` in the graph: the sweep calls [`TaskNode::set_ready`]
1601    /// the first time an `observed` write advances, instead of the task
1602    /// asserting readiness itself.
1603    #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
1604    ready_on_write: bool,
1605    /// The signals this node consumes, as one table per source — the
1606    /// `reads:` list, a `discover` node's derived table, each adopted
1607    /// `dataflow:` fn's table. Read by the signal-indexed queries and the
1608    /// diagram tool; never by the spawn machinery.
1609    #[cfg(feature = "coupling")]
1610    reads: &'static [&'static [Coupling]],
1611    /// The write-side tables. An `observed` entry here is additionally polled
1612    /// by [`Supervisor::monitor`], which turns an advance into a beat.
1613    #[cfg(feature = "coupling")]
1614    writes: &'static [&'static [Coupling]],
1615    /// The `bound`-marked subset of `deps:` — providers whose readiness
1616    /// *controls* this node rather than merely gating its first spawn.
1617    #[cfg(feature = "bound-deps")]
1618    bound_deps: &'static [&'static TaskNode],
1619    /// The graph this node belongs to — its view of its own peers, and what a
1620    /// data-driven dependency resolves a producer through. Set by
1621    /// [`supervisor_graph!`]; the graph names the nodes and each node names the
1622    /// graph, which is a cycle only in the address sense and so is a perfectly
1623    /// ordinary pair of statics.
1624    ///
1625    /// [`NO_GRAPH`](graph_ref::NO_GRAPH) for a hand-built node that belongs to
1626    /// no graph, which simply has no peers to answer about.
1627    #[cfg(feature = "data-deps")]
1628    graph: &'static GraphRef,
1629    /// `fault-inject`: true if the node uses a `task:` shell wrapped by [`Injected`].
1630    #[cfg(feature = "fault-inject")]
1631    shelled: bool,
1632}
1633
1634impl NodeCfg {
1635    /// The declared side of a single-instance node started at boot
1636    /// (`Terminate`/`Pause`) or on demand (`Mode::OnDemand`). Every node is
1637    /// single-instance; an elastic service is modelled as several `OnDemand`
1638    /// nodes of the same pooled task fn.
1639    ///
1640    /// A node carries only its own identity and behaviour; the graph's
1641    /// dependency edges live in the compile-time index table that
1642    /// [`supervisor_graph!`] emits and [`Supervisor::new`] consumes.
1643    /// `spawn` is `None` for a parked node the application spawns itself.
1644    pub const fn new(
1645        name: &'static str,
1646        mode: Mode,
1647        spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
1648    ) -> Self {
1649        Self {
1650            name,
1651            mode,
1652            spawn,
1653            spawn_slot: None,
1654            resource_gates: &[],
1655            provides: &[],
1656            #[cfg(feature = "budget")]
1657            claims: &[],
1658            #[cfg(feature = "readiness")]
1659            ready_deps: &[],
1660            slot_timeout: SLOT_READY_TIMEOUT,
1661            ack_timeout: embassy_time::Duration::from_millis(SHUTDOWN_ACK_TIMEOUT_MS),
1662            #[cfg(feature = "liveness-monitor")]
1663            beat_timeout: None,
1664            #[cfg(feature = "liveness-monitor")]
1665            beat_window: 1,
1666            #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
1667            ready_on_write: false,
1668            #[cfg(feature = "coupling")]
1669            reads: &[],
1670            #[cfg(feature = "coupling")]
1671            writes: &[],
1672            #[cfg(feature = "bound-deps")]
1673            bound_deps: &[],
1674            #[cfg(feature = "data-deps")]
1675            graph: &graph_ref::NO_GRAPH,
1676            #[cfg(feature = "fault-inject")]
1677            shelled: false,
1678        }
1679    }
1680
1681    /// Mark the node as a `task:` shell. Only shells can be stalled, crashed or hogged.
1682    #[cfg(feature = "fault-inject")]
1683    pub const fn with_shell(mut self) -> Self {
1684        self.shelled = true;
1685        self
1686    }
1687
1688    /// Route this node's spawn through the given executor [`SpawnerSlot`] (the
1689    /// `executor: NAME` graph annotation). The supervisor awaits the slot before
1690    /// spawning the node, so a tier filled late — or from another core — is handled
1691    /// without a race, and the generated glue's non-blocking `get` is already filled.
1692    /// `const` and chainable in a `static` initializer; emitted by [`supervisor_graph!`].
1693    pub const fn with_executor(mut self, slot: &'static SpawnerSlot) -> Self {
1694        self.spawn_slot = Some(slot);
1695        self
1696    }
1697
1698    /// Declare the [`ResourceSlot`]s this node's spawn takes from (the
1699    /// `resources:` graph clause). The supervisor awaits every gate being
1700    /// filled before spawning the node, so the generated glue's non-blocking
1701    /// `take()` finds the value. `const` and chainable in a `static`
1702    /// initializer; emitted by [`supervisor_graph!`].
1703    pub const fn with_resources(mut self, gates: &'static [&'static dyn ResourceGate]) -> Self {
1704        self.resource_gates = gates;
1705        self
1706    }
1707
1708    /// Declare the slots this node's task fills at runtime (the `provides:`
1709    /// graph clause); a stop ack clears them. `const` and chainable in a
1710    /// `static` initializer; emitted by [`supervisor_graph!`].
1711    pub const fn with_provides(mut self, slots: &'static [&'static dyn ResourceGate]) -> Self {
1712        self.provides = slots;
1713        self
1714    }
1715
1716    /// Declare the `divisible` budget slots this node claims (`resources:`).
1717    /// Released on stop. `const` and chainable; emitted by [`supervisor_graph!`].
1718    #[cfg(feature = "budget")]
1719    pub const fn with_claims(mut self, claims: &'static [(&'static dyn Divisible, u8)]) -> Self {
1720        self.claims = claims;
1721        self
1722    }
1723
1724    /// Declare the deps whose task-asserted readiness bring-up awaits before
1725    /// spawning this node (the `ready`-marked subset of `deps:`). `const` and
1726    /// chainable in a `static` initializer; emitted by [`supervisor_graph!`].
1727    #[cfg(feature = "readiness")]
1728    pub const fn with_ready_deps(mut self, deps: &'static [&'static TaskNode]) -> Self {
1729        self.ready_deps = deps;
1730        self
1731    }
1732
1733    /// Override the pre-spawn slot/gate wait bound for this node (the
1734    /// `slot_timeout: <millis>` graph clause). The default
1735    /// (`SLOT_READY_TIMEOUT`, 100 ms) assumes slots are provided *before*
1736    /// `start()`; a node consuming a **provider node's** outputs must cover the
1737    /// provider's async build time (the failure mode stays a loud
1738    /// `NodeFault`, just later). `const` and chainable in a `static`
1739    /// initializer; emitted by [`supervisor_graph!`].
1740    pub const fn with_slot_timeout(mut self, timeout: embassy_time::Duration) -> Self {
1741        self.slot_timeout = timeout;
1742        self
1743    }
1744
1745    /// Override how long a stop waits for this node's shutdown ack before
1746    /// faulting it with [`FaultKind::ShutdownTimeout`] (the
1747    /// `ack_timeout: <millis>` graph clause, default 2 s). Raise it for a node
1748    /// whose cleanup legitimately outlasts the default — a flash sync, a
1749    /// peripheral settle; the missed-ack failure mode stays a loud
1750    /// [`NodeFault`], just later. `const` and chainable in a `static`
1751    /// initializer; emitted by [`supervisor_graph!`].
1752    pub const fn with_ack_timeout(mut self, timeout: embassy_time::Duration) -> Self {
1753        self.ack_timeout = timeout;
1754        self
1755    }
1756
1757    /// Opt this node into liveness policing (the `beat_timeout: <millis>` graph
1758    /// clause): [`Supervisor::monitor`] reports it once it has been running
1759    /// without a [`beat`](TaskNode::beat) for longer than `timeout`.
1760    ///
1761    /// Only declare this on a node whose body actually beats — an un-beating
1762    /// node reads permanently stale. `const` and chainable in a `static`
1763    /// initializer; emitted by [`supervisor_graph!`].
1764    #[cfg(feature = "liveness-monitor")]
1765    pub const fn with_beat_timeout(mut self, timeout: embassy_time::Duration) -> Self {
1766        self.beat_timeout = Some(timeout);
1767        self
1768    }
1769
1770    /// How many consecutive stale sweeps the monitor requires before it reports
1771    /// this node (the `beat_window: <n>` graph clause, default 1). Raise it for
1772    /// a node whose beat interval is legitimately jittery — the effective
1773    /// grace period becomes roughly `beat_timeout` + `n` sweep periods.
1774    ///
1775    /// `0` is treated as `1`. `const` and chainable in a `static` initializer;
1776    /// emitted by [`supervisor_graph!`].
1777    #[cfg(feature = "liveness-monitor")]
1778    pub const fn with_beat_window(mut self, sweeps: u8) -> Self {
1779        self.beat_window = if sweeps == 0 { 1 } else { sweeps };
1780        self
1781    }
1782
1783    /// Let an observed write assert readiness (the `ready_on_write` graph
1784    /// clause).
1785    ///
1786    /// The sweep calls [`set_ready`](TaskNode::set_ready) the first time one of this
1787    /// node's `observed` writes advances, so "ready" means "actually producing"
1788    /// rather than "reached the line where it says so". Requires
1789    /// `beat_timeout:`, which is what puts the node in the sweep at all.
1790    ///
1791    /// Monotone by design: it never withdraws readiness. A node that goes quiet
1792    /// is reported through [`wait_health`], and what to do about that stays the
1793    /// application's decision.
1794    #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
1795    pub const fn with_ready_on_write(mut self) -> Self {
1796        self.ready_on_write = true;
1797        self
1798    }
1799
1800    /// Declare the signals this node consumes (the `reads:` graph clause).
1801    /// Purely descriptive: the supervisor never gates on it, and reads carry
1802    /// neither heartbeat nor readiness. What it buys is a graph that says what
1803    /// the node consumes — to [`Graph::readers_of`], to the diagram tool.
1804    /// `const` and chainable
1805    /// in a `static` initializer; emitted by [`supervisor_graph!`].
1806    #[cfg(feature = "coupling")]
1807    pub const fn with_reads(mut self, reads: &'static [&'static [Coupling]]) -> Self {
1808        self.reads = reads;
1809        self
1810    }
1811
1812    /// Declare the signals this node produces (the `writes:` graph clause).
1813    /// See [`with_reads`](Self::with_reads).
1814    #[cfg(feature = "coupling")]
1815    pub const fn with_writes(mut self, writes: &'static [&'static [Coupling]]) -> Self {
1816        self.writes = writes;
1817        self
1818    }
1819
1820    /// Declare the `bound`-marked subset of `deps:` — providers whose
1821    /// readiness controls this node. `const` and chainable in a `static`
1822    /// initializer; emitted by [`supervisor_graph!`].
1823    #[cfg(feature = "bound-deps")]
1824    pub const fn with_bound_deps(mut self, deps: &'static [&'static TaskNode]) -> Self {
1825        self.bound_deps = deps;
1826        self
1827    }
1828
1829    /// Point the node at its own graph. `const` and chainable in a `static`
1830    /// initializer; emitted by [`supervisor_graph!`].
1831    #[cfg(feature = "data-deps")]
1832    pub const fn with_graph(mut self, graph: &'static GraphRef) -> Self {
1833        self.graph = graph;
1834        self
1835    }
1836}
1837
1838impl TaskNode {
1839    /// A node over its flash-resident [`NodeCfg`]. `disabled_at_boot` seeds
1840    /// the node's disabled flag so a control-started node (e.g. an OTA task)
1841    /// can be declared down and started later via a control op. `const`;
1842    /// [`supervisor_graph!`] emits the config `static` and this call together.
1843    pub const fn new(cfg: &'static NodeCfg, disabled_at_boot: bool) -> Self {
1844        Self {
1845            cfg,
1846            handle: TaskHandle::new(disabled_at_boot),
1847        }
1848    }
1849
1850    /// Human-readable name. Used in defmt logs and panic messages.
1851    pub const fn name(&self) -> &'static str {
1852        self.cfg.name
1853    }
1854
1855    /// Lifecycle policy. See [`Mode`].
1856    pub const fn mode(&self) -> Mode {
1857        self.cfg.mode
1858    }
1859
1860    /// Does this node let an observed write assert its readiness?
1861    #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
1862    pub const fn ready_on_write(&self) -> bool {
1863        self.cfg.ready_on_write
1864    }
1865
1866    // ── Declaration getters ──────────────────────────────────────────────
1867    //
1868    // The `with_*` builders are write-only from the application's side; these
1869    // read back what the graph declared, so a status endpoint or a diagnostic
1870    // can report the configuration alongside the live state.
1871
1872    /// This node's pre-spawn gate-wait bound (see
1873    /// [`with_slot_timeout`](NodeCfg::with_slot_timeout)). The whole-graph
1874    /// waves budget all of a node's gates together, from when its in-pass deps
1875    /// resolve; the single-node path ([`start_node`](Supervisor::start_node))
1876    /// gives each gate — executor slot, each `resources:` slot, each `ready`
1877    /// dep — the full budget.
1878    pub const fn slot_timeout(&self) -> embassy_time::Duration {
1879        self.cfg.slot_timeout
1880    }
1881
1882    /// How long a stop waits for this node's shutdown ack before faulting it
1883    /// (see [`with_ack_timeout`](NodeCfg::with_ack_timeout)). Both stop paths
1884    /// honor it: the single-node wait, and the whole-graph wave, where each
1885    /// node's window runs from the moment *it* is signalled.
1886    pub const fn ack_timeout(&self) -> embassy_time::Duration {
1887        self.cfg.ack_timeout
1888    }
1889
1890    /// The deps whose readiness bring-up awaits before spawning this node (the
1891    /// `ready`-marked subset of `deps:`). Empty when none are marked.
1892    #[cfg(feature = "readiness")]
1893    pub const fn ready_deps(&self) -> &'static [&'static TaskNode] {
1894        self.cfg.ready_deps
1895    }
1896
1897    /// This node's liveness budget, or `None` when it is not policed (see
1898    /// [`with_beat_timeout`](NodeCfg::with_beat_timeout)).
1899    #[cfg(feature = "liveness-monitor")]
1900    pub const fn beat_timeout(&self) -> Option<embassy_time::Duration> {
1901        self.cfg.beat_timeout
1902    }
1903
1904    /// Consecutive stale sweeps required before the monitor reports this node
1905    /// (see [`with_beat_window`](NodeCfg::with_beat_window)).
1906    #[cfg(feature = "liveness-monitor")]
1907    pub const fn beat_window(&self) -> u8 {
1908        self.cfg.beat_window
1909    }
1910
1911    /// The signals this node declares it consumes (`reads:`).
1912    #[cfg(feature = "coupling")]
1913    pub const fn reads(&self) -> &'static [&'static [Coupling]] {
1914        self.cfg.reads
1915    }
1916
1917    /// The signals this node declares it produces (`writes:`).
1918    #[cfg(feature = "coupling")]
1919    pub const fn writes(&self) -> &'static [&'static [Coupling]] {
1920        self.cfg.writes
1921    }
1922
1923    /// Every coupling entry in one direction: tables in bound order, entries
1924    /// in table order.
1925    #[cfg(feature = "coupling")]
1926    fn entries(&self, is_write: bool) -> impl Iterator<Item = &'static Coupling> {
1927        let tables = if is_write {
1928            self.cfg.writes
1929        } else {
1930            self.cfg.reads
1931        };
1932        tables.iter().flat_map(|t| t.iter())
1933    }
1934
1935    /// Is `signal` among this node's entries in the given direction?
1936    #[cfg(feature = "coupling")]
1937    fn has_entry(&self, signal: &Coupling, is_write: bool) -> bool {
1938        self.entries(is_write).any(|e| same_signal(e, signal))
1939    }
1940
1941    /// The `bound`-marked subset of `deps:` (see
1942    /// [`with_bound_deps`](NodeCfg::with_bound_deps)).
1943    #[cfg(feature = "bound-deps")]
1944    pub const fn bound_deps(&self) -> &'static [&'static TaskNode] {
1945        self.cfg.bound_deps
1946    }
1947
1948    /// The node slots of the graph this node belongs to, `#[cfg]`-ed-out slots
1949    /// included as `None` — the same table [`Graph::nodes`] exposes, reached
1950    /// from a node rather than from the graph static. Empty for a node no
1951    /// graph declared.
1952    #[cfg(feature = "data-deps")]
1953    pub const fn graph(&self) -> &'static [Option<&'static TaskNode>] {
1954        self.cfg.graph.nodes()
1955    }
1956
1957    /// True while this node is down because a bound provider withdrew
1958    /// readiness — as opposed to `is_disabled`, which means somebody stopped it
1959    /// on purpose. The distinction matters: a bound stop must lift by itself
1960    /// when the provider recovers, and a manual stop must not.
1961    #[cfg(feature = "bound-deps")]
1962    pub fn is_bound_stopped(&self) -> bool {
1963        self.handle.flag(flag::BOUND_STOPPED)
1964    }
1965
1966    // ── Task-side API ────────────────────────────────────────────────────
1967    //
1968    // Called from inside the `#[embassy_executor::task] async fn` body. The
1969    // whole task-side protocol is four rules (the README's "Writing supervised
1970
1971    /// True if the supervisor has asked this node to shut down.
1972    /// A [`Fault::Wedge`] hides the request until cleared.
1973    pub fn shutdown_requested(&self) -> bool {
1974        #[cfg(feature = "fault-inject")]
1975        if self.fault() == Fault::Wedge {
1976            return false;
1977        }
1978        self.handle.flag(flag::SHUTDOWN)
1979    }
1980
1981    /// Wait until the supervisor asks this node to shut down.
1982    /// A [`Fault::Wedge`] keeps this pending until cleared.
1983    pub async fn wait_shutdown(&self) {
1984        #[cfg(feature = "fault-inject")]
1985        while !self.shutdown_requested() {
1986            core::future::poll_fn(|cx| {
1987                self.handle.fault_waker.register(cx.waker());
1988                if self.shutdown_requested() {
1989                    return Poll::Ready(());
1990                }
1991                core::pin::pin!(self.handle.shutdown_wake.wait()).poll(cx)
1992            })
1993            .await;
1994        }
1995        #[cfg(not(feature = "fault-inject"))]
1996        if !self.handle.flag(flag::SHUTDOWN) {
1997            self.handle.shutdown_wake.wait().await;
1998        }
1999    }
2000
2001    /// Poll the shutdown request, used by the cancel/pause drivers.
2002    /// A [`Fault::Wedge`] hides the request but leaves the waker registered.
2003    fn poll_shutdown(&self, cx: &mut Context<'_>) -> bool {
2004        #[cfg(feature = "fault-inject")]
2005        self.handle.fault_waker.register(cx.waker());
2006        #[cfg(feature = "fault-inject")]
2007        if self.fault() == Fault::Wedge {
2008            return false;
2009        }
2010        self.handle.flag(flag::SHUTDOWN)
2011            || core::pin::pin!(self.handle.shutdown_wake.wait())
2012                .poll(cx)
2013                .is_ready()
2014    }
2015
2016    /// Acknowledge that this node's instance has dropped and notify waiters.
2017    /// A [`Fault::Wedge`] swallows the ack until cleared.
2018    pub fn ack_dropped(&self) {
2019        self.release_provided();
2020        #[cfg(feature = "fault-inject")]
2021        if self.wedge_swallows(flag::PENDING_ACK) {
2022            return;
2023        }
2024        self.settle_dropped();
2025    }
2026
2027    /// Swallow an ack while wedged. Returns true if the clear must deliver it.
2028    #[cfg(feature = "fault-inject")]
2029    fn wedge_swallows(&self, bits: u16) -> bool {
2030        if self.fault() == Fault::Wedge {
2031            self.handle.flag_set(bits);
2032            if self.fault() == Fault::Wedge {
2033                return true;
2034            }
2035            let prev = self.handle.flags.fetch_and(!bits, Ordering::AcqRel);
2036            if prev & flag::PENDING_ACK == 0 {
2037                return true;
2038            }
2039        }
2040        false
2041    }
2042
2043    /// Drop what this instance provided and release its claims.
2044    fn release_provided(&self) {
2045        if !matches!(self.cfg.mode, Mode::Pause) {
2046            for gate in self.cfg.provides {
2047                gate.clear();
2048            }
2049            #[cfg(feature = "budget")]
2050            self.release_claims();
2051        }
2052    }
2053
2054    /// Update flags and wakes to record the dropped instance.
2055    fn settle_dropped(&self) {
2056        self.handle.flag_clear(flag::RUNNING);
2057        self.handle.flag_set(flag::DROPPED);
2058        self.handle.dropped_wake.signal(());
2059        STOP_EVT.signal(());
2060        #[cfg(feature = "bound-deps")]
2061        notify_bind();
2062        #[cfg(all(feature = "data-deps", feature = "readiness"))]
2063        crate::data_deps::notify_serving();
2064    }
2065
2066    /// Deliver an ack or exit that a wedge swallowed.
2067    #[cfg(feature = "fault-inject")]
2068    fn replay_swallowed(&self) {
2069        let pending = self
2070            .handle
2071            .flags
2072            .fetch_and(!(flag::PENDING_ACK | flag::PENDING_EXIT), Ordering::AcqRel);
2073        if pending & flag::PENDING_ACK != 0 {
2074            if pending & flag::PENDING_EXIT != 0 {
2075                self.handle.flag_set(flag::COMPLETED);
2076            }
2077            self.settle_dropped();
2078        }
2079    }
2080
2081    /// Give back every `divisible` share this node holds (see
2082    /// [`NodeCfg::with_claims`]). Idempotent: releasing an empty slot stores zero.
2083    #[cfg(feature = "budget")]
2084    pub fn release_claims(&self) {
2085        for (budget, slot) in self.cfg.claims {
2086            budget.release(*slot);
2087        }
2088    }
2089
2090    /// Mark the task as completed and acknowledge its drop.
2091    /// A wedge hides this until cleared.
2092    pub fn mark_exited(&self) {
2093        self.release_provided();
2094        #[cfg(feature = "fault-inject")]
2095        if self.wedge_swallows(flag::PENDING_EXIT | flag::PENDING_ACK) {
2096            return;
2097        }
2098        self.handle.flag_set(flag::COMPLETED);
2099        self.settle_dropped();
2100    }
2101
2102    /// Current injected fault, or `None` if healthy.
2103    #[cfg(feature = "fault-inject")]
2104    pub fn fault(&self) -> Fault {
2105        Fault::decode(
2106            self.handle.fault.load(Ordering::Acquire),
2107            self.handle.hog_ms.load(Ordering::Relaxed),
2108        )
2109    }
2110
2111    /// Inject a fault, or clear it with `Fault::None`.
2112    /// Stall, crash and hog need a `task:` shell; wedge works on any node.
2113    #[cfg(feature = "fault-inject")]
2114    pub fn inject(&self, fault: Fault) -> Result<(), InjectError> {
2115        if fault == Fault::None {
2116            self.clear_fault();
2117            return Ok(());
2118        }
2119        if fault != Fault::Wedge && !self.cfg.shelled {
2120            return Err(InjectError::NoShell);
2121        }
2122        let (code, ms) = fault.encode();
2123        self.handle.hog_ms.store(ms, Ordering::Relaxed);
2124        self.handle.fault.store(code, Ordering::Release);
2125        // Replacing a wedge releases anything it was holding back.
2126        if fault != Fault::Wedge {
2127            self.replay_swallowed();
2128        }
2129        // Wake the shell so stall or hog take effect immediately.
2130        self.handle.fault_waker.wake();
2131        Ok(())
2132    }
2133
2134    /// Clear the injected fault and replay anything it withheld.
2135    #[cfg(feature = "fault-inject")]
2136    pub fn clear_fault(&self) {
2137        self.handle.fault.swap(0, Ordering::AcqRel);
2138        self.replay_swallowed();
2139        // Wake any worker parked on the now-visible shutdown request.
2140        // Do not re-fire `shutdown_wake`; its latch would outlive this fault.
2141        self.handle.fault_waker.wake();
2142    }
2143
2144    #[doc(hidden)]
2145    pub fn mark_lost_resource(&self) {
2146        warn!(
2147            "supervisor: {} lost a resource between spawn and first poll",
2148            self.name()
2149        );
2150        self.mark_exited();
2151    }
2152
2153    /// Return `true` if the node has been marked as exited.
2154    pub fn has_exited(&self) -> bool {
2155        self.handle.flag(flag::COMPLETED)
2156    }
2157
2158    #[cfg(feature = "readiness")]
2159    /// Assert this node's readiness and wake dependents waiting on it.
2160    pub fn set_ready(&self) {
2161        self.handle.flag_set(flag::READY);
2162        self.handle.ready_wake.signal(());
2163        __sv_gate_event();
2164        #[cfg(feature = "data-deps")]
2165        crate::data_deps::notify_serving();
2166        #[cfg(feature = "bound-deps")]
2167        notify_bind();
2168        #[cfg(feature = "pool")]
2169        request_scale();
2170    }
2171
2172    #[cfg(feature = "readiness")]
2173    /// Clear this node's readiness.
2174    pub fn clear_ready(&self) {
2175        self.handle.flag_clear(flag::READY);
2176        #[cfg(feature = "bound-deps")]
2177        notify_bind();
2178    }
2179
2180    #[cfg(feature = "readiness")]
2181    /// Return whether this node is currently ready.
2182    pub fn is_ready(&self) -> bool {
2183        self.handle.flag(flag::READY)
2184    }
2185
2186    #[cfg(feature = "readiness")]
2187    /// Wait until this node becomes ready.
2188    pub async fn wait_ready(&self) {
2189        loop {
2190            if self.is_ready() {
2191                return;
2192            }
2193            self.handle.ready_wake.wait().await;
2194        }
2195    }
2196
2197    #[cfg(all(feature = "pool", feature = "readiness"))]
2198    pub(crate) fn ready_deps_ok(&self) -> bool {
2199        self.cfg.ready_deps.iter().all(|d| d.is_ready())
2200    }
2201    #[cfg(all(feature = "pool", not(feature = "readiness")))]
2202    pub(crate) fn ready_deps_ok(&self) -> bool {
2203        true
2204    }
2205
2206    #[cfg(feature = "liveness")]
2207    #[inline]
2208    /// Record a liveness beat for this node.
2209    pub fn beat(&self) {
2210        self.handle.pending_beat.store(true, Ordering::Relaxed);
2211    }
2212
2213    #[cfg(all(feature = "coupling-observe", feature = "liveness"))]
2214    /// Return whether any observed beat coupling has changed since last call.
2215    pub fn poll_observed_writes(&self) -> bool {
2216        let mut mark = 0u32;
2217        let mut any = false;
2218        for w in self.entries(true) {
2219            if !w.beats() {
2220                continue;
2221            }
2222            if let Some(o) = w.observer() {
2223                mark = mark.wrapping_add(o.count());
2224                any = true;
2225            }
2226        }
2227        any && self.handle.write_mark.swap(mark, Ordering::AcqRel) != mark
2228    }
2229
2230    #[cfg(all(feature = "coupling-observe", feature = "liveness"))]
2231    fn seed_write_mark(&self) {
2232        let mut mark = 0u32;
2233        for w in self.entries(true) {
2234            if w.beats()
2235                && let Some(o) = w.observer()
2236            {
2237                mark = mark.wrapping_add(o.count());
2238            }
2239        }
2240        self.handle.write_mark.store(mark, Ordering::Release);
2241    }
2242
2243    #[cfg(feature = "node-status")]
2244    /// Report a status string for this node.
2245    pub fn report_status(&self, status: &'static str) {
2246        let prev = self.handle.status.lock(|s| s.replace(Some(status)));
2247        // `&'static str`s for a status are typically literals, so pointer
2248        // inequality is "changed" for logging purposes; a same-text status
2249        // reached through two literals logs once more, harmlessly.
2250        if prev.is_none_or(|p| !core::ptr::eq(p.as_ptr(), status.as_ptr())) {
2251            info!("supervisor: {}: {}", self.cfg.name, status);
2252        }
2253    }
2254
2255    /// The node's current self-description, if it reported one this activation.
2256    #[cfg(feature = "node-status")]
2257    pub fn status(&self) -> Option<&'static str> {
2258        self.handle.status.lock(|s| s.get())
2259    }
2260
2261    /// Ticks since the last [`beat`](Self::beat) — where, with
2262    /// `dataflow`, a write through the node's verbs since the previous
2263    /// call counts as one, granted here (wrapping arithmetic; correct
2264    /// for gaps under the u32 tick wrap, ~71 min at 1 MHz — far above any sane
2265    /// `max_age`).
2266    #[cfg(feature = "liveness")]
2267    pub fn ticks_since_beat(&self) -> u32 {
2268        let now = embassy_time::Instant::now().as_ticks() as u32;
2269        // A beat since the last look is granted here: the checker pays the
2270        // (already-read) clock, the beating task never does. Load-then-swap so
2271        // a plain check stays one load; racing checkers are benign — one
2272        // stamps, the rest see it stamped.
2273        if self.handle.pending_beat.load(Ordering::Relaxed)
2274            && self.handle.pending_beat.swap(false, Ordering::AcqRel)
2275        {
2276            self.handle.last_beat.store(now, Ordering::Release);
2277        }
2278        now.wrapping_sub(self.handle.last_beat.load(Ordering::Acquire))
2279    }
2280
2281    /// Ticks until this node next needs looking at, for the monitor's sleep.
2282    ///
2283    /// `None` when the node is unpoliced. Three cases, in the order they matter:
2284    ///
2285    /// * **Waiting to assert readiness** (`ready_on_write`, not yet ready) — a
2286    ///   short probe. Readiness gates dependents' spawns against their
2287    ///   `slot_timeout`, so noticing the first write late spends someone else's
2288    ///   budget. Bring-up is brief and latency-sensitive; a fraction of the beat
2289    ///   budget buys that back and costs nothing once the node is ready.
2290    /// * **Overdue** — half a budget, so a stalled node's `beat_window` strikes
2291    ///   accumulate at a bounded rate instead of spinning on a zero delay.
2292    /// * **Running normally** — exactly when it would go stale.
2293    ///
2294    /// A node that is down or detached is re-examined a budget later: there is
2295    /// nothing to report about it now, but it may be running by then.
2296    #[cfg(feature = "liveness-monitor")]
2297    fn ticks_until_check(&self) -> Option<u64> {
2298        let budget = self.cfg.beat_timeout?.as_ticks();
2299        if self.is_detached() || !self.is_running() {
2300            return Some(budget);
2301        }
2302        // The probe serves the sweep-driven (`observed`) form only; a verb
2303        // write asserts readiness inline, with nothing to poll for.
2304        #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
2305        if self.cfg.ready_on_write && !self.is_ready() {
2306            return Some((budget / READY_PROBE_DIVISOR).max(1));
2307        }
2308        Some(
2309            match budget.saturating_sub(self.ticks_since_beat() as u64) {
2310                0 => (budget / 2).max(1),
2311                remaining => remaining,
2312            },
2313        )
2314    }
2315
2316    /// True when the node is running but hasn't beaten within `max_age` — the
2317    /// alive-but-stalled detector (a task hogging nothing, parked on an await
2318    /// that will never complete). Not-running nodes are never stale: a stopped
2319    /// or completed node is *down*, which `is_running`/`has_exited` already
2320    /// report. Complements the `trace` stall watermark, which catches the
2321    /// opposite failure (a poll that never yields).
2322    #[cfg(feature = "liveness")]
2323    pub fn is_stale(&self, max_age: embassy_time::Duration) -> bool {
2324        self.is_running() && u64::from(self.ticks_since_beat()) > max_age.as_ticks()
2325    }
2326
2327    /// This node's activation generation: `0` before the first spawn, then
2328    /// incremented on every transition into `running` — a fresh spawn, a pool
2329    /// grow, a `respawn_terminate`, or a `Pause` node's resume.
2330    ///
2331    /// **The dependent-side answer to "my provider was restarted underneath
2332    /// me".** `deps:` gates a spawn once; nothing re-gates a node that is
2333    /// *already running* when one of its providers cycles. A consumer holding
2334    /// derived state (a filter, a session, a cached handle) samples this once
2335    /// and compares it each iteration — one relaxed load, cheap enough for a
2336    /// 1 kHz loop:
2337    ///
2338    /// ```ignore
2339    /// let mut seen = PROVIDER.epoch();
2340    /// loop {
2341    ///     let sample = INPUT.wait().await;
2342    ///     let now = PROVIDER.epoch();
2343    ///     if now != seen {
2344    ///         seen = now;
2345    ///         filter.reset();   // the provider is a new instance; derived state is stale
2346    ///     }
2347    ///     // ...
2348    /// }
2349    /// ```
2350    #[cfg(feature = "epochs")]
2351    pub fn epoch(&self) -> u32 {
2352        self.handle.epoch.load(Ordering::Acquire)
2353    }
2354
2355    #[cfg(feature = "epochs")]
2356    /// Wait until the node's epoch counter differs from `seen`.
2357    pub async fn wait_epoch_change(&self, seen: u32) -> u32 {
2358        loop {
2359            let now = self.epoch();
2360            if now != seen {
2361                return now;
2362            }
2363            self.handle.epoch_wake.wait().await;
2364        }
2365    }
2366
2367    /// Wait until the supervisor signals this Pause-mode node to resume.
2368    pub async fn wait_resume(&self) {
2369        self.handle.resume_wake.wait().await;
2370    }
2371
2372    /// Run `fut` until it completes or the node is stopped, returning [`Aborted`] on stop.
2373    pub fn run_cancellable<F: Future>(
2374        &self,
2375        fut: F,
2376    ) -> impl Future<Output = Result<F::Output, Aborted>> {
2377        RunCancellable {
2378            fut: Some(fut),
2379            node: self,
2380            ack: false,
2381        }
2382    }
2383
2384    /// Like [`run_cancellable`](Self::run_cancellable), but also acks the stop handshake.
2385    pub fn run_cancellable_acked<F: Future>(
2386        &self,
2387        fut: F,
2388    ) -> impl Future<Output = Result<F::Output, Aborted>> {
2389        RunCancellable {
2390            fut: Some(fut),
2391            node: self,
2392            ack: true,
2393        }
2394    }
2395
2396    /// Run `fut` until it completes or the node is paused, returning [`Resumed`] on pause.
2397    pub fn run_pausable<F: Future>(
2398        &self,
2399        fut: F,
2400    ) -> impl Future<Output = Result<F::Output, Resumed>> {
2401        RunPausable {
2402            fut: Some(fut),
2403            node: self,
2404            parked: false,
2405        }
2406    }
2407
2408    /// Run `body` in a `run_pausable` loop forever, surviving pause/resume cycles.
2409    pub async fn run_pausable_loop(&self, mut body: impl AsyncFnMut()) -> ! {
2410        loop {
2411            let _ = self.run_pausable(body()).await;
2412        }
2413    }
2414
2415    /// Mark this node as busy, requesting pool scale-out if one is configured.
2416    pub fn mark_busy(&self) {
2417        if !self.handle.flag_swap(flag::BUSY, true) {
2418            request_scale();
2419        }
2420    }
2421
2422    /// Mark this node as idle, requesting pool scale-in if one is configured.
2423    pub fn mark_idle(&self) {
2424        if self.handle.flag_swap(flag::BUSY, false) {
2425            request_scale();
2426        }
2427    }
2428
2429    /// Return `true` if the node is currently marked busy.
2430    pub fn is_busy(&self) -> bool {
2431        self.handle.flag(flag::BUSY)
2432    }
2433
2434    /// Return `true` if the node has a running instance.
2435    pub fn is_running(&self) -> bool {
2436        self.handle.flag(flag::RUNNING)
2437    }
2438
2439    /// Return `true` if the node has been manually disabled.
2440    pub fn is_disabled(&self) -> bool {
2441        self.handle.flag(flag::DISABLED)
2442    }
2443
2444    /// Return `true` if the node is held stopped as a dependent of a
2445    /// deactivated node.
2446    pub fn is_collateral(&self) -> bool {
2447        self.handle.flag(flag::COLLATERAL)
2448    }
2449
2450    /// Set whether this node is detached from automatic lifecycle management.
2451    pub fn set_detached(&self, detached: bool) {
2452        self.handle.flag_put(flag::DETACHED, detached);
2453    }
2454
2455    /// Return `true` if the node is detached from automatic lifecycle management.
2456    pub fn is_detached(&self) -> bool {
2457        self.handle.flag(flag::DETACHED)
2458    }
2459
2460    #[cfg(feature = "trace")]
2461    /// Record the executor task id for this node instance.
2462    pub fn set_task_id(&self, id: u32) {
2463        self.handle.task_id.store(id, Ordering::Release);
2464    }
2465
2466    #[cfg(feature = "trace")]
2467    /// Adopt a spawn token's task id (and name, if enabled) for tracing.
2468    pub fn adopt<S>(&self, token: &embassy_executor::SpawnToken<S>) {
2469        let id = token.id();
2470        #[cfg(embassy_supervisor_trace_v2)]
2471        let id = trace::task_key(id);
2472        self.set_task_id(id);
2473        #[cfg(feature = "metadata-names")]
2474        self.stamp_name(token);
2475    }
2476
2477    #[cfg(feature = "trace")]
2478    /// Adopt the current task's id for tracing.
2479    pub async fn adopt_current(&self) {
2480        self.set_task_id(trace::current_task_id().await);
2481    }
2482
2483    #[cfg(feature = "metadata-names")]
2484    /// Set the spawn token's task name to this node's configured name.
2485    pub fn stamp_name<S>(&self, token: &embassy_executor::SpawnToken<S>) {
2486        token.metadata().set_name(self.cfg.name);
2487    }
2488
2489    #[cfg(feature = "trace")]
2490    /// Return the id of the task currently adopted by this node.
2491    pub fn task_id(&self) -> u32 {
2492        self.handle.task_id.load(Ordering::Acquire)
2493    }
2494
2495    #[cfg(feature = "trace")]
2496    /// Return the accumulated execution tick count for this node.
2497    pub fn exec_ticks(&self) -> u32 {
2498        self.handle.exec_ticks.load(Ordering::Relaxed)
2499    }
2500
2501    #[cfg(feature = "trace")]
2502    /// Return the number of poll cycles recorded for this node.
2503    pub fn poll_count(&self) -> u32 {
2504        self.handle.polls.load(Ordering::Relaxed)
2505    }
2506
2507    #[cfg(feature = "trace")]
2508    /// Return the longest single-poll tick count recorded for this node.
2509    pub fn max_poll_ticks(&self) -> u32 {
2510        self.handle.max_poll_ticks.load(Ordering::Relaxed)
2511    }
2512
2513    pub(crate) fn signal_shutdown(&self) {
2514        self.handle.flag_set(flag::SHUTDOWN);
2515        self.handle.shutdown_wake.signal(());
2516    }
2517
2518    pub(crate) fn signal_resume(&self) {
2519        self.handle.resume_wake.signal(());
2520    }
2521
2522    pub(crate) fn set_running(&self, running: bool) {
2523        self.handle.flag_put(flag::RUNNING, running);
2524        #[cfg(feature = "liveness")]
2525        if running {
2526            self.handle.last_beat.store(
2527                embassy_time::Instant::now().as_ticks() as u32,
2528                Ordering::Release,
2529            );
2530            #[cfg(feature = "coupling-observe")]
2531            self.seed_write_mark();
2532        }
2533        #[cfg(feature = "liveness")]
2534        if running {
2535            self.handle.pending_beat.store(false, Ordering::Release);
2536        }
2537        #[cfg(feature = "node-status")]
2538        if running {
2539            self.handle.status.lock(|s| s.set(None));
2540        }
2541        #[cfg(feature = "epochs")]
2542        if running {
2543            self.handle.epoch.fetch_add(1, Ordering::AcqRel);
2544            self.handle.epoch_wake.signal(());
2545        }
2546    }
2547
2548    /// Manually disable or re-enable this node.
2549    pub fn set_disabled(&self, disabled: bool) {
2550        self.handle.flag_put(flag::DISABLED, disabled);
2551    }
2552
2553    pub(crate) fn has_acked_stop(&self) -> bool {
2554        self.handle.flag(flag::DROPPED) && !self.handle.flag(flag::COMPLETED)
2555    }
2556
2557    pub(crate) async fn wait_dropped(&self) {
2558        if self.handle.flag(flag::DROPPED) {
2559            return;
2560        }
2561        self.handle.dropped_wake.wait().await;
2562    }
2563
2564    pub(crate) fn has_dropped(&self) -> bool {
2565        self.handle.flag(flag::DROPPED)
2566    }
2567
2568    pub(crate) fn reset(&self) {
2569        let stale = flag::SHUTDOWN | flag::DROPPED | flag::BUSY | flag::COMPLETED;
2570        #[cfg(feature = "readiness")]
2571        let stale = stale | flag::READY;
2572        // Pending bits belong to the old instance; do not carry them forward.
2573        #[cfg(feature = "fault-inject")]
2574        let stale = stale | flag::PENDING_ACK | flag::PENDING_EXIT;
2575        self.handle.flag_clear(stale);
2576        #[cfg(feature = "readiness")]
2577        self.handle.ready_wake.reset();
2578        self.handle.shutdown_wake.reset();
2579        self.handle.dropped_wake.reset();
2580    }
2581}
2582
2583impl core::fmt::Debug for TaskNode {
2584    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2585        let mut d = f.debug_struct("TaskNode");
2586        d.field("name", &self.cfg.name)
2587            .field("mode", &self.cfg.mode)
2588            .field("running", &self.is_running())
2589            .field("busy", &self.is_busy())
2590            .field("disabled", &self.is_disabled())
2591            .field("collateral", &self.is_collateral())
2592            .field("detached", &self.is_detached());
2593        #[cfg(feature = "epochs")]
2594        d.field("epoch", &self.epoch());
2595        d.finish_non_exhaustive()
2596    }
2597}
2598
2599// ─── Topology ────────────────────────────────────────────────────────────
2600
2601/// Structural facts about a graph, as bits in [`Topology::SHAPE`] — what the
2602/// graph *contains*, decided at `supervisor_graph!` expansion and carried in
2603/// the topology's **type**, so lifecycle code serving an absent structure is
2604pub mod shape {
2605    /// The graph contains at least one `ready` dependency.
2606    pub const READY_DEPS: u32 = 1 << 0;
2607    /// The graph declares at least one named executor slot.
2608    pub const EXEC_SLOTS: u32 = 1 << 1;
2609    /// The graph declares at least one resource slot.
2610    pub const RESOURCES: u32 = 1 << 2;
2611    /// The graph contains at least one `Pause` node or pool member.
2612    pub const PAUSE: u32 = 1 << 3;
2613    /// The graph contains at least one `OnDemand` node or pool member.
2614    pub const ON_DEMAND: u32 = 1 << 4;
2615    /// The graph declares at least one heartbeat (`beat_timeout:` or `beat`).
2616    pub const BEATS: u32 = 1 << 5;
2617    /// The graph declares at least one `observed` signal entry.
2618    pub const OBSERVED: u32 = 1 << 6;
2619    /// The graph contains at least one `bound` dependency.
2620    pub const BOUND_DEPS: u32 = 1 << 7;
2621    /// The graph declares at least one elastic pool.
2622    pub const POOLS: u32 = 1 << 8;
2623    /// The graph declares at least one `divisible` resource.
2624    pub const CLAIMS: u32 = 1 << 9;
2625    /// All shape bits set.
2626    pub const ALL: u32 = u32::MAX;
2627}
2628
2629/// Structural information about a graph, used by [`Supervisor`] to decide
2630/// which lifecycle code paths can be compiled out.
2631pub trait Topology<const N: usize>: 'static {
2632    /// Structural-fact bits (see [`shape`]). An unset bit promises the
2633    /// structure is absent from the whole graph.
2634    const SHAPE: u32;
2635
2636    /// The dependency indices of slot `i` — what node `i` declared it needs
2637    /// spawned first. **Spawn ordering, not runtime coupling**: see the crate
2638    /// docs on what a `deps:` edge does and does not assert.
2639    fn deps_of(&self, i: u8) -> &'static [u8];
2640
2641    /// Return the slot index at topological position `k` (0..N).
2642    fn order_at(&self, k: usize) -> u8;
2643}
2644
2645/// A [`Topology`] whose nodes are topologically sorted by their `deps:` edges.
2646pub struct Ordered<const N: usize, const SHAPE: u32> {
2647    deps: &'static [&'static [u8]; N],
2648    order: [u8; N],
2649}
2650
2651impl<const N: usize, const SHAPE: u32> Ordered<N, SHAPE> {
2652    /// Build a topology from a static array of dependency lists.
2653    pub const fn new(deps: &'static [&'static [u8]; N]) -> Self {
2654        Self {
2655            deps,
2656            order: topo_sort_const(deps),
2657        }
2658    }
2659}
2660
2661impl<const N: usize, const SHAPE: u32> Topology<N> for Ordered<N, SHAPE> {
2662    const SHAPE: u32 = SHAPE;
2663
2664    fn deps_of(&self, i: u8) -> &'static [u8] {
2665        self.deps[i as usize]
2666    }
2667
2668    fn order_at(&self, k: usize) -> u8 {
2669        self.order[k]
2670    }
2671}
2672
2673/// The [`Topology`] of a graph with **no** `deps:` edges anywhere: zero-sized,
2674/// every dep list is empty by type, and the topological order is declaration
2675/// order. The walks a [`Supervisor`] runs over it fold to plain index loops,
2676/// and the dependency cascades (`activate`, `deactivate`, `restart`) collapse
2677/// to their seed sets.
2678pub struct Flat<const SHAPE: u32>;
2679
2680impl<const SHAPE: u32> Flat<SHAPE> {
2681    /// The (zero-sized) flat topology.
2682    pub const fn new() -> Self {
2683        Self
2684    }
2685}
2686
2687impl<const SHAPE: u32> Default for Flat<SHAPE> {
2688    fn default() -> Self {
2689        Self
2690    }
2691}
2692
2693impl<const N: usize, const SHAPE: u32> Topology<N> for Flat<SHAPE> {
2694    const SHAPE: u32 = SHAPE;
2695
2696    fn deps_of(&self, _i: u8) -> &'static [u8] {
2697        &[]
2698    }
2699
2700    fn order_at(&self, k: usize) -> u8 {
2701        k as u8
2702    }
2703}
2704
2705const fn has(shape_bits: u32, bit: u32) -> bool {
2706    shape_bits & bit != 0
2707}
2708
2709/// A static task graph: the nodes, the topology over them, and optional pools.
2710pub struct Graph<const N: usize, T: Topology<N> = Ordered<N, { shape::ALL }>> {
2711    /// The fixed array of node slots; `None` marks a disabled or cfg-gapped slot.
2712    pub nodes: &'static [Option<&'static TaskNode>; N],
2713    /// The topology that defines spawn order and dependency rows.
2714    pub topo: T,
2715    #[cfg(feature = "pool")]
2716    /// The elastic pools declared in the graph.
2717    pub pools: &'static [&'static dyn Pool],
2718    #[cfg(feature = "graph-ref")]
2719    /// A reference used to enumerate the graph at runtime.
2720    pub graph_ref: &'static GraphRef,
2721}
2722
2723impl<const N: usize, T: Topology<N>> Graph<N, T> {
2724    /// Slot index of `node` in this graph (pointer identity — every node is a
2725    /// `&'static`), or `None` if it belongs to another graph. The inverse of
2726    /// indexing [`nodes`](Self::nodes), and the bridge an app-side health view
2727    /// needs to get from a node back to its [`deps_of`](Self::deps_of) row.
2728    pub fn index_of(&self, node: &'static TaskNode) -> Option<u8> {
2729        self.nodes
2730            .iter()
2731            .position(|s| s.is_some_and(|n| core::ptr::eq(n, node)))
2732            .map(|i| i as u8)
2733    }
2734
2735    /// The dependency indices of slot `i` — what node `i` declared it needs
2736    /// spawned first. **Spawn ordering, not runtime coupling**: see the crate
2737    /// docs on what a `deps:` edge does and does not assert.
2738    ///
2739    /// # Panics
2740    /// If `i >= N` (on an [`Ordered`] topology; [`Flat`] has no rows to index).
2741    pub fn deps_of(&self, i: u8) -> &'static [u8] {
2742        self.topo.deps_of(i)
2743    }
2744
2745    /// The slot indices in topological order (dependencies before their
2746    /// dependents; `.rev()` is the teardown order). Declaration order on a
2747    /// [`Flat`] topology.
2748    pub fn order(&self) -> impl DoubleEndedIterator<Item = u8> + ExactSizeIterator + '_ {
2749        (0..N).map(|k| self.topo.order_at(k))
2750    }
2751
2752    /// Call `visit` with the slot index of every node that declares slot `i` as
2753    /// a dependency (direct dependents only). Computed by a forward scan of
2754    /// the dep rows — no reverse-edge table is stored, so this is
2755    /// O(N·E) and meant for control paths and status endpoints, not hot loops.
2756    ///
2757    /// `&mut dyn FnMut` rather than a generic: one instantiation, no
2758    /// monomorphization per call site.
2759    pub fn dependents_of(&self, i: u8, visit: &mut dyn FnMut(u8)) {
2760        for j in 0..N {
2761            if self.topo.deps_of(j as u8).contains(&i) {
2762                visit(j as u8);
2763            }
2764        }
2765    }
2766
2767    /// Iterate the live nodes with their slot indices, skipping `#[cfg]`-ed-out
2768    /// slots — the ergonomic form of `GRAPH.nodes.iter().enumerate()` for a
2769    /// status endpoint that needs the index (to reach `deps`) alongside the node.
2770    pub fn iter_nodes(&self) -> impl Iterator<Item = (u8, &'static TaskNode)> + '_ {
2771        self.nodes
2772            .iter()
2773            .enumerate()
2774            .filter_map(|(i, s)| s.map(|n| (i as u8, n)))
2775    }
2776
2777    /// Every node declaring `signal` in its `writes:`, by slot and node.
2778    /// Matched by address, so it is the *static* that is compared, not the
2779    /// path text.
2780    #[cfg(feature = "coupling")]
2781    pub fn writers_of(&self, signal: &Coupling, visit: &mut dyn FnMut(u8, &'static TaskNode)) {
2782        for (i, node) in self.iter_nodes() {
2783            if node.has_entry(signal, true) {
2784                visit(i, node);
2785            }
2786        }
2787    }
2788
2789    /// Every node declaring `signal` in its `reads:`. The counterpart to
2790    /// [`writers_of`](Self::writers_of); together they answer the structural
2791    /// questions about one signal — who produces it, who consumes it, which
2792    /// pairs are coupled in a loop.
2793    #[cfg(feature = "coupling")]
2794    pub fn readers_of(&self, signal: &Coupling, visit: &mut dyn FnMut(u8, &'static TaskNode)) {
2795        for (i, node) in self.iter_nodes() {
2796            if node.has_entry(signal, false) {
2797                visit(i, node);
2798            }
2799        }
2800    }
2801}
2802
2803// ─── Supervisor ──────────────────────────────────────────────────────────
2804
2805/// Orchestrates a set of managed tasks across spawn / teardown / bring-up.
2806///
2807/// Owned by a single supervisor task. Concurrent access from other tasks goes
2808/// through each [`TaskNode`]'s own atomic state, not the `Supervisor` struct.
2809pub struct Supervisor<const N: usize, T: Topology<N> = Ordered<N, { shape::ALL }>> {
2810    /// Node slots, one per declared node. `None` marks a slot whose node was
2811    /// `#[cfg]`-ed out of the build (feature-gated); every method skips those.
2812    nodes: &'static [Option<&'static TaskNode>; N],
2813    /// The graph's [`Topology`]: the dep rows and topological order every walk
2814    /// iterates (reverse iteration is the teardown order), and the structural
2815    /// [`shape`] bits the gates fold on. Borrowed from the `static` [`Graph`]
2816    /// rather than copied: a `Supervisor` usually lives inside a task future
2817    /// (i.e. in that task's `static` storage), so an inline order array would
2818    /// cost N bytes of RAM per supervisor plus the copy code for no benefit —
2819    /// and for [`Flat`] this reference is the field's entire cost.
2820    topo: &'static T,
2821    /// Elastic pools, so the control interface can co-control a whole pool from
2822    /// any one member (`apply_control` expands the target through
2823    /// [`Pool::members`]) — the same registry `run_pools` drives. Taken from
2824    /// `GRAPH.pools` at construction (empty when no pool is declared).
2825    #[cfg(feature = "pool")]
2826    pools: &'static [&'static dyn Pool],
2827    /// This graph as one `'static`, linked into the binary-wide chain by
2828    /// [`start`](Supervisor::start) so the trace hooks can resolve a task id to
2829    /// one of its nodes.
2830    #[cfg(feature = "trace")]
2831    graph_ref: &'static GraphRef,
2832}
2833
2834async fn await_spawn_slot(node: &'static TaskNode) -> Result<(), NodeFault> {
2835    if let Some(slot) = node.cfg.spawn_slot {
2836        with_timeout(node.slot_timeout(), slot.ready())
2837            .await
2838            .map_err(|_| NodeFault {
2839                node,
2840                kind: FaultKind::ExecutorSlotEmpty,
2841            })?;
2842    }
2843    Ok(())
2844}
2845
2846/// Await every [`ResourceSlot`] a node's `resources:` clause takes from being
2847async fn await_resources(node: &'static TaskNode) -> Result<(), NodeFault> {
2848    for gate in node.cfg.resource_gates {
2849        let wait = async {
2850            loop {
2851                if gate.is_filled() {
2852                    break;
2853                }
2854                gate.filled_signal().wait().await;
2855            }
2856        };
2857        with_timeout(node.slot_timeout(), wait)
2858            .await
2859            .map_err(|_| NodeFault {
2860                node,
2861                kind: FaultKind::ResourceMissing,
2862            })?;
2863    }
2864    Ok(())
2865}
2866
2867/// Await every `ready`-marked dep's task-asserted readiness before spawning
2868#[cfg(feature = "readiness")]
2869async fn await_ready_deps(node: &'static TaskNode) -> Result<(), NodeFault> {
2870    for dep in node.ready_deps() {
2871        if with_timeout(node.slot_timeout(), dep.wait_ready())
2872            .await
2873            .is_err()
2874        {
2875            return Err(NodeFault {
2876                node,
2877                kind: FaultKind::ReadyDepTimeout { dep },
2878            });
2879        }
2880    }
2881    Ok(())
2882}
2883#[cfg(not(feature = "readiness"))]
2884async fn await_ready_deps(_node: &'static TaskNode) -> Result<(), NodeFault> {
2885    Ok(())
2886}
2887
2888impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
2889    const NODE_CAP: () = assert!(N <= 256, "supervisor: a graph holds at most 256 nodes");
2890
2891    /// Create a supervisor from a statically-built graph.
2892    pub const fn new(graph: &'static Graph<N, T>) -> Self {
2893        let () = Self::NODE_CAP;
2894        Self {
2895            nodes: graph.nodes,
2896            topo: &graph.topo,
2897            #[cfg(feature = "pool")]
2898            pools: graph.pools,
2899            #[cfg(feature = "trace")]
2900            graph_ref: graph.graph_ref,
2901        }
2902    }
2903
2904    /// Does this graph's shape carry `bit` (see [`shape`])? `T::SHAPE` is a
2905    #[inline(always)]
2906    fn has(bit: u32) -> bool {
2907        has(T::SHAPE, bit)
2908    }
2909
2910    fn order_iter(&self) -> impl DoubleEndedIterator<Item = usize> + '_ {
2911        (0..N).map(|k| self.topo.order_at(k) as usize)
2912    }
2913
2914    /// The pre-spawn gate sequence — executor slot, resource gates, `ready`
2915    /// deps — with each wait compiled out when the graph's shape lacks the
2916    async fn await_gates(node: &'static TaskNode) -> Result<(), NodeFault> {
2917        if Self::has(shape::EXEC_SLOTS) {
2918            await_spawn_slot(node).await?;
2919        }
2920        if Self::has(shape::RESOURCES) {
2921            await_resources(node).await?;
2922        }
2923        if Self::has(shape::READY_DEPS) {
2924            await_ready_deps(node).await?;
2925        }
2926        Ok(())
2927    }
2928
2929    /// Bring the graph from any quiescent state to running, in dependency
2930    /// order — cold boot AND re-entry (a sub-graph supervisor is legitimately
2931    /// `start()`/`teardown()`-cycled per app phase). Idempotent: running nodes
2932    /// are skipped; detached nodes are skipped on re-entry (their instance
2933    /// survived the teardown — the first start still spawns them, the flag is
2934    /// app-set afterwards); a `Pause` instance parked by an earlier teardown is
2935    /// **resumed in place** (never double-spawned; like
2936    /// [`resume_pausable`](Self::resume_pausable) this bypasses the gate waits,
2937    /// since the parked instance retains its resources and its slots are empty
2938    /// by design). `Mode::OnDemand` nodes are skipped — they're brought up at
2939    pub async fn start(&self, spawner: &Spawner) -> Result<(), NodeFault> {
2940        #[cfg(feature = "trace")]
2941        self.graph_ref.register();
2942
2943        #[cfg(feature = "trace-self")]
2944        if let Some(node) = self.graph_ref.self_node() {
2945            node.set_task_id(trace::current_task_id().await);
2946            node.handle.flag_set(flag::RUNNING | flag::DETACHED);
2947        }
2948
2949        self.start_nodes(
2950            spawner,
2951            &mut |_, node| {
2952                !(Self::has(shape::ON_DEMAND) && matches!(node.mode(), Mode::OnDemand))
2953                    && !node.is_disabled()
2954                    && !node.is_collateral()
2955                    && !node.is_running()
2956                    && !node.is_detached()
2957            },
2958            false,
2959        )
2960        .await
2961    }
2962
2963    #[cfg(any(feature = "pool", feature = "control"))]
2964    /// Run this node to completion, handling start, driver, and monitoring.
2965    pub async fn run(&self, spawner: &Spawner) -> NodeFault {
2966        if let Err(e) = self.start(spawner).await {
2967            return e;
2968        }
2969        #[cfg(feature = "liveness-monitor")]
2970        match select(self.run_driver(spawner), self.monitor()).await {
2971            Either::First(e) => e,
2972            Either::Second(never) => match never {},
2973        }
2974        #[cfg(not(feature = "liveness-monitor"))]
2975        self.run_driver(spawner).await
2976    }
2977
2978    #[cfg(any(feature = "pool", feature = "control"))]
2979    async fn run_driver(&self, spawner: &Spawner) -> NodeFault {
2980        #[cfg(feature = "bound-deps")]
2981        return match select(self.run_driver_inner(spawner), self.run_binds(spawner)).await {
2982            Either::First(e) | Either::Second(e) => e,
2983        };
2984        #[cfg(not(feature = "bound-deps"))]
2985        self.run_driver_inner(spawner).await
2986    }
2987
2988    #[cfg(all(feature = "bound-deps", any(feature = "pool", feature = "control")))]
2989    async fn run_binds(&self, spawner: &Spawner) -> NodeFault {
2990        loop {
2991            wait_bind().await;
2992            if let Err(e) = self.apply_bind(spawner).await {
2993                return e;
2994            }
2995        }
2996    }
2997
2998    #[cfg(any(feature = "pool", feature = "control"))]
2999    async fn run_driver_inner(&self, spawner: &Spawner) -> NodeFault {
3000        #[cfg(all(feature = "pool", feature = "control"))]
3001        loop {
3002            match select(self.run_pools(spawner), wait_control()).await {
3003                Either::First(e) => return e,
3004                Either::Second(cmd) => {
3005                    if let Err(e) = self.apply_control(cmd, spawner).await {
3006                        return e;
3007                    }
3008                }
3009            }
3010        }
3011        #[cfg(all(feature = "pool", not(feature = "control")))]
3012        return self.run_pools(spawner).await;
3013        #[cfg(all(feature = "control", not(feature = "pool")))]
3014        loop {
3015            let cmd = wait_control().await;
3016            if let Err(e) = self.apply_control(cmd, spawner).await {
3017                return e;
3018            }
3019        }
3020    }
3021
3022    #[cfg(feature = "liveness-monitor")]
3023    /// Monitor node liveness beats and restart nodes that miss deadlines.
3024    pub async fn monitor(&self) -> core::convert::Infallible {
3025        if !Self::has(shape::BEATS)
3026            || !self
3027                .nodes
3028                .iter()
3029                .flatten()
3030                .any(|n| n.beat_timeout().is_some())
3031        {
3032            info!("supervisor: liveness monitor idle (no node declares beat_timeout)");
3033            let never: core::convert::Infallible = core::future::pending().await;
3034            match never {}
3035        }
3036
3037        loop {
3038            let sleep = self
3039                .nodes
3040                .iter()
3041                .flatten()
3042                .filter_map(|n| n.ticks_until_check())
3043                .min()
3044                .unwrap_or(1);
3045            Timer::after(embassy_time::Duration::from_ticks(sleep)).await;
3046
3047            for node in self.nodes.iter().flatten() {
3048                let Some(budget) = node.beat_timeout() else {
3049                    continue;
3050                };
3051                if node.is_detached() || !node.is_running() {
3052                    node.handle.stale_strikes.store(0, Ordering::Release);
3053                    continue;
3054                }
3055
3056                #[cfg(feature = "coupling-observe")]
3057                if Self::has(shape::OBSERVED) && node.poll_observed_writes() {
3058                    node.beat();
3059                    #[cfg(feature = "readiness")]
3060                    if node.ready_on_write() && !node.is_ready() {
3061                        node.set_ready();
3062                    }
3063                }
3064
3065                if node.is_stale(budget) {
3066                    let window = node.beat_window().max(1);
3067                    let strikes = node.handle.stale_strikes.load(Ordering::Acquire);
3068                    if strikes < window {
3069                        let strikes = strikes + 1;
3070                        node.handle.stale_strikes.store(strikes, Ordering::Release);
3071                        if strikes == window {
3072                            warn!(
3073                                "supervisor: {} has not beaten in {} ticks",
3074                                node.name(),
3075                                node.ticks_since_beat()
3076                            );
3077                            emit_health(HealthEvent {
3078                                node,
3079                                kind: HealthKind::Stale {
3080                                    ticks: node.ticks_since_beat(),
3081                                },
3082                            });
3083                        }
3084                    }
3085                } else {
3086                    let had = node.handle.stale_strikes.swap(0, Ordering::AcqRel);
3087                    if had >= node.beat_window().max(1) {
3088                        info!("supervisor: {} is beating", node.name());
3089                        emit_health(HealthEvent {
3090                            node,
3091                            kind: HealthKind::Recovered,
3092                        });
3093                    }
3094                }
3095            }
3096        }
3097    }
3098
3099    /// Start a single node if it is not already running and not detached.
3100    pub async fn start_node(
3101        &self,
3102        node: &'static TaskNode,
3103        spawner: &Spawner,
3104    ) -> Result<(), NodeFault> {
3105        node.reset();
3106        node.handle.flag_clear(flag::COLLATERAL);
3107        if let Some(spawn) = node.cfg.spawn {
3108            match Self::await_gates(node).await {
3109                Ok(()) => {}
3110                Err(_fault) => {
3111                    #[cfg(feature = "bound-deps")]
3112                    if let FaultKind::ReadyDepTimeout { dep } = _fault.kind
3113                        && node.bound_deps().iter().any(|b| core::ptr::eq(*b, dep))
3114                    {
3115                        info!(
3116                            "supervisor: {} parked, bound dep {} not ready",
3117                            node.name(),
3118                            dep.name()
3119                        );
3120                        node.handle.flag_set(flag::BOUND_STOPPED);
3121                        return Ok(());
3122                    }
3123                    return Err(_fault);
3124                }
3125            }
3126            let mut result = spawn(*spawner);
3127            if result.is_err() {
3128                // A just-stopped instance's storage frees one executor pass
3129                embassy_futures::yield_now().await;
3130                result = spawn(*spawner);
3131            }
3132            result.map_err(|err| NodeFault {
3133                node,
3134                kind: FaultKind::Spawn(err),
3135            })?;
3136        }
3137        node.set_running(true);
3138        #[cfg(feature = "bound-deps")]
3139        node.handle.flag_clear(flag::BOUND_STOPPED);
3140        info!("supervisor: started {}", node.name());
3141        Ok(())
3142    }
3143
3144    async fn shutdown_and_wait(&self, node: &'static TaskNode) -> Result<(), NodeFault> {
3145        node.signal_shutdown();
3146        self.await_ack(node).await
3147    }
3148
3149    /// The waiting half of [`shutdown_and_wait`](Self::shutdown_and_wait), for
3150    /// a caller that signalled the node earlier. Returns immediately for a node
3151    /// that already acked.
3152    async fn await_ack(&self, node: &'static TaskNode) -> Result<(), NodeFault> {
3153        if let Either::Second(()) =
3154            select(node.wait_dropped(), Timer::after(node.ack_timeout())).await
3155        {
3156            warn!(
3157                "supervisor: task {} did not ack shutdown within {}ms",
3158                node.name(),
3159                node.ack_timeout().as_millis(),
3160            );
3161            #[cfg(feature = "budget")]
3162            if Self::has(shape::CLAIMS) {
3163                node.release_claims();
3164            }
3165            return Err(NodeFault {
3166                node,
3167                kind: FaultKind::ShutdownTimeout,
3168            });
3169        }
3170        node.set_running(false);
3171        Ok(())
3172    }
3173
3174    async fn stop_nodes(
3175        &self,
3176        select: &mut dyn FnMut(usize, &'static TaskNode) -> bool,
3177        keep_going: bool,
3178    ) -> Result<(), NodeFault> {
3179        let mut chosen = [false; N];
3180        for j in self.order_iter().rev() {
3181            let Some(node) = self.nodes[j] else {
3182                continue;
3183            };
3184            if select(j, node) {
3185                chosen[j] = true;
3186            }
3187        }
3188
3189        self.stop_wave(&chosen, keep_going).await
3190    }
3191
3192    /// The down direction: signal each chosen node the moment every *chosen
3193    /// dependent* of it has resolved, parking on the
3194    /// ack event ([`STOP_EVT`]) between rounds. Two guarantees at once, which
3195    /// signalling everything up front could not give together:
3196    ///
3197    ///   * a `deps:` dependency keeps serving until its stopping dependents
3198    ///     have acked, so a dependent may flush over a link — or drive one
3199    ///     last ioctl through a runner it depends on — during its own
3200    ///     shutdown;
3201    ///   * a node whose shutdown waits on a node it has NO edge to (a
3202    ///     producer draining a [`Leased`] signal) is never kept waiting on
3203    ///     the wave's own progress: every node without an unresolved chosen
3204    async fn stop_wave(&self, chosen: &[bool; N], keep_going: bool) -> Result<(), NodeFault> {
3205        const UNSIGNALED: u32 = u32::MAX;
3206        let epoch = embassy_time::Instant::now();
3207        let mut resolved = [false; N];
3208        let mut signaled: [u32; N] = [UNSIGNALED; N];
3209        let mut first_err = Ok(());
3210        loop {
3211            for j in self.order_iter().rev() {
3212                if !chosen[j] || resolved[j] || signaled[j] != UNSIGNALED {
3213                    continue;
3214                }
3215                let held = (0..N).any(|k| {
3216                    chosen[k] && !resolved[k] && self.topo.deps_of(k as u8).contains(&(j as u8))
3217                });
3218                if held {
3219                    continue;
3220                }
3221                self.nodes[j]
3222                    .expect("a chosen slot is occupied")
3223                    .signal_shutdown();
3224                signaled[j] = ((embassy_time::Instant::now() - epoch).as_millis())
3225                    .min(UNSIGNALED as u64 - 1) as u32;
3226            }
3227
3228            let mut progress = false;
3229            for j in self.order_iter().rev() {
3230                if !chosen[j] || resolved[j] {
3231                    continue;
3232                }
3233                let node = self.nodes[j].expect("a chosen slot is occupied");
3234                if node.has_dropped() {
3235                    node.set_running(false);
3236                    resolved[j] = true;
3237                    progress = true;
3238                }
3239            }
3240            if (0..N).all(|j| !chosen[j] || resolved[j]) {
3241                return first_err;
3242            }
3243            if progress {
3244                continue;
3245            }
3246
3247            let now_ms = (embassy_time::Instant::now() - epoch).as_millis();
3248            let mut deadline: Option<embassy_time::Instant> = None;
3249            for j in self.order_iter().rev() {
3250                if !chosen[j] || resolved[j] || signaled[j] == UNSIGNALED {
3251                    continue;
3252                }
3253                let node = self.nodes[j].expect("a chosen slot is occupied");
3254                let due_ms = signaled[j] as u64 + node.ack_timeout().as_millis();
3255                if now_ms < due_ms {
3256                    let due = epoch + embassy_time::Duration::from_millis(due_ms);
3257                    deadline = Some(deadline.map_or(due, |d| d.min(due)));
3258                    continue;
3259                }
3260                warn!(
3261                    "supervisor: task {} did not ack shutdown within {}ms",
3262                    node.name(),
3263                    node.ack_timeout().as_millis(),
3264                );
3265                #[cfg(feature = "budget")]
3266                if Self::has(shape::CLAIMS) {
3267                    node.release_claims();
3268                }
3269                let fault = NodeFault {
3270                    node,
3271                    kind: FaultKind::ShutdownTimeout,
3272                };
3273                if !keep_going {
3274                    return Err(fault);
3275                }
3276                if first_err.is_ok() {
3277                    first_err = Err(fault);
3278                }
3279                resolved[j] = true;
3280                progress = true;
3281            }
3282            if progress {
3283                continue;
3284            }
3285            let deadline = deadline.expect("an unresolved wave has a signalled node");
3286            let _ = embassy_futures::select::select(STOP_EVT.wait(), Timer::at(deadline)).await;
3287        }
3288    }
3289
3290    async fn start_nodes(
3291        &self,
3292        spawner: &Spawner,
3293        select: &mut dyn FnMut(usize, &'static TaskNode) -> bool,
3294        keep_going: bool,
3295    ) -> Result<(), NodeFault> {
3296        let mut pending = [false; N];
3297        for j in self.order_iter() {
3298            let Some(node) = self.nodes[j] else {
3299                continue;
3300            };
3301            if select(j, node) {
3302                pending[j] = true;
3303            }
3304        }
3305
3306        // When each node's deps resolved — the start of its gate budget, and
3307        const UNARMED: u32 = u32::MAX;
3308        let epoch = embassy_time::Instant::now();
3309        let mut armed: [u32; N] = [UNARMED; N];
3310        let mut first_err = Ok(());
3311        loop {
3312            let mut progress = false;
3313            let mut waiting = false;
3314            let mut respawn_wait = false;
3315            'nodes: for j in self.order_iter() {
3316                if !pending[j] {
3317                    continue;
3318                }
3319                let node = self.nodes[j].expect("a pending slot is occupied");
3320                if self
3321                    .topo
3322                    .deps_of(j as u8)
3323                    .iter()
3324                    .any(|&d| pending[d as usize])
3325                {
3326                    waiting = true;
3327                    continue;
3328                }
3329                let budget_start = if armed[j] != UNARMED {
3330                    armed[j]
3331                } else {
3332                    {
3333                        let now = ((embassy_time::Instant::now() - epoch).as_millis())
3334                            .min(UNARMED as u64 - 1) as u32;
3335                        armed[j] = now;
3336                        // Parked `Pause` instance: resume in place, before the
3337                        // reset that clears the ack flags this reads — and
3338                        // deliberately without the gate waits, since the
3339                        // parked instance retains its resources.
3340                        if Self::has(shape::PAUSE)
3341                            && matches!(node.mode(), Mode::Pause)
3342                            && node.has_acked_stop()
3343                        {
3344                            node.reset();
3345                            info!("supervisor: resuming {} in place", node.name());
3346                            node.signal_resume();
3347                            node.set_running(true);
3348                            #[cfg(feature = "bound-deps")]
3349                            node.handle.flag_clear(flag::BOUND_STOPPED);
3350                            pending[j] = false;
3351                            progress = true;
3352                            continue;
3353                        }
3354                        node.reset();
3355                        info!("supervisor: spawning {} ({})", node.name(), node.mode());
3356                        now
3357                    }
3358                };
3359                let Some(spawn) = node.cfg.spawn else {
3360                    // A parked node the app spawns itself; only marked.
3361                    node.set_running(true);
3362                    pending[j] = false;
3363                    progress = true;
3364                    continue;
3365                };
3366                // The gate sequence — executor slot, resources, ready deps —
3367                // tested without blocking; the first unsatisfied gate defers
3368                // the node to the next round, or faults it once its budget is
3369                // spent.
3370                let overdue = (embassy_time::Instant::now() - epoch).as_millis()
3371                    >= budget_start as u64 + node.slot_timeout().as_millis();
3372                let mut unsatisfied = |kind: FaultKind| -> Result<bool, NodeFault> {
3373                    if !overdue {
3374                        return Ok(true);
3375                    }
3376                    let fault = NodeFault { node, kind };
3377                    if !keep_going {
3378                        return Err(fault);
3379                    }
3380                    warn!("supervisor: {}", fault);
3381                    if first_err.is_ok() {
3382                        first_err = Err(fault);
3383                    }
3384                    Ok(false)
3385                };
3386                let blocked = if Self::has(shape::EXEC_SLOTS)
3387                    && node.cfg.spawn_slot.is_some_and(|s| s.get().is_none())
3388                {
3389                    Some(unsatisfied(FaultKind::ExecutorSlotEmpty)?)
3390                } else if Self::has(shape::RESOURCES)
3391                    && node.cfg.resource_gates.iter().any(|g| !g.is_filled())
3392                {
3393                    Some(unsatisfied(FaultKind::ResourceMissing)?)
3394                } else {
3395                    None
3396                };
3397                #[cfg(feature = "readiness")]
3398                let blocked = match blocked {
3399                    Some(b) => Some(b),
3400                    None if Self::has(shape::READY_DEPS) => {
3401                        match node.ready_deps().iter().find(|d| !d.is_ready()) {
3402                            Some(dep) => {
3403                                #[cfg(feature = "bound-deps")]
3404                                if overdue
3405                                    && node.bound_deps().iter().any(|b| core::ptr::eq(*b, *dep))
3406                                {
3407                                    info!(
3408                                        "supervisor: {} parked, bound dep {} not ready",
3409                                        node.name(),
3410                                        dep.name()
3411                                    );
3412                                    node.handle.flag_set(flag::BOUND_STOPPED);
3413                                    pending[j] = false;
3414                                    progress = true;
3415                                    continue 'nodes;
3416                                }
3417                                Some(unsatisfied(FaultKind::ReadyDepTimeout { dep })?)
3418                            }
3419                            None => None,
3420                        }
3421                    }
3422                    None => None,
3423                };
3424                match blocked {
3425                    Some(true) => {
3426                        waiting = true;
3427                        continue 'nodes;
3428                    }
3429                    Some(false) => {
3430                        pending[j] = false;
3431                        progress = true;
3432                        continue 'nodes;
3433                    }
3434                    None => {}
3435                }
3436                if let Err(err) = spawn(*spawner) {
3437                    // A respawn can catch the previous instance's storage
3438                    if unsatisfied(FaultKind::Spawn(err))? {
3439                        respawn_wait = true;
3440                        waiting = true;
3441                    } else {
3442                        pending[j] = false;
3443                        progress = true;
3444                    }
3445                    continue;
3446                }
3447                node.set_running(true);
3448                #[cfg(feature = "bound-deps")]
3449                node.handle.flag_clear(flag::BOUND_STOPPED);
3450                pending[j] = false;
3451                progress = true;
3452            }
3453            if !waiting {
3454                return first_err;
3455            }
3456            if progress {
3457                continue;
3458            }
3459            if respawn_wait {
3460                embassy_futures::yield_now().await;
3461                continue;
3462            }
3463            let deadline_ms = self
3464                .order_iter()
3465                .filter(|&j| pending[j] && armed[j] != UNARMED)
3466                .map(|j| {
3467                    armed[j] as u64
3468                        + self.nodes[j]
3469                            .expect("a pending slot is occupied")
3470                            .slot_timeout()
3471                            .as_millis()
3472                })
3473                .min()
3474                .expect("a waiting wave has an armed node");
3475            let deadline = epoch + embassy_time::Duration::from_millis(deadline_ms);
3476            let _ = embassy_futures::select::select(GATE_EVT.wait(), Timer::at(deadline)).await;
3477        }
3478    }
3479
3480    /// Stop a single node, waiting for its shutdown ack.
3481    pub async fn stop_node(&self, node: &'static TaskNode) -> Result<(), NodeFault> {
3482        if !node.is_running() || node.is_detached() {
3483            return Ok(());
3484        }
3485        self.shutdown_and_wait(node).await?;
3486        info!("supervisor: stopped {}", node.name());
3487        Ok(())
3488    }
3489
3490    /// Stop every **running** node, dependents before their dependencies.
3491    /// Down `OnDemand` nodes are skipped (no instance to ack). Pause-mode nodes
3492    /// ack and park on `wait_resume()`; Terminate/OnDemand nodes exit.
3493    ///
3494    /// The stop runs as a wave: a node is signalled once every dependent
3495    /// stopping with it has acked — a `deps:` dependency keeps serving
3496    /// through its dependents' cleanup — and a node with no such dependents
3497    pub async fn teardown(&self) -> Result<(), NodeFault> {
3498        self.stop_nodes(
3499            &mut |_, node| {
3500                if !node.is_running() || node.is_detached() {
3501                    return false;
3502                }
3503                info!("supervisor: tearing down {}", node.name());
3504                true
3505            },
3506            false,
3507        )
3508        .await
3509    }
3510
3511    /// Like [`teardown`](Self::teardown), but do not clear the shutdown flags
3512    /// so a later [`respawn_terminate`](Self::respawn_terminate) can restart.
3513    pub async fn teardown_continue(&self) -> Result<(), NodeFault> {
3514        self.stop_nodes(
3515            &mut |_, node| {
3516                if !node.is_running() || node.is_detached() {
3517                    return false;
3518                }
3519                info!("supervisor: tearing down {}", node.name());
3520                true
3521            },
3522            true,
3523        )
3524        .await
3525    }
3526
3527    /// Resume a single parked Pause-mode node.
3528    pub fn resume_node(&self, node: &'static TaskNode) {
3529        if !Self::has(shape::PAUSE)
3530            || !matches!(node.mode(), Mode::Pause)
3531            || node.is_disabled()
3532            || node.is_collateral()
3533            || node.is_detached()
3534            || !node.has_acked_stop()
3535        {
3536            return;
3537        }
3538        node.reset();
3539        info!("supervisor: resuming {}", node.name());
3540        node.signal_resume();
3541        node.set_running(true);
3542    }
3543
3544    /// Signal every **parked** Pause-mode node to resume. Cheap and synchronous —
3545    /// the tasks were parked on `wait_resume()` and pick up immediately. Called
3546    /// separately from `respawn_terminate` so the application can fire resume
3547    /// independently of the respawn step. Disabled (manually-paused) nodes are
3548    /// skipped so a manual pause sticks, detached (self-managed) Pause nodes are
3549    /// left parked, and — as in [`resume_node`](Self::resume_node) — a node
3550    /// without a parked instance (`has_acked_stop`) is skipped: signaling it
3551    /// would latch `resume_wake` with no waiter, and the node's *next* park
3552    pub fn resume_pausable(&self) {
3553        if !Self::has(shape::PAUSE) {
3554            return;
3555        }
3556        for j in self.order_iter() {
3557            let Some(node) = self.nodes[j] else {
3558                continue;
3559            };
3560            if matches!(node.mode(), Mode::Pause)
3561                && !node.is_disabled()
3562                && !node.is_collateral()
3563                && !node.is_detached()
3564                && node.has_acked_stop()
3565            {
3566                node.reset();
3567                info!("supervisor: resuming {}", node.name());
3568                node.signal_resume();
3569                node.set_running(true);
3570            }
3571        }
3572    }
3573
3574    /// Restart every terminated Terminate-mode node that is not running,
3575    /// disabled, or detached.
3576    pub async fn respawn_terminate(&self, spawner: &Spawner) -> Result<(), NodeFault> {
3577        self.start_nodes(
3578            spawner,
3579            &mut |_, node| {
3580                matches!(node.mode(), Mode::Terminate)
3581                    && !node.is_disabled()
3582                    && !node.is_collateral()
3583                    && !node.is_detached()
3584                    && !node.is_running()
3585            },
3586            false,
3587        )
3588        .await
3589    }
3590}
3591
3592#[cfg(any(feature = "control", feature = "pool"))]
3593impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
3594    fn index_of(&self, node: &'static TaskNode) -> Option<usize> {
3595        self.nodes
3596            .iter()
3597            .position(|n| n.is_some_and(|x| core::ptr::eq(x, node)))
3598    }
3599
3600    /// Whether every dependency of `node` is currently running, resolved through
3601    /// the graph's index table. The pool driver checks this before growing a
3602    #[cfg(feature = "pool")]
3603    pub(crate) fn deps_running(&self, node: &'static TaskNode) -> bool {
3604        match self.index_of(node) {
3605            Some(i) => self
3606                .topo
3607                .deps_of(i as u8)
3608                .iter()
3609                .all(|&di| self.nodes[di as usize].is_some_and(|n| n.is_running())),
3610            None => false,
3611        }
3612    }
3613}
3614
3615#[cfg(feature = "control")]
3616impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
3617    /// Seed a membership set with `target` plus — if `target` belongs to an
3618    /// elastic pool — every member of that pool, so control is applied to the
3619    /// whole pool atomically. Pool membership is read from `GRAPH.pools`; with no
3620    /// pools (the `pool` feature off, or none declared) this is just `{target}`.
3621    fn seed(&self, target: &'static TaskNode, set: &mut [bool; N]) {
3622        if let Some(i) = self.index_of(target) {
3623            set[i] = true;
3624        }
3625        #[cfg(feature = "pool")]
3626        if Self::has(shape::POOLS) {
3627            for pool in self.pools {
3628                let members = pool.members();
3629                if members.iter().any(|m| core::ptr::eq(*m, target)) {
3630                    for m in members {
3631                        if let Some(i) = self.index_of(m) {
3632                            set[i] = true;
3633                        }
3634                    }
3635                }
3636            }
3637        }
3638    }
3639
3640    fn collect_dependents(&self, set: &mut [bool; N]) {
3641        for j in self.order_iter() {
3642            if set[j] {
3643                continue;
3644            }
3645            let Some(node) = self.nodes[j] else {
3646                continue;
3647            };
3648            if node.is_detached() {
3649                continue;
3650            }
3651            if self
3652                .topo
3653                .deps_of(j as u8)
3654                .iter()
3655                .any(|&di| set[di as usize])
3656            {
3657                set[j] = true;
3658            }
3659        }
3660    }
3661
3662    /// Apply a control command to the requested node.
3663    pub async fn apply_control(
3664        &self,
3665        cmd: ControlCommand,
3666        spawner: &Spawner,
3667    ) -> Result<(), NodeFault> {
3668        match cmd.op {
3669            ControlOp::Deactivate => self.deactivate(cmd.node).await,
3670            ControlOp::Activate => {
3671                self.activate(cmd.node, spawner).await;
3672                Ok(())
3673            }
3674            #[cfg(feature = "restart")]
3675            ControlOp::Restart => match self.restart(cmd.node, spawner).await {
3676                Ok(()) => Ok(()),
3677                Err(e) if matches!(e.kind, FaultKind::ShutdownTimeout) => Err(e),
3678                Err(e) => {
3679                    let node = e.node;
3680                    warn!(
3681                        "supervisor: {} did not come back after restart",
3682                        node.name()
3683                    );
3684                    Ok(())
3685                }
3686            },
3687            #[allow(unreachable_patterns)]
3688            _ => Ok(()),
3689        }
3690    }
3691
3692    /// Deactivate the target node and all of its dependents.
3693    pub async fn deactivate(&self, target: &'static TaskNode) -> Result<(), NodeFault> {
3694        let mut seed = [false; N];
3695        self.seed(target, &mut seed);
3696        let mut set = seed;
3697        self.collect_dependents(&mut set);
3698
3699        // Down in reverse topo order (dependents before their deps).
3700        self.stop_nodes(
3701            &mut |j, node| {
3702                if !set[j] {
3703                    return false;
3704                }
3705                // A detached node is self-managed — never control-stop it. The growth
3706                // loop keeps detached *dependents* out of the set; this also covers a
3707                // detached node that was seeded directly (or a detached pool member).
3708                // Without it a detached one-shot that already exited (stale
3709                // `is_running`, no ack path) would be signalled a shutdown it can never
3710                // acknowledge, failing here with a spurious missed-ack fault.
3711                if node.is_detached() {
3712                    return false;
3713                }
3714                if seed[j] {
3715                    node.set_disabled(true);
3716                } else {
3717                    node.handle.flag_set(flag::COLLATERAL);
3718                }
3719                if !node.is_running() {
3720                    return false;
3721                }
3722                info!("supervisor: control-stop {}", node.name());
3723                true
3724            },
3725            false,
3726        )
3727        .await
3728    }
3729
3730    /// Bring `target` (and its pool, and every transitive dependency) up, in
3731    /// topological order so each dependency starts before its dependent — the
3732    /// cascading "turn this subsystem on" verb, and the entry half of the
3733    /// subordinate sub-graph pattern's one-graph variant: `activate` on a
3734    pub async fn activate(&self, target: &'static TaskNode, spawner: &Spawner) {
3735        let mut set = [false; N];
3736        self.seed(target, &mut set);
3737
3738        // Grow the set to include transitive deps. Walk dependents-first
3739        // (reverse topo); when a set member is seen, pull in its direct deps.
3740        // A detached member's `deps:` are start-ordering only (the node is
3741        for j in self.order_iter().rev() {
3742            if set[j] && !self.nodes[j].is_some_and(|n| n.is_detached()) {
3743                for &di in self.topo.deps_of(j as u8) {
3744                    set[di as usize] = true;
3745                }
3746            }
3747        }
3748        for j in self.order_iter() {
3749            if set[j]
3750                && let Some(node) = self.nodes[j]
3751                && !node.is_detached()
3752            {
3753                node.set_disabled(false);
3754            }
3755        }
3756        let mut held = [false; N]; // disabled, or depends on a disabled node
3757        let mut revive = [false; N];
3758        for j in self.order_iter() {
3759            let Some(node) = self.nodes[j] else {
3760                continue;
3761            };
3762            if node.is_detached() {
3763                continue;
3764            }
3765            let blocked = self
3766                .topo
3767                .deps_of(j as u8)
3768                .iter()
3769                .any(|&di| held[di as usize]);
3770            held[j] = blocked || node.is_disabled();
3771            if !blocked && node.is_collateral() {
3772                node.handle.flag_clear(flag::COLLATERAL);
3773                revive[j] = !node.is_disabled();
3774            }
3775        }
3776
3777        let _ = self
3778            .start_nodes(
3779                spawner,
3780                &mut |j, node| {
3781                    if !(set[j] || revive[j]) || node.is_detached() {
3782                        return false;
3783                    }
3784                    !node.is_running()
3785                        && !node.is_collateral()
3786                        && !(Self::has(shape::ON_DEMAND) && matches!(node.mode(), Mode::OnDemand))
3787                },
3788                true,
3789            )
3790            .await;
3791        #[cfg(feature = "pool")]
3792        if Self::has(shape::POOLS) {
3793            request_scale();
3794        }
3795    }
3796}
3797
3798#[cfg(feature = "bound-deps")]
3799fn is_serving(node: &TaskNode) -> bool {
3800    node.is_running() && node.is_ready()
3801}
3802
3803#[cfg(feature = "bound-deps")]
3804impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
3805    /// React to bound-dependency readiness changes by stopping or resuming nodes.
3806    pub async fn apply_bind(&self, spawner: &Spawner) -> Result<(), NodeFault> {
3807        if !Self::has(shape::BOUND_DEPS) {
3808            return Ok(());
3809        }
3810        let mut stopping = [false; N];
3811        for j in self.order_iter() {
3812            let Some(node) = self.nodes[j] else {
3813                continue;
3814            };
3815            if node.is_detached() || !node.is_running() {
3816                continue;
3817            }
3818            let mut down = false;
3819            for d in node.bound_deps() {
3820                if !is_serving(d) {
3821                    down = true;
3822                    break;
3823                }
3824                for (k, slot) in self.nodes.iter().enumerate() {
3825                    if stopping[k] && slot.is_some_and(|x| core::ptr::eq(x, *d)) {
3826                        down = true;
3827                        break;
3828                    }
3829                }
3830                if down {
3831                    break;
3832                }
3833            }
3834            stopping[j] = down;
3835        }
3836
3837        self.stop_nodes(
3838            &mut |j, node| {
3839                if !stopping[j] {
3840                    return false;
3841                }
3842                info!(
3843                    "supervisor: bound-stop {} (a bound provider withdrew readiness)",
3844                    node.name()
3845                );
3846                node.handle.flag_set(flag::BOUND_STOPPED);
3847                true
3848            },
3849            false,
3850        )
3851        .await?;
3852
3853        for j in self.order_iter() {
3854            let Some(node) = self.nodes[j] else {
3855                continue;
3856            };
3857            if !node.handle.flag(flag::BOUND_STOPPED) {
3858                continue;
3859            }
3860            if node.is_disabled() || node.is_collateral() || node.is_detached() || node.is_running()
3861            {
3862                continue;
3863            }
3864            if !node.bound_deps().iter().all(|d| is_serving(d)) {
3865                continue;
3866            }
3867            match node.mode() {
3868                Mode::Terminate => {
3869                    info!("supervisor: bound-restart {}", node.name());
3870                    if self.start_node(node, spawner).await.is_err() {
3871                        warn!("supervisor: {} could not be bound-restarted", node.name());
3872                    }
3873                }
3874                Mode::Pause if Self::has(shape::PAUSE) => {
3875                    info!("supervisor: bound-resume {}", node.name());
3876                    node.reset();
3877                    node.signal_resume();
3878                    node.set_running(true);
3879                    node.handle.flag_clear(flag::BOUND_STOPPED);
3880                }
3881                Mode::Pause => {}
3882                Mode::OnDemand => {
3883                    node.handle.flag_clear(flag::BOUND_STOPPED);
3884                    #[cfg(feature = "pool")]
3885                    if Self::has(shape::POOLS) {
3886                        request_scale();
3887                    }
3888                }
3889            }
3890        }
3891        Ok(())
3892    }
3893}
3894
3895#[cfg(feature = "restart")]
3896impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
3897    /// Restart the target node and all of its dependents.
3898    pub async fn restart(
3899        &self,
3900        target: &'static TaskNode,
3901        spawner: &Spawner,
3902    ) -> Result<(), NodeFault> {
3903        let mut set = [false; N];
3904        self.seed(target, &mut set);
3905        self.collect_dependents(&mut set);
3906
3907        info!(
3908            "supervisor: restarting {} and its dependents",
3909            target.name()
3910        );
3911
3912        // Down, dependents first. No `disabled` latch: this is a cycle, not a
3913        // stop, and a concurrent observer must never see the subtree as
3914        // deliberately disabled.
3915        self.stop_nodes(
3916            &mut |j, node| {
3917                if !set[j] || node.is_detached() || !node.is_running() {
3918                    return false;
3919                }
3920                info!("supervisor: stopping {}", node.name());
3921                true
3922            },
3923            false,
3924        )
3925        .await?;
3926
3927        // Up, dependencies first, each through the full gate sequence — as a
3928        // wave, aborting on the first fault.
3929        let r = self
3930            .start_nodes(
3931                spawner,
3932                &mut |j, node| {
3933                    set[j]
3934                        && !node.is_detached()
3935                        && !node.is_disabled()
3936                        && !node.is_collateral()
3937                        && !node.is_running()
3938                        && !(Self::has(shape::ON_DEMAND) && matches!(node.mode(), Mode::OnDemand))
3939                },
3940                false,
3941            )
3942            .await;
3943        #[cfg(feature = "pool")]
3944        if Self::has(shape::POOLS) {
3945            request_scale();
3946        }
3947
3948        r
3949    }
3950}
3951
3952// ─── Topological sort (Kahn's algorithm, const) ───────────────────────────
3953
3954#[doc(hidden)]
3955#[must_use]
3956pub const fn topo_sort_const<const N: usize>(deps: &[&'static [u8]; N]) -> [u8; N] {
3957    assert!(
3958        N <= 256,
3959        "supervisor graph exceeds 256 node slots (indices are u8)"
3960    );
3961    // in_degree[i] = number of deps of node i not yet resolved.
3962    let mut in_degree = [0u8; N];
3963    let mut i = 0;
3964    while i < N {
3965        in_degree[i] = deps[i].len() as u8;
3966        i += 1;
3967    }
3968
3969    // Queue (fixed array, head/tail indices) seeded with the dependency-free nodes.
3970    let mut queue = [0u8; N];
3971    let mut tail = 0;
3972    i = 0;
3973    while i < N {
3974        if in_degree[i] == 0 {
3975            queue[tail] = i as u8;
3976            tail += 1;
3977        }
3978        i += 1;
3979    }
3980
3981    let mut order = [0u8; N];
3982    let mut produced = 0;
3983    let mut head = 0;
3984    while head < tail {
3985        let node = queue[head] as usize;
3986        head += 1;
3987        order[produced] = node as u8;
3988        produced += 1;
3989
3990        // Decrement the in-degree of every node that depends on `node`.
3991        let mut j = 0;
3992        while j < N {
3993            if in_degree[j] != 0 {
3994                let mut depends = false;
3995                let mut k = 0;
3996                while k < deps[j].len() {
3997                    if deps[j][k] as usize == node {
3998                        depends = true;
3999                    }
4000                    k += 1;
4001                }
4002                if depends {
4003                    in_degree[j] -= 1;
4004                    if in_degree[j] == 0 {
4005                        queue[tail] = j as u8;
4006                        tail += 1;
4007                    }
4008                }
4009            }
4010            j += 1;
4011        }
4012    }
4013
4014    // A cycle leaves some nodes unproduced. During const eval this panic is a
4015    // compile error, so cyclic graphs are rejected at build time. `core::panic!`
4016    // (not the crate's defmt-shimmed `panic!`) keeps this const-evaluable.
4017    if produced != N {
4018        core::panic!("supervisor_graph!: dependency cycle");
4019    }
4020    order
4021}
4022
4023#[cfg(feature = "pool")]
4024mod pool;
4025#[cfg(feature = "pool")]
4026pub use pool::*;
4027
4028#[cfg(feature = "budget")]
4029mod budget;
4030#[cfg(feature = "budget")]
4031pub use budget::*;
4032
4033#[cfg(feature = "veto")]
4034mod veto;
4035#[cfg(feature = "veto")]
4036pub use veto::*;
4037
4038#[cfg(feature = "coupling")]
4039mod stamped;
4040#[cfg(feature = "coupling")]
4041pub use stamped::Stamped;
4042
4043#[cfg(feature = "dataflow")]
4044mod dataflow;
4045#[cfg(feature = "dataflow")]
4046pub use dataflow::*;
4047
4048#[cfg(feature = "data-deps")]
4049mod data_deps;
4050#[cfg(feature = "data-deps")]
4051pub use data_deps::*;
4052
4053#[cfg(feature = "graph-ref")]
4054mod graph_ref;
4055#[cfg(feature = "graph-ref")]
4056pub use graph_ref::*;
4057
4058#[cfg(feature = "trace")]
4059/// Runtime tracing hooks and task introspection helpers.
4060pub mod trace;
4061
4062#[cfg(feature = "trace")]
4063#[doc(hidden)]
4064pub use embassy_executor as __executor;
4065
4066/// The executor's trace hooks, forwarding to the [`trace`] recorders.
4067///
4068/// `supervisor_graph!` expands this once, at the unnamed graph's declaration
4069/// site, under `trace-hooks`; it lives here rather than in the proc-macro so
4070/// the hook *bodies* follow this crate's build, not the macro crate's. The
4071/// executor's hook API is an implementation detail that has already changed
4072/// shape once: embassy-executor 0.10 links seven `_embassy_trace_*` symbols
4073/// taking `u32` ids, while its git main (the next release) takes a
4074/// `raw::trace::Trace` impl registered with `trace_impl!` and passes
4075/// `ExecutorId`/`TaskRef`. `--cfg embassy_supervisor_trace_v2` (RUSTFLAGS)
4076/// selects the latter; the recorders keep their `u32` keys either way, ids
4077/// narrowing through [`trace::task_key`] / [`trace::executor_key`] at the hook.
4078#[cfg(all(feature = "trace", not(embassy_supervisor_trace_v2)))]
4079#[doc(hidden)]
4080#[macro_export]
4081macro_rules! __sv_trace_hooks {
4082    () => {
4083        #[unsafe(no_mangle)]
4084        fn _embassy_trace_poll_start(executor_id: u32) {
4085            $crate::trace::on_poll_start(executor_id);
4086        }
4087        #[unsafe(no_mangle)]
4088        fn _embassy_trace_task_new(_executor_id: u32, _task_id: u32) {}
4089        #[unsafe(no_mangle)]
4090        fn _embassy_trace_task_end(executor_id: u32, task_id: u32) {
4091            $crate::trace::on_task_end(executor_id, task_id);
4092        }
4093        #[unsafe(no_mangle)]
4094        fn _embassy_trace_task_exec_begin(executor_id: u32, task_id: u32) {
4095            $crate::trace::on_task_exec_begin(executor_id, task_id);
4096        }
4097        #[unsafe(no_mangle)]
4098        fn _embassy_trace_task_exec_end(executor_id: u32, task_id: u32) {
4099            $crate::trace::on_task_exec_end(executor_id, task_id);
4100        }
4101        #[unsafe(no_mangle)]
4102        fn _embassy_trace_task_ready_begin(_executor_id: u32, _task_id: u32) {}
4103        #[unsafe(no_mangle)]
4104        fn _embassy_trace_executor_idle(executor_id: u32) {
4105            $crate::trace::on_executor_idle(executor_id);
4106        }
4107    };
4108}
4109
4110#[cfg(all(feature = "trace", embassy_supervisor_trace_v2))]
4111#[doc(hidden)]
4112#[macro_export]
4113macro_rules! __sv_trace_hooks {
4114    () => {
4115        const _: () = {
4116            // `trace_impl!` spells its shims' parameter types unqualified, so
4117            // both must be in scope where it expands.
4118            use $crate::__executor::ExecutorId;
4119            use $crate::__executor::raw::TaskRef;
4120
4121            struct __SvTraceHooks;
4122            impl $crate::__executor::raw::trace::Trace for __SvTraceHooks {
4123                fn poll_start(executor: ExecutorId) {
4124                    $crate::trace::on_poll_start($crate::trace::executor_key(executor));
4125                }
4126                fn task_new(_executor: ExecutorId, _task: TaskRef) {}
4127                fn task_end(executor: ExecutorId, task: TaskRef) {
4128                    $crate::trace::on_task_end(
4129                        $crate::trace::executor_key(executor),
4130                        $crate::trace::task_key(task.id()),
4131                    );
4132                }
4133                fn task_exec_begin(executor: ExecutorId, task: TaskRef) {
4134                    $crate::trace::on_task_exec_begin(
4135                        $crate::trace::executor_key(executor),
4136                        $crate::trace::task_key(task.id()),
4137                    );
4138                }
4139                fn task_exec_end(executor: ExecutorId, task: TaskRef) {
4140                    $crate::trace::on_task_exec_end(
4141                        $crate::trace::executor_key(executor),
4142                        $crate::trace::task_key(task.id()),
4143                    );
4144                }
4145                fn task_ready_begin(_executor: ExecutorId, _task: TaskRef) {}
4146                fn executor_idle(executor: ExecutorId) {
4147                    $crate::trace::on_executor_idle($crate::trace::executor_key(executor));
4148                }
4149                fn idle() {}
4150                fn task_name_set(_task: TaskRef, _name: &'static str) {}
4151                fn task_priority_set(_task: TaskRef, _priority: u8) {}
4152                fn task_deadline_set(_task: TaskRef, _deadline: u64) {}
4153            }
4154            $crate::__executor::trace_impl!(__SvTraceHooks);
4155        };
4156    };
4157}
4158
4159#[cfg(feature = "macros")]
4160pub use embassy_supervisor_macros::supervisor_fragment;
4161#[cfg(feature = "macros")]
4162pub use embassy_supervisor_macros::supervisor_graph;
4163
4164#[cfg(feature = "macros")]
4165#[macro_export]
4166/// Compose a graph out of one or more `supervisor_fragment!` declarations.
4167///
4168/// The fragments are spliced into the final graph at the compose site. This
4169/// macro is re-exported by `embassy-supervisor` when the `macros` feature is
4170/// enabled.
4171macro_rules! compose_graph {
4172    (name: $n:ident, fragments: [$f:path $(, $r:path)* $(,)?], graph: {$($g:tt)*}) => {
4173        $f! { @emit $crate::compose_graph, [$($r),*], {name: $n;}, {$($g)*} }
4174    };
4175    (fragments: [$f:path $(, $r:path)* $(,)?], graph: {$($g:tt)*}) => {
4176        $f! { @emit $crate::compose_graph, [$($r),*], {}, {$($g)*} }
4177    };
4178    (@next [], {$($acc:tt)*}, {$($g:tt)*}) => {
4179        $crate::supervisor_graph! { $($acc)* $($g)* }
4180    };
4181    (@next [$f:path $(, $r:path)*], {$($acc:tt)*}, $g:tt) => {
4182        $f! { @emit $crate::compose_graph, [$($r),*], {$($acc)*}, $g }
4183    };
4184}
4185
4186#[doc(hidden)]
4187pub mod _export {
4188    pub use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
4189    pub use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
4190    pub use embassy_sync::signal::Signal;
4191    pub use embassy_time::Duration;
4192}