#![no_std]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#[macro_use]
mod fmt;
use core::sync::atomic::Ordering;
use embassy_executor::{SpawnError, Spawner};
use embassy_futures::select::{Either, select};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
#[cfg(feature = "control")]
use embassy_sync::channel::Channel;
use embassy_sync::signal::Signal;
use embassy_time::Timer;
use heapless::Vec;
use portable_atomic::AtomicBool;
#[cfg(feature = "pool")]
static SCALE_REQ: Signal<CriticalSectionRawMutex, ()> = Signal::new();
pub fn request_scale() {
#[cfg(feature = "pool")]
SCALE_REQ.signal(());
}
#[cfg(feature = "pool")]
pub async fn wait_scale() {
SCALE_REQ.wait().await;
}
#[cfg(feature = "control")]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ControlOp {
Activate,
Deactivate,
}
#[cfg(feature = "control")]
#[derive(Clone, Copy)]
pub struct ControlCommand {
pub node: &'static TaskNode,
pub op: ControlOp,
}
#[cfg(feature = "control")]
static CONTROL_REQ: Channel<CriticalSectionRawMutex, ControlCommand, 4> = Channel::new();
#[cfg(feature = "control")]
pub fn request_control(node: &'static TaskNode, op: ControlOp) {
let _ = CONTROL_REQ.try_send(ControlCommand { node, op });
}
#[cfg(feature = "control")]
pub async fn wait_control() -> ControlCommand {
CONTROL_REQ.receive().await
}
const SHUTDOWN_ACK_TIMEOUT_MS: u64 = 2_000;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Terminate,
Pause,
OnDemand,
}
impl Mode {
pub fn as_str(&self) -> &'static str {
match self {
Mode::Terminate => "terminate",
Mode::Pause => "pause",
Mode::OnDemand => "ondemand",
}
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Mode {
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "{}", self.as_str());
}
}
pub struct TaskHandle {
shutdown: AtomicBool,
shutdown_wake: Signal<CriticalSectionRawMutex, ()>,
dropped: AtomicBool,
dropped_wake: Signal<CriticalSectionRawMutex, ()>,
running: AtomicBool,
busy: AtomicBool,
resume_wake: Signal<CriticalSectionRawMutex, ()>,
disabled: AtomicBool,
detached: AtomicBool,
}
impl TaskHandle {
const fn new() -> Self {
Self {
shutdown: AtomicBool::new(false),
shutdown_wake: Signal::new(),
dropped: AtomicBool::new(false),
dropped_wake: Signal::new(),
running: AtomicBool::new(false),
busy: AtomicBool::new(false),
resume_wake: Signal::new(),
disabled: AtomicBool::new(false),
detached: AtomicBool::new(false),
}
}
}
pub struct TaskNode {
pub name: &'static str,
pub mode: Mode,
pub deps: &'static [&'static TaskNode],
pub spawn: fn(Spawner) -> Result<(), SpawnError>,
handle: TaskHandle,
}
impl TaskNode {
pub const fn new(
name: &'static str,
mode: Mode,
deps: &'static [&'static TaskNode],
spawn: fn(Spawner) -> Result<(), SpawnError>,
) -> Self {
Self { name, mode, deps, spawn, handle: TaskHandle::new() }
}
pub fn shutdown_requested(&self) -> bool {
self.handle.shutdown.load(Ordering::Acquire)
}
pub async fn wait_shutdown(&self) {
if self.handle.shutdown.load(Ordering::Acquire) {
return;
}
self.handle.shutdown_wake.wait().await;
}
pub fn ack_dropped(&self) {
self.handle.dropped.store(true, Ordering::Release);
self.handle.dropped_wake.signal(());
}
pub async fn wait_resume(&self) {
self.handle.resume_wake.wait().await;
}
pub fn mark_busy(&self) {
if !self.handle.busy.swap(true, Ordering::Release) {
request_scale();
}
}
pub fn mark_idle(&self) {
if self.handle.busy.swap(false, Ordering::Release) {
request_scale();
}
}
pub fn is_busy(&self) -> bool {
self.handle.busy.load(Ordering::Acquire)
}
pub fn is_running(&self) -> bool {
self.handle.running.load(Ordering::Acquire)
}
pub fn is_disabled(&self) -> bool {
self.handle.disabled.load(Ordering::Acquire)
}
pub fn set_detached(&self, detached: bool) {
self.handle.detached.store(detached, Ordering::Release);
}
pub fn is_detached(&self) -> bool {
self.handle.detached.load(Ordering::Acquire)
}
pub(crate) fn signal_shutdown(&self) {
self.handle.shutdown.store(true, Ordering::Release);
self.handle.shutdown_wake.signal(());
}
pub(crate) fn signal_resume(&self) {
self.handle.resume_wake.signal(());
}
pub(crate) fn set_running(&self, running: bool) {
self.handle.running.store(running, Ordering::Release);
}
pub fn set_disabled(&self, disabled: bool) {
self.handle.disabled.store(disabled, Ordering::Release);
}
pub(crate) async fn wait_dropped(&self) {
if self.handle.dropped.load(Ordering::Acquire) {
return;
}
self.handle.dropped_wake.wait().await;
}
pub(crate) fn reset(&self) {
self.handle.shutdown.store(false, Ordering::Release);
self.handle.dropped.store(false, Ordering::Release);
self.handle.busy.store(false, Ordering::Release);
self.handle.shutdown_wake.reset();
self.handle.dropped_wake.reset();
}
}
#[macro_export]
macro_rules! task_graph {
(@munch units=[$($u:tt)*] nodes=[$($n:tt)*];) => {
pub const NODE_COUNT: usize = [$($u)*].len();
pub static ALL_NODES: [&$crate::TaskNode; NODE_COUNT] = [$($n)*];
};
(@munch units=[$($u:tt)*] nodes=[$($n:tt)*]; #[cfg($c:meta)] $node:expr, $($rest:tt)*) => {
$crate::task_graph!(@munch
units=[$($u)* #[cfg($c)] (),]
nodes=[$($n)* #[cfg($c)] $node,];
$($rest)*
);
};
(@munch units=[$($u:tt)*] nodes=[$($n:tt)*]; $node:expr, $($rest:tt)*) => {
$crate::task_graph!(@munch
units=[$($u)* (),]
nodes=[$($n)* $node,];
$($rest)*
);
};
(@munch units=[$($u:tt)*] nodes=[$($n:tt)*]; #[cfg($c:meta)] $node:expr) => {
$crate::task_graph!(@munch
units=[$($u)* #[cfg($c)] (),]
nodes=[$($n)* #[cfg($c)] $node,];
);
};
(@munch units=[$($u:tt)*] nodes=[$($n:tt)*]; $node:expr) => {
$crate::task_graph!(@munch units=[$($u)* (),] nodes=[$($n)* $node,];);
};
($($items:tt)*) => {
$crate::task_graph!(@munch units=[] nodes=[]; $($items)*);
};
}
#[derive(Debug)]
pub enum BuildError {
Cycle,
}
#[cfg(feature = "defmt")]
impl defmt::Format for BuildError {
fn format(&self, f: defmt::Formatter) {
match self {
BuildError::Cycle => defmt::write!(f, "Cycle"),
}
}
}
pub struct Supervisor<const N: usize> {
nodes: &'static [&'static TaskNode],
order: Vec<u8, N>,
#[cfg(feature = "pool")]
pools: &'static [&'static dyn Pool],
}
impl<const N: usize> Supervisor<N> {
pub fn new(nodes: &'static [&'static TaskNode; N]) -> Result<Self, BuildError> {
let order = topo_sort::<N>(nodes)?;
let nodes: &'static [&'static TaskNode] = nodes;
Ok(Self {
nodes,
order,
#[cfg(feature = "pool")]
pools: &[],
})
}
#[cfg(feature = "pool")]
pub fn with_pools(mut self, pools: &'static [&'static dyn Pool]) -> Self {
self.pools = pools;
self
}
pub fn start(&self, spawner: Spawner) -> Result<(), SpawnError> {
for i in self.order.iter() {
let node = self.nodes[*i as usize];
if matches!(node.mode, Mode::OnDemand) || node.is_disabled() {
continue;
}
info!("supervisor: spawning {} ({})", node.name, node.mode);
(node.spawn)(spawner)?;
node.set_running(true);
}
Ok(())
}
pub fn start_node(
&self,
node: &'static TaskNode,
spawner: Spawner,
) -> Result<(), SpawnError> {
node.reset();
(node.spawn)(spawner)?;
node.set_running(true);
info!("supervisor: started {}", node.name);
Ok(())
}
async fn shutdown_and_wait(&self, node: &'static TaskNode) {
node.signal_shutdown();
if let Either::Second(()) = select(
node.wait_dropped(),
Timer::after_millis(SHUTDOWN_ACK_TIMEOUT_MS),
)
.await
{
panic!(
"supervisor: task {} did not ack shutdown within {}ms",
node.name,
SHUTDOWN_ACK_TIMEOUT_MS,
);
}
node.set_running(false);
}
pub async fn stop_node(&self, node: &'static TaskNode) {
if !node.is_running() {
return;
}
self.shutdown_and_wait(node).await;
info!("supervisor: stopped {}", node.name);
}
pub async fn teardown(&self) {
for i in self.order.iter().rev() {
let node = self.nodes[*i as usize];
if !node.is_running() {
continue;
}
info!("supervisor: tearing down {}", node.name);
self.shutdown_and_wait(node).await;
}
}
pub fn resume_pausable(&self) {
for i in self.order.iter() {
let node = self.nodes[*i as usize];
if matches!(node.mode, Mode::Pause) && !node.is_disabled() {
node.reset();
info!("supervisor: resuming {}", node.name);
node.signal_resume();
node.set_running(true);
}
}
}
pub fn respawn_terminate(&self, spawner: Spawner) -> Result<(), SpawnError> {
for i in self.order.iter() {
let node = self.nodes[*i as usize];
if matches!(node.mode, Mode::Terminate) && !node.is_disabled() {
node.reset();
info!("supervisor: respawning {}", node.name);
(node.spawn)(spawner)?;
node.set_running(true);
}
}
Ok(())
}
}
#[cfg(feature = "control")]
impl<const N: usize> Supervisor<N> {
fn index_of(&self, node: &'static TaskNode) -> Option<usize> {
self.nodes.iter().position(|n| core::ptr::eq(*n, node))
}
fn seed(&self, target: &'static TaskNode, set: &mut [bool; N]) {
if let Some(i) = self.index_of(target) {
set[i] = true;
}
#[cfg(feature = "pool")]
for pool in self.pools {
let members = pool.members();
if members.iter().any(|m| core::ptr::eq(*m, target)) {
for m in members {
if let Some(i) = self.index_of(m) {
set[i] = true;
}
}
}
}
}
pub async fn apply_control(&self, cmd: ControlCommand, spawner: Spawner) {
match cmd.op {
ControlOp::Deactivate => self.deactivate(cmd.node).await,
ControlOp::Activate => self.activate(cmd.node, spawner).await,
}
}
async fn deactivate(&self, target: &'static TaskNode) {
let mut set = [false; N];
self.seed(target, &mut set);
for i in self.order.iter() {
let j = *i as usize;
if set[j] {
continue;
}
if self.nodes[j].is_detached() {
continue;
}
if self.nodes[j]
.deps
.iter()
.any(|d| self.index_of(d).is_some_and(|di| set[di]))
{
set[j] = true;
}
}
for i in self.order.iter().rev() {
let j = *i as usize;
if !set[j] {
continue;
}
let node = self.nodes[j];
node.set_disabled(true);
if node.is_running() {
info!("supervisor: control-stop {}", node.name);
self.shutdown_and_wait(node).await;
}
}
}
async fn activate(&self, target: &'static TaskNode, spawner: Spawner) {
let mut set = [false; N];
self.seed(target, &mut set);
for i in self.order.iter().rev() {
let j = *i as usize;
if set[j] {
for d in self.nodes[j].deps {
if let Some(di) = self.index_of(d) {
set[di] = true;
}
}
}
}
for i in self.order.iter() {
let j = *i as usize;
if !set[j] {
continue;
}
let node = self.nodes[j];
node.set_disabled(false);
if node.is_running() {
continue;
}
match node.mode {
Mode::Terminate => {
info!("supervisor: control-start {}", node.name);
let _ = self.start_node(node, spawner);
}
Mode::Pause => {
info!("supervisor: control-resume {}", node.name);
node.reset();
node.signal_resume();
node.set_running(true);
}
Mode::OnDemand => {}
}
}
}
}
fn topo_sort<const N: usize>(
nodes: &'static [&'static TaskNode],
) -> Result<Vec<u8, N>, BuildError> {
let n = nodes.len();
let mut in_degree: Vec<u8, N> = Vec::new();
for node in nodes {
let _ = in_degree.push(node.deps.len() as u8);
}
let mut queue: Vec<u8, N> = Vec::new();
for i in 0..n {
if in_degree[i] == 0 {
let _ = queue.push(i as u8);
}
}
let mut order: Vec<u8, N> = Vec::new();
let mut head = 0;
while head < queue.len() {
let i = queue[head] as usize;
head += 1;
let _ = order.push(i as u8);
for j in 0..n {
if in_degree[j] == 0 {
continue;
}
let depends_on_i = nodes[j]
.deps
.iter()
.any(|d| core::ptr::eq(*d, nodes[i]));
if depends_on_i {
in_degree[j] -= 1;
if in_degree[j] == 0 {
let _ = queue.push(j as u8);
}
}
}
}
if order.len() != n {
return Err(BuildError::Cycle);
}
Ok(order)
}
#[cfg(feature = "pool")]
mod pool;
#[cfg(feature = "pool")]
pub use pool::*;