Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions examples/embassy-mqtt-connector-demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ aimdb-serial-connector = { path = "../../aimdb-serial-connector", default-featur

# Shared demo logic. `serde` makes the shared types serde-codec'able in no_std so
# they can be exposed over the serial remote-access server.
mqtt-connector-demo-common = { path = "../mqtt-connector-demo-common", features = [
"alloc",
"derive",
"serde",
mqtt-connector-demo-common = { path = "../mqtt-connector-demo-common" }
# publish / subscribe; the common crate enables linkable-json for its contracts.
aimdb-data-contracts = { path = "../../aimdb-data-contracts", default-features = false, features = [
"linkable",
] }

# Embassy ecosystem - STM32H563ZI with Ethernet
Expand Down
121 changes: 28 additions & 93 deletions examples/embassy-mqtt-connector-demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
extern crate alloc;

use aimdb_core::remote::SecurityPolicy;
use aimdb_core::{AimDbBuilder, Producer, RecordKey, RuntimeContext};
use aimdb_core::{AimDbBuilder, RecordKey};
use aimdb_data_contracts::{publish, subscribe};
use aimdb_embassy_adapter::io::EmbassyUart;
use aimdb_embassy_adapter::{EmbassyAdapter, EmbassyBufferType, EmbassyRecordRegistrarExtCustom};
use aimdb_serial_connector::SerialServer;
Expand All @@ -91,9 +92,10 @@ use aimdb_mqtt_connector::MqttConnector;
#[cfg(feature = "tls")]
use aimdb_mqtt_connector::TlsOptions;

// Import shared types, monitors, and compile-time safe keys from the common crate
// Shared contracts, keys, thermometers and monitors from the common crate
use mqtt_connector_demo_common::{
CommandKey, SensorKey, Temperature, TemperatureCommand, command_consumer, temperature_logger,
CommandKey, SensorKey, SimThermometers, Temperature, TemperatureCommand, Thermometer,
command_consumer, temperature_logger,
};

// Simple embedded allocator (required by some dependencies)
Expand All @@ -116,71 +118,6 @@ async fn net_task(mut runner: embassy_net::Runner<'static, Device>) -> ! {
runner.run().await
}

// ============================================================================
// TEMPERATURE PRODUCERS (platform-specific due to embassy-time)
//
// Each cycles its readings endlessly rather than stopping after a fixed
// count: reconnect, re-subscribe and ping cadence only become observable
// while something is still publishing.
// ============================================================================

/// Indoor temperature sensor producer
async fn indoor_temp_producer(ctx: RuntimeContext, temperature: Producer<Temperature>) {
let log = ctx.log();
log.info("🏠 Starting INDOOR temperature producer...\n");

for i in (0..5).cycle() {
let temp = Temperature::new("indoor-001", 22.0 + (i as f32 * 0.5)); // Indoor temps: 22-24°C

log.info(&alloc::format!(
"🏠 Indoor sensor producing: {:.1}°C",
temp.celsius
));

temperature.produce(temp);

Timer::after(Duration::from_secs(2)).await;
}
}

/// Outdoor temperature sensor producer
async fn outdoor_temp_producer(ctx: RuntimeContext, temperature: Producer<Temperature>) {
let log = ctx.log();
log.info("🌳 Starting OUTDOOR temperature producer...\n");

for i in (0..5).cycle() {
let temp = Temperature::new("outdoor-001", 5.0 + (i as f32 * 1.0)); // Outdoor temps: 5-9°C (cold!)

log.info(&alloc::format!(
"🌳 Outdoor sensor producing: {:.1}°C",
temp.celsius
));

temperature.produce(temp);

Timer::after(Duration::from_secs(2)).await;
}
}

/// Server room temperature sensor producer
async fn server_room_temp_producer(ctx: RuntimeContext, temperature: Producer<Temperature>) {
let log = ctx.log();
log.info("🖥️ Starting SERVER ROOM temperature producer...\n");

for i in (0..5).cycle() {
let temp = Temperature::new("server-room-001", 18.0 + (i as f32 * 0.2)); // Server room: 18-19°C (cooled)

log.info(&alloc::format!(
"🖥️ Server room sensor producing: {:.1}°C",
temp.celsius
));

temperature.produce(temp);

Timer::after(Duration::from_secs(2)).await;
}
}

// ============================================================================
// MQTT CONFIGURATION
// ============================================================================
Expand Down Expand Up @@ -428,13 +365,21 @@ async fn main(spawner: Spawner) {
.with_keep_alive(MQTT_KEEP_ALIVE)
};

let mut builder = AimDbBuilder::new()
.runtime(runtime.clone())
.with_connector(mqtt)
.with_connector(
SerialServer::new(EmbassyUart::new(serial_rx, serial_tx))
.security_policy(SecurityPolicy::read_only()),
);
// The serial server takes no links, so it needs no handle.
let mut builder = AimDbBuilder::new().runtime(runtime.clone()).with_connector(
SerialServer::new(EmbassyUart::new(serial_rx, serial_tx))
.security_policy(SecurityPolicy::read_only()),
);
let mqtt = builder.connect(mqtt);

// The simulated thermometers cycle their readings endlessly: reconnect,
// re-subscribe and ping cadence only become observable while something
// is still publishing.
let SimThermometers {
indoor,
outdoor,
server_room,
} = SimThermometers::new();

// ========================================================================
// TEMPERATURE SENSORS (outbound: AimDB → MQTT)
Expand All @@ -447,31 +392,25 @@ async fn main(spawner: Spawner) {
builder.configure::<Temperature>(SensorKey::TempIndoor, |reg| {
reg.buffer_sized::<16, 3>(EmbassyBufferType::SpmcRing)
.with_remote_access()
.source(indoor_temp_producer)
.input(Thermometer::on(indoor))
.tap(temperature_logger)
.link_to(SensorKey::TempIndoor.link_address().unwrap())
.with_serializer(|_ctx, temp: &Temperature| Ok(temp.to_json_vec()))
.finish();
.output(publish(mqtt, SensorKey::TempIndoor.link_address().unwrap()));
});

builder.configure::<Temperature>(SensorKey::TempOutdoor, |reg| {
reg.buffer_sized::<16, 3>(EmbassyBufferType::SpmcRing)
.with_remote_access()
.source(outdoor_temp_producer)
.input(Thermometer::on(outdoor))
.tap(temperature_logger)
.link_to(SensorKey::TempOutdoor.link_address().unwrap())
.with_serializer(|_ctx, temp: &Temperature| Ok(temp.to_json_vec()))
.finish();
.output(publish(mqtt, SensorKey::TempOutdoor.link_address().unwrap()));
});

builder.configure::<Temperature>(SensorKey::TempServerRoom, |reg| {
reg.buffer_sized::<16, 3>(EmbassyBufferType::SpmcRing)
.with_remote_access()
.source(server_room_temp_producer)
.input(Thermometer::on(server_room))
.tap(temperature_logger)
.link_to(SensorKey::TempServerRoom.link_address().unwrap())
.with_serializer(|_ctx, temp: &Temperature| Ok(temp.to_json_vec()))
.finish();
.output(publish(mqtt, SensorKey::TempServerRoom.link_address().unwrap()));
});

// ========================================================================
Expand All @@ -483,18 +422,14 @@ async fn main(spawner: Spawner) {
reg.buffer_sized::<8, 2>(EmbassyBufferType::SpmcRing)
.with_remote_access()
.tap(command_consumer)
.link_from(CommandKey::TempIndoor.link_address().unwrap())
.with_deserializer(|_ctx, data: &[u8]| TemperatureCommand::from_json(data))
.finish();
.input(subscribe(mqtt, CommandKey::TempIndoor.link_address().unwrap()));
});

builder.configure::<TemperatureCommand>(CommandKey::TempOutdoor, |reg| {
reg.buffer_sized::<8, 2>(EmbassyBufferType::SpmcRing)
.with_remote_access()
.tap(command_consumer)
.link_from(CommandKey::TempOutdoor.link_address().unwrap())
.with_deserializer(|_ctx, data: &[u8]| TemperatureCommand::from_json(data))
.finish();
.input(subscribe(mqtt, CommandKey::TempOutdoor.link_address().unwrap()));
});

info!("✅ Database configured with multi-sensor MQTT:");
Expand Down
32 changes: 11 additions & 21 deletions examples/mqtt-connector-demo-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,23 @@ description = "Shared business logic for MQTT demos - runs on MCU, edge, and clo

[features]
default = []
std = ["alloc", "aimdb-core/std", "serde", "serde_json"]
alloc = ["aimdb-core/alloc"]
derive = ["aimdb-core/derive"]
# serde derive on the shared types — works in `no_std + alloc` too (no serde_json
# needed: the no_std JSON paths are hand-rolled), so an embassy demo can expose
# these records over a remote-access (serial) server.
serde = ["dep:serde"]
# `SchemaType` + JSON `#[derive(Linkable)]`. `no_std + alloc`
# compatible (`linkable-json` uses serde_json's alloc-only configuration).
# Not enabled by default: the
# embassy demo's hand-rolled no_std JSON path is an intentional illustration,
# left untouched; only the tokio demo opts into this feature.
data-contracts = ["alloc", "serde", "dep:aimdb-data-contracts"]

std = ["aimdb-core/std", "aimdb-data-contracts/std", "serde/std"]
# Embassy-specific features (no_std)
defmt = ["dep:defmt"]

[dependencies]
# Core AimDB - runtime agnostic
aimdb-core = { path = "../../aimdb-core", default-features = false }
# Serialization (std only)
serde = { workspace = true, features = ["derive"], optional = true }
serde_json = { workspace = true, optional = true }
# Optional: SchemaType + JSON #[derive(Linkable)] (no_std + alloc)
aimdb-data-contracts = { path = "../../aimdb-data-contracts", default-features = false, optional = true, features = [
# Core AimDB - runtime agnostic; `derive` for the `RecordKey` keys
aimdb-core = { path = "../../aimdb-core", default-features = false, features = [
"alloc",
"derive",
] }
# The contracts' wire format: JSON `#[derive(Linkable)]`, `no_std + alloc`
# compatible (serde_json's alloc-only configuration), for publish/subscribe
aimdb-data-contracts = { path = "../../aimdb-data-contracts", default-features = false, features = [
"linkable-json",
] }
# Serialize/Deserialize on the contracts: the wire format, and remote access
serde = { workspace = true }

# Optional defmt for embedded logging
defmt = { workspace = true, optional = true }
38 changes: 23 additions & 15 deletions examples/mqtt-connector-demo-common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The same monitor functions, data types, and business logic work across all platf
┌─────────────────────────────────────────────────────────────────┐
│ mqtt-connector-demo-common │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Data Types │ │ Monitor Functions│ │
│ │ Contracts │ │ Monitor Functions│ │
│ │ Temperature │ │ temperature_logger│ │
│ │ TemperatureCmd │ │ command_consumer │ │
│ └─────────────────┘ └──────────────────┘ │
Expand All @@ -31,39 +31,47 @@ The same monitor functions, data types, and business logic work across all platf
└─────────────┘ └─────────────┘ └─────────────┘
```

## Contents

- **Contracts** (`types`): `Temperature`, `TemperatureCommand`. Their wire format is JSON through `#[derive(Linkable)]` on every target, `no_std + alloc` included.
- **Keys** (`keys`): `SensorKey`, `CommandKey`, each with its MQTT topic in `#[link_address]`.
- **Simulated thermometers** (`sim`): `SimThermometers` hands out one owned channel per sensor; `Thermometer::on(channel)` is an input for every record type built `From<Reading>`.
- **Monitors** (`monitors`): `temperature_logger`, `command_consumer`.

## Features

- `std` - Enable std support (serde_json for serialization)
- `alloc` - Enable allocator support (required for most features)
- `std` - Enable std support
- `defmt` - Enable defmt integration for embedded logging

## Usage

The wiring is the same on both runtimes; only the buffer differs.

### Tokio (Edge/Cloud)

```rust
use mqtt_connector_demo_common::{Temperature, TemperatureCommand, temperature_logger, command_consumer};
use aimdb_data_contracts::publish;
use mqtt_connector_demo_common::{SensorKey, SimThermometers, Temperature, Thermometer, temperature_logger};

builder.configure::<Temperature>("sensor.temp.indoor", |reg| {
let mqtt = builder.connect(MqttConnector::new(broker_url));
let SimThermometers { indoor, .. } = SimThermometers::new();

builder.configure::<Temperature>(SensorKey::TempIndoor, |reg| {
reg.buffer(BufferCfg::SpmcRing { capacity: 10 })
.input(Thermometer::on(indoor))
.tap(temperature_logger) // Shared monitor
.link_to("mqtt://sensors/temp/indoor")
.with_serializer(|temp: &Temperature| Ok(temp.to_json_vec()))
.finish();
.output(publish(mqtt, SensorKey::TempIndoor.link_address().unwrap()));
});
```

### Embassy (MCU)

```rust
use mqtt_connector_demo_common::{Temperature, TemperatureCommand, temperature_logger, command_consumer};

builder.configure::<Temperature>("sensor.temp.indoor", |reg| {
builder.configure::<Temperature>(SensorKey::TempIndoor, |reg| {
reg.buffer_sized::<16, 2>(EmbassyBufferType::SpmcRing)
.tap(temperature_logger) // Same monitor works!
.link_to("mqtt://sensors/temp/indoor")
.with_serializer(|temp: &Temperature| Ok(temp.to_json_vec()))
.finish();
.input(Thermometer::on(indoor)) // Same input works!
.tap(temperature_logger) // Same monitor works!
.output(publish(mqtt, SensorKey::TempIndoor.link_address().unwrap()));
});
```

Expand Down
25 changes: 16 additions & 9 deletions examples/mqtt-connector-demo-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,34 @@
//!
//! This crate demonstrates AimDB's core strength: **write once, run anywhere**.
//!
//! The same monitor functions, data types, and business logic work across all
//! platforms by being generic over the `RuntimeAdapter` trait. Only the
//! platform-specific initialization (hardware setup, network stack) differs.
//! Everything here is runtime-agnostic: the contracts and their wire format
//! ([`types`]), the record keys and their MQTT topics ([`keys`]), the
//! simulated thermometers as an input ([`sim`]), and the monitors
//! ([`monitors`]). A demo adds only its platform setup and the record wiring:
//!
//! # Compile-Time Safe Keys
//! ```rust,ignore
//! let mqtt = builder.connect(MqttConnector::new(broker_url));
//! let SimThermometers { indoor, .. } = SimThermometers::new();
//!
//! This crate also demonstrates the `RecordKey` derive macro for type-safe
//! record keys. See the `keys` module for examples — unlinked, as it exists
//! only behind the `derive` feature.
//! builder.configure::<Temperature>(SensorKey::TempIndoor, |reg| {
//! reg.buffer(BufferCfg::SpmcRing { capacity: 10 })
//! .input(Thermometer::on(indoor))
//! .tap(temperature_logger)
//! .output(publish(mqtt, SensorKey::TempIndoor.link_address().unwrap()));
//! });
//! ```

#![cfg_attr(not(feature = "std"), no_std)]

extern crate alloc;

#[cfg(feature = "derive")]
pub mod keys;
pub mod monitors;
pub mod sim;
pub mod types;

// Re-export main types
#[cfg(feature = "derive")]
pub use keys::{CommandKey, SensorKey};
pub use monitors::{command_consumer, temperature_logger};
pub use sim::{Reading, SimChannel, SimThermometers, Thermometer};
pub use types::{Temperature, TemperatureCommand};
Loading
Loading