Skip to content

Commit b6f85ac

Browse files
committed
Test: embassy on host??
1 parent 71f6c66 commit b6f85ac

3 files changed

Lines changed: 164 additions & 8 deletions

File tree

crates/ziggurat-driver/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,20 @@ parking_lot = "0.12.4"
2121
rand = "0.10.1"
2222
thiserror = "2.0.12"
2323
tokio = { version = "1.43.0", features = ["rt", "macros", "time", "sync", "io-util"] }
24+
25+
# The embassy runtime adapter, host-runnable via arch-std so it can stand in for tokio.
26+
embassy-executor = { version = "0.7", features = [
27+
"arch-std",
28+
"executor-thread",
29+
], optional = true }
30+
embassy-time = { version = "0.4", features = ["std"], optional = true }
31+
embassy-sync = { version = "0.6", optional = true }
32+
spin = { version = "0.9", default-features = false, features = [
33+
"spin_mutex",
34+
], optional = true }
35+
36+
[features]
37+
default = []
38+
# Select the embassy runtime adapter (and its no_std-friendly sync primitives) instead of
39+
# the default tokio one. Mutually overrides tokio at the `sync`/`runtime` seam.
40+
embassy = ["dep:embassy-executor", "dep:embassy-time", "dep:embassy-sync", "dep:spin"]

crates/ziggurat-driver/src/runtime.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,92 @@ impl Spawn for TokioSpawner {
153153
while tasks.join_next().await.is_some() {}
154154
}
155155
}
156+
157+
/// The embassy runtime adapter. Host-runnable through `arch-std` so it can stand in for
158+
/// tokio, and the same impl drives the MCU once an esp PHY backs it.
159+
#[cfg(feature = "embassy")]
160+
pub use embassy_impl::{EmbassyRuntime, EmbassySpawner};
161+
162+
#[cfg(feature = "embassy")]
163+
mod embassy_impl {
164+
use super::{RtInstant, Runtime, Spawn, SpawnedTask};
165+
use core::future::Future;
166+
use core::ops::Add;
167+
use core::time::Duration;
168+
169+
const fn to_embassy(duration: Duration) -> embassy_time::Duration {
170+
embassy_time::Duration::from_micros(duration.as_micros() as u64)
171+
}
172+
173+
const fn from_embassy(duration: embassy_time::Duration) -> Duration {
174+
Duration::from_micros(duration.as_micros())
175+
}
176+
177+
/// Wraps `embassy_time::Instant` so the trait's `core::time::Duration` arithmetic
178+
/// works against embassy's own `Duration` type.
179+
#[derive(Copy, Clone)]
180+
pub struct EmbassyInstant(embassy_time::Instant);
181+
182+
impl Add<Duration> for EmbassyInstant {
183+
type Output = Self;
184+
185+
fn add(self, rhs: Duration) -> Self {
186+
Self(self.0 + to_embassy(rhs))
187+
}
188+
}
189+
190+
impl RtInstant for EmbassyInstant {
191+
fn saturating_duration_since(self, earlier: Self) -> Duration {
192+
from_embassy(self.0.saturating_duration_since(earlier.0))
193+
}
194+
}
195+
196+
pub struct EmbassyRuntime;
197+
198+
impl Runtime for EmbassyRuntime {
199+
type Instant = EmbassyInstant;
200+
type Spawner = EmbassySpawner;
201+
202+
fn now() -> Self::Instant {
203+
EmbassyInstant(embassy_time::Instant::now())
204+
}
205+
206+
fn sleep(duration: Duration) -> impl Future<Output = ()> + Send {
207+
embassy_time::Timer::after(to_embassy(duration))
208+
}
209+
210+
fn sleep_until(deadline: Self::Instant) -> impl Future<Output = ()> + Send {
211+
embassy_time::Timer::at(deadline.0)
212+
}
213+
}
214+
215+
/// Each detached task runs in one slot of this fixed pool — embassy has no dynamic
216+
/// spawn, so the size bounds the stack's concurrent background tasks (long-lived
217+
/// reactors plus the transient ZDP/indirect/route-request ones).
218+
#[embassy_executor::task(pool_size = 24)]
219+
async fn task_runner(task: SpawnedTask) {
220+
task.await;
221+
}
222+
223+
/// Spawns into the embassy executor. Holds a [`SendSpawner`](embassy_executor::SendSpawner)
224+
/// so it is `Send + Sync`; obtained from the executor at startup.
225+
pub struct EmbassySpawner(embassy_executor::SendSpawner);
226+
227+
impl EmbassySpawner {
228+
pub const fn new(spawner: embassy_executor::SendSpawner) -> Self {
229+
Self(spawner)
230+
}
231+
}
232+
233+
impl Spawn for EmbassySpawner {
234+
fn spawn(&self, task: SpawnedTask) {
235+
if self.0.spawn(task_runner(task)).is_err() {
236+
tracing::error!("embassy task pool exhausted; background task dropped");
237+
}
238+
}
239+
240+
// Embassy cannot cancel spawned tasks; the MCU stack is never replaced, so there
241+
// is nothing to stop.
242+
async fn shutdown(&self) {}
243+
}
244+
}

crates/ziggurat-driver/src/sync.rs

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,60 @@
1-
//! The synchronization primitives the stack rests on: a blocking [`Mutex`] and an async
2-
//! [`Notify`].
1+
//! The synchronization primitives the stack rests on: a blocking [`Mutex`], an async
2+
//! [`AsyncMutex`], and an [`Notify`].
3+
//!
4+
//! Everything in the driver imports these from here rather than naming `parking_lot`,
5+
//! `tokio`, `spin`, or `embassy-sync` directly, so this module is the single seam where
6+
//! the implementation is chosen by the `embassy` feature. The blocking [`Mutex`] must
7+
//! never be held across an `.await` (the protocol core's
8+
//! [`CoreGuard`](crate::zigbee_stack::CoreGuard) enforces this by being `!Send`); use
9+
//! [`AsyncMutex`] for the few guards that genuinely outlive an await point.
310
4-
pub use parking_lot::{Mutex, MutexGuard};
5-
pub use tokio::sync::Notify;
11+
#[cfg(not(feature = "embassy"))]
12+
mod imp {
13+
pub use parking_lot::{Mutex, MutexGuard};
14+
pub use tokio::sync::Mutex as AsyncMutex;
15+
pub use tokio::sync::Notify;
16+
}
617

7-
/// An async mutex, for the few places a guard is held across an `.await` (the radio
8-
/// stream and reset receivers). Distinct from the blocking [`Mutex`], which must never
9-
/// span a yield; reach for this only when the guard genuinely outlives an await point.
10-
pub use tokio::sync::Mutex as AsyncMutex;
18+
#[cfg(feature = "embassy")]
19+
mod imp {
20+
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
21+
22+
// A spinlock is a guard-returning, `Sync`, no_std mutex; on the cooperative
23+
// single-core MCU executor it never actually spins, and the stack never holds a guard
24+
// across an `.await`, so the lock window is always brief.
25+
pub use spin::{Mutex, MutexGuard};
26+
27+
/// The async mutex, for guards held across an `.await` (the radio stream + reset
28+
/// receivers). Pinned to the critical-section raw mutex.
29+
pub type AsyncMutex<T> = embassy_sync::mutex::Mutex<CriticalSectionRawMutex, T>;
30+
31+
/// A parameterless wake matching `tokio::sync::Notify`'s surface.
32+
///
33+
/// Built over embassy's single-slot [`Signal`](embassy_sync::signal::Signal):
34+
/// `notify_one` stores one permit and coalesces repeats; `notified` consumes it — the
35+
/// same single-waiter contract every wake in the stack relies on.
36+
#[derive(Default)]
37+
pub struct Notify(embassy_sync::signal::Signal<CriticalSectionRawMutex, ()>);
38+
39+
impl Notify {
40+
pub const fn new() -> Self {
41+
Self(embassy_sync::signal::Signal::new())
42+
}
43+
44+
pub fn notify_one(&self) {
45+
self.0.signal(());
46+
}
47+
48+
pub async fn notified(&self) {
49+
self.0.wait().await;
50+
}
51+
}
52+
53+
impl core::fmt::Debug for Notify {
54+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55+
f.write_str("Notify")
56+
}
57+
}
58+
}
59+
60+
pub use imp::*;

0 commit comments

Comments
 (0)