diff --git a/Cargo.lock b/Cargo.lock index 7efed8bb..3b4b5420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1815,6 +1815,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "hello-mailbox-async" +version = "1.1.0" +dependencies = [ + "aimdb-core", + "aimdb-tokio-adapter", + "tokio", +] + [[package]] name = "hello-single-latest-async" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2b7dbb45..83289636 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "examples/weather-mesh-demo/weather-station-beta", "examples/weather-mesh-demo/weather-station-gamma", "examples/hello-mailbox", + "examples/hello-mailbox-async", "examples/hello-single-latest-async", ] exclude = ["_external"] @@ -55,16 +56,10 @@ categories = ["database-implementations", "embedded", "asynchronous"] [workspace.dependencies] # Core async runtime -tokio = { version = "1.47.1", default-features = false, features = [ - "macros", - "rt-multi-thread", -] } +tokio = { version = "1.47.1", default-features = false, features = ["macros", "rt-multi-thread"] } # Serialization (no_std compatible by default) -serde = { version = "1.0", default-features = false, features = [ - "derive", - "alloc", -] } +serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } # Error handling thiserror = "2.0.16" @@ -85,9 +80,7 @@ tracing = { version = "0.1", default-features = false } # Async utilities futures = "0.3" -futures-util = { version = "0.3", default-features = false, features = [ - "alloc", -] } +futures-util = { version = "0.3", default-features = false, features = ["alloc"] } # CLI (for aimdb-cli) clap = { version = "4.0", features = ["derive"] } @@ -126,9 +119,7 @@ embassy-net = { version = "0.9.0", path = "./_external/embassy/embassy-net", fea "medium-ethernet", "proto-ipv6", ] } -embassy-usb = { version = "0.6.0", path = "./_external/embassy/embassy-usb", features = [ - "defmt", -] } +embassy-usb = { version = "0.6.0", path = "./_external/embassy/embassy-usb", features = ["defmt"] } embassy-futures = { version = "0.1.2", path = "./_external/embassy/embassy-futures" } # Embedded HAL for peripheral abstractions @@ -143,10 +134,7 @@ embedded-nal-async = "0.8.0" embedded-storage = "0.3.1" # Embedded runtime and utilities -cortex-m = { version = "0.7.6", features = [ - "inline-asm", - "critical-section-single-core", -] } +cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] } cortex-m-rt = "0.7.0" critical-section = "1.1" static_cell = "2" diff --git a/Makefile b/Makefile index 3ea61cef..ea3b66ac 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,7 @@ test: fmt: @printf "$(GREEN)Formatting code (workspace members only)...$(NC)\n" - @for pkg in aimdb-executor aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-uds-connector aimdb-serial-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo embassy-serial-connector-demo weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox hello-single-latest-async; do \ + @for pkg in aimdb-executor aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-uds-connector aimdb-serial-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo embassy-serial-connector-demo weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox hello-mailbox-async hello-single-latest-async; do \ printf "$(YELLOW) → Formatting $$pkg$(NC)\n"; \ cargo fmt -p $$pkg 2>/dev/null || true; \ done @@ -185,7 +185,7 @@ fmt: fmt-check: @printf "$(GREEN)Checking code formatting (workspace members only)...$(NC)\n" @FAILED=0; \ - for pkg in aimdb-executor aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-uds-connector aimdb-serial-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo embassy-serial-connector-demo weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox hello-single-latest-async; do \ + for pkg in aimdb-executor aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-uds-connector aimdb-serial-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo embassy-serial-connector-demo weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox hello-mailbox-async hello-single-latest-async; do \ printf "$(YELLOW) → Checking $$pkg$(NC)\n"; \ if ! cargo fmt -p $$pkg -- --check 2>&1; then \ printf "$(RED)❌ Formatting check failed for $$pkg$(NC)\n"; \ @@ -386,6 +386,8 @@ examples: cargo build --package remote-access-demo @printf "$(YELLOW) → Building hello-mailbox (sync)$(NC)\n" cargo build --package hello-mailbox + @printf "$(YELLOW) → Building hello-mailbox-async $(NC)\n" + cargo build --package hello-mailbox-async @printf "$(YELLOW) → Building hello-single-latest-async$(NC)\n" cargo build --package hello-single-latest-async @printf "$(GREEN)All examples built successfully!$(NC)\n" diff --git a/README.md b/README.md index d7f791e4..e4c9c192 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ docker compose up | --- | --- | --- | | **SPMC Ring** | Bounded stream with independent consumers | Sensor telemetry, event logs | | [**SingleLatest**](examples/hello-single-latest-async) | Only the current value matters | Feature flags, config, UI state | -| [**Mailbox**](examples/hello-mailbox) | Latest instruction wins | Device commands, actuation, RPC | +| [**Mailbox**](examples/hello-mailbox) / [**async Mailbox**](examples/hello-mailbox-async)| Latest instruction wins | Device commands, actuation, RPC | **Four capability traits** — opt-in, type-checked: diff --git a/aimdb-core/Cargo.toml b/aimdb-core/Cargo.toml index 3c7b3a89..fa6cbe2c 100644 --- a/aimdb-core/Cargo.toml +++ b/aimdb-core/Cargo.toml @@ -64,22 +64,14 @@ defmt = ["dep:defmt"] # Embedded logging via probe (no_std) # Independent of `profiling`; works in no_std (only needs heap + atomics). # `portable-atomic/critical-section` provides the 64-bit-atomic fallback on # targets without native 64-bit atomics (e.g. thumbv7em); no-op elsewhere. -metrics = [ - "alloc", - "portable-atomic/fallback", - "portable-atomic/critical-section", -] +metrics = ["alloc", "portable-atomic/fallback", "portable-atomic/critical-section"] # Automatic stage profiling (.source()/.tap()/.link() timing). # Independent of `metrics`; works in no_std (only needs heap + a runtime clock). # `portable-atomic/critical-section` provides the 64-bit-atomic fallback on targets # without native 64-bit atomics (e.g. thumbv7em); it's a no-op where native atomics # exist, and embedded binaries already supply a `critical-section` impl. -profiling = [ - "alloc", - "portable-atomic/fallback", - "portable-atomic/critical-section", -] +profiling = ["alloc", "portable-atomic/fallback", "portable-atomic/critical-section"] # Testing features test-utils = ["std"] @@ -104,9 +96,7 @@ futures-util = { version = "0.3", default-features = false, features = [ # Runtime-neutral `oneshot` for the session engines (alloc-backed, no_std-ready). # `futures-channel`'s `mpsc` is std-only, so the engines use `async-channel` for # that (below); only its `oneshot` is used here. -futures-channel = { version = "0.3", default-features = false, features = [ - "alloc", -] } +futures-channel = { version = "0.3", default-features = false, features = ["alloc"] } # Runtime-neutral mpsc (bounded + unbounded) for the session engines — one # alloc-backed implementation for every runtime (tokio + Embassy). Owned, # cloneable `Arc`-based senders (so the `'static` per-connection/-subscription @@ -133,15 +123,10 @@ tracing = { workspace = true, optional = true } defmt = { workspace = true, optional = true } # Synchronization primitives for no_std -spin = { version = "0.9", default-features = false, features = [ - "mutex", - "spin_mutex", -] } +spin = { version = "0.9", default-features = false, features = ["mutex", "spin_mutex"] } # Hash map for no_std (same implementation as std::collections::HashMap) -hashbrown = { version = "0.15", default-features = false, features = [ - "default-hasher", -] } +hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } [dev-dependencies] # For no_std testing diff --git a/examples/hello-mailbox-async/Cargo.toml b/examples/hello-mailbox-async/Cargo.toml new file mode 100644 index 00000000..6ee24807 --- /dev/null +++ b/examples/hello-mailbox-async/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "hello-mailbox-async" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "AimDB minimal async example demonstrating Mailbox buffer (latest-wins) semantics" +publish = false + +[dependencies] +aimdb-core = { path = "../../aimdb-core", features = ["std"] } +aimdb-tokio-adapter = { path = "../../aimdb-tokio-adapter", features = ["tokio-runtime"] } +tokio = { workspace = true, features = ["time"] } diff --git a/examples/hello-mailbox-async/README.md b/examples/hello-mailbox-async/README.md new file mode 100644 index 00000000..6e7336d1 --- /dev/null +++ b/examples/hello-mailbox-async/README.md @@ -0,0 +1,25 @@ +# hello-mailbox-async: async Mailbox buffer demo +The Mailbox buffer keeps only the last message written. It's like a real mailbox where only the most recent letter is visible. + +## When to use +This Mailbox buffer demo is useful in a variety of scenarios where you want to retain only the last message, for example: +- When you have a robotic arm that needs to execute a sequence of commands, and you only want to execute the last command, or even if the robot gets too much commands in a short period of time. + +## How it works +The Mailbox buffer demo works by simulating a mailbox buffer that retains only the last message using async API. +It sends 3 Colors and the MailBox only gets the last message. + +## How to run +From the workspace root, run: +``` +cargo run -p hello-mailbox-async +``` +**Expected output** +``` +=== hello-mailbox-async: Mailbox buffer demo === + +Firing three rapid commands BEFORE consumer exists: Red → Green → Blue + ✓ Got: Blue ← only the latest survived +Shutting down... + ✓ Done. +``` \ No newline at end of file diff --git a/examples/hello-mailbox-async/src/main.rs b/examples/hello-mailbox-async/src/main.rs new file mode 100644 index 00000000..370f47c0 --- /dev/null +++ b/examples/hello-mailbox-async/src/main.rs @@ -0,0 +1,55 @@ +use aimdb_core::{buffer::BufferCfg, AimDbBuilder}; +use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt}; +use std::sync::Arc; + +// Enum of colors for the LED +#[derive(Debug, Clone, PartialEq, Eq)] +enum Color { + Red, + Green, + Blue, +} + +// Struct representing the LED state +#[derive(Debug, Clone, PartialEq, Eq)] +struct Led { + color: Color, +} + +// Main function +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== hello-mailbox-async: Mailbox buffer demo ===\n"); + + // configuration + let adapter = Arc::new(TokioAdapter); + let mut builder = AimDbBuilder::new().runtime(adapter); + + builder.configure::("actuator.led", |reg| { + reg.buffer(BufferCfg::Mailbox) + .source(|ctx, producer| async move { + // Produce quickly BEFORE creating the consumer + println!("Firing three rapid commands BEFORE consumer exists: Red → Green → Blue"); + producer.produce(Led { color: Color::Red }); + producer.produce(Led { + color: Color::Green, + }); + producer.produce(Led { color: Color::Blue }); + ctx.time().sleep_millis(100).await; + }) + .tap(|ctx, consumer| async move { + // Now we create the consumer — it will only see the last value in the Mailbox + let mut reader = consumer.subscribe(); + ctx.time().sleep_millis(100).await; + + match reader.recv().await { + Ok(msg) => println!(" ✓ Got: {:?} ← only the latest survived", msg.color), + Err(_) => println!(" (mailbox was already empty)"), + } + }); + }); + builder.run().await?; + println!("Shutting down..."); + println!(" ✓ Done."); + Ok(()) +} diff --git a/examples/hello-mailbox/README.md b/examples/hello-mailbox/README.md index 12f1f32b..e77f147e 100644 --- a/examples/hello-mailbox/README.md +++ b/examples/hello-mailbox/README.md @@ -32,4 +32,5 @@ cargo run -p hello-mailbox ✓ Got: Green ← only the latest survived (Green) 3. Shutting down... ✓ Done. + ```