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
29 changes: 18 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This is a procedural macro crate that provides the `#[controller]` attribute mac
* Signal mechanism for broadcasting events.
* Pub/sub system for state change notifications.

The macro works by processing both struct definitions (to add publishers) and impl blocks (to generate the controller's `run` method, client API, and signal infrastructure).
The macro is applied to a module containing both the controller struct definition and its impl block, allowing coordinated code generation of the controller infrastructure, client API, and communication channels.

## Build & Test Commands

Expand Down Expand Up @@ -41,18 +41,32 @@ cargo doc --locked
## Architecture

### Macro Entry Point (`src/lib.rs`)
The `controller` attribute macro dispatches to either `item_struct` or `item_impl` based on input type.
The `controller` attribute macro parses the input as an `ItemMod` (module) and calls `controller::expand_module()`.

### Module Processing (`src/controller/mod.rs`)
The `expand_module()` function:
* Validates the module has a body with exactly one struct and one impl block.
* Extracts the struct and impl items from the module.
* Validates that the impl block matches the struct name.
* Calls `item_struct::expand()` and `item_impl::expand()` to process each component.
* Combines the generated code back into the module structure along with any other items.

Channel capacities and subscriber limits are also defined here:
* `ALL_CHANNEL_CAPACITY`: 8
* `SIGNAL_CHANNEL_CAPACITY`: 8
* `BROADCAST_MAX_PUBLISHERS`: 1
* `BROADCAST_MAX_SUBSCRIBERS`: 16

### Struct Processing (`src/controller/item_struct.rs`)
Processes `#[controller]` on struct definitions. For fields marked with `#[controller(publish)]`:
Processes the controller struct definition. For fields marked with `#[controller(publish)]`:
* Adds publisher fields to the struct.
* Generates setters (`set_<field>`) that broadcast changes.
* Creates `<StructName><FieldName>` stream type and `<StructName><FieldName>Changed` event struct.

The generated `new()` method initializes both user fields and generated publisher fields.

### Impl Processing (`src/controller/item_impl.rs`)
Processes `#[controller]` on impl blocks. Distinguishes between:
Processes the controller impl block. Distinguishes between:

**Proxied methods** (normal methods):
* Creates request/response channels for each method.
Expand All @@ -67,13 +81,6 @@ Processes `#[controller]` on impl blocks. Distinguishes between:

The generated `run()` method contains a `select_biased!` loop that receives method calls from clients and dispatches them to the user's implementations.

### Constants (`src/controller/mod.rs`)
Channel capacities and subscriber limits are defined here:
* `ALL_CHANNEL_CAPACITY`: 8
* `SIGNAL_CHANNEL_CAPACITY`: 8
* `BROADCAST_MAX_PUBLISHERS`: 1
* `BROADCAST_MAX_SUBSCRIBERS`: 16

### Utilities (`src/util.rs`)
Case conversion functions (`pascal_to_snake_case`, `snake_to_pascal_case`) used for generating type and method names.

Expand Down
31 changes: 30 additions & 1 deletion Cargo.lock

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

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "firmware-controller"
description = "Controller to decouple interactions between components in a no_std environment."
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = [
"Zeeshan Ali Khan <zeenix@gmail.com>",
Expand All @@ -22,7 +22,10 @@ syn = { version = "2", features = ["extra-traits", "fold", "full"] }
heapless = { version = "0.7", default-features = false }
futures = { version = "0.3", default-features = false, features = [
"async-await",
"std",
"executor",
] }
critical-section = { version = "1.2", features = ["std"] }
embassy-sync = "0.7.2"
embassy-executor = { version = "0.9.1", features = [
"arch-std",
Expand Down
127 changes: 68 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,53 +39,58 @@ pub enum State {
Disabled,
}

// The controller struct. This is where you define the state of your firmware.
#[controller]
pub struct Controller {
#[controller(publish)]
state: State,
// Other fields. Note: No all of them need to be published.
}
mod controller {
use super::*;

// The controller struct. This is where you define the state of your firmware.
pub struct Controller {
#[controller(publish)]
state: State,
// Other fields. Note: Not all of them need to be published.
}

// The controller implementation. This is where you define the logic of your firmware.
#[controller]
impl Controller {
// The `signal` attribute marks this method signature (note: no implementation body) as a
// signal, that you can use to notify other parts of your code about specific events.
#[controller(signal)]
pub async fn power_error(&self, description: heapless::String<64>);

pub async fn enable_power(&mut self) -> Result<(), MyFirmwareError> {
if self.state != State::Disabled {
return Err(MyFirmwareError::InvalidState);
}
// The controller implementation. This is where you define the logic of your firmware.
impl Controller {
// The `signal` attribute marks this method signature (note: no implementation body) as a
// signal, that you can use to notify other parts of your code about specific events.
#[controller(signal)]
pub async fn power_error(&self, description: heapless::String<64>);

// Any other logic you want to run when enabling power.
pub async fn enable_power(&mut self) -> Result<(), MyFirmwareError> {
if self.state != State::Disabled {
return Err(MyFirmwareError::InvalidState);
}

self.set_state(State::Enabled).await;
self.power_error("Dummy error just for the showcase".try_into().unwrap())
.await;
// Any other logic you want to run when enabling power.

Ok(())
}
self.set_state(State::Enabled).await;
self.power_error("Dummy error just for the showcase".try_into().unwrap())
.await;

pub async fn disable_power(&mut self) -> Result<(), MyFirmwareError> {
if self.state != State::Enabled {
return Err(MyFirmwareError::InvalidState);
Ok(())
}

// Any other logic you want to run when enabling power.
pub async fn disable_power(&mut self) -> Result<(), MyFirmwareError> {
if self.state != State::Enabled {
return Err(MyFirmwareError::InvalidState);
}

// Any other logic you want to run when enabling power.

self.set_state(State::Disabled).await;
self.set_state(State::Disabled).await;

Ok(())
}
Ok(())
}

// Method that doesn't return anything.
pub async fn return_nothing(&self) {
// Method that doesn't return anything.
pub async fn return_nothing(&self) {
}
}
}

use controller::*;

#[embassy_executor::main]
async fn main(spawner: embassy_executor::Spawner) {
let mut controller = Controller::new(State::Disabled);
Expand All @@ -104,9 +109,8 @@ async fn client() {
use embassy_time::{Timer, Duration};

let mut client = ControllerClient::new();
// SAFETY: We don't create more than 16 instances so we won't panic.
let state_changed = ControllerState::new().unwrap().map(Either::Left);
let error_stream = ControllerPowerError::new().unwrap().map(Either::Right);
let state_changed = client.receive_state_changed().unwrap().map(Either::Left);
let error_stream = client.receive_power_error().unwrap().map(Either::Right);
let mut stream = select(state_changed, error_stream);

client.enable_power().await.unwrap();
Expand Down Expand Up @@ -141,34 +145,39 @@ async fn client() {

# Details

The `controller` macro will generated the following for you:
The `controller` macro will generate the following for you:

## Controller struct

* A `new` method that takes the fields of the struct as arguments and returns the struct.
* For each `published` field:
* Setter for this field, named `set_<field-name>` (so`set_state` here), which broadcasts any
* Setter for this field, named `set_<field-name>` (e.g., `set_state`), which broadcasts any
changes made to this field.
* Two client-side types:
* struct named `<struct-name><field-name-in-pascal-case>Changed` (so `ControllerStateChanged`
for `state` field), containing two public fields, named `previous` and `new` fields
representing the previous and new values of the field, respectively.
* Type named `<struct-name><field-name-in-pascal-case>` (so `ControllerState` for
`state` field), which implements `futures::Stream`, yielding each state change as the change
struct described above.
* `run` method with signature `pub async fn run(&mut self);` which runs the controller logic,
proxying calls from the client to the implementations here and their return value back to
the clients (internally via channels). Typically you'd call it at the end of your `main`
or run it as a task.
* Client-side API for this struct, named `<struct-name>Client` (`ControllerClient` here)
which provides exactly the same methods (except signal methods) defined in this implementation
that other parts of the code use to call these methods.
* A `run` method with signature `pub async fn run(mut self);` which runs the controller logic,
proxying calls from the client to the implementations and their return values back to the
clients (internally via channels). Typically you'd call it at the end of your `main` or run it
as a task.
* For each `signal` method:
* The method body, that broadcasts the signal to all clients that are listening to it.

## Client API

A client struct named `<struct-name>Client` (`ControllerClient` in the example) with the following
methods:

* All methods defined in the controller impl (except signal methods), which proxy calls to the
controller and return the results.
* For each `published` field:
* `receive_<field-name>_changed()` method (e.g., `receive_state_changed()`) that returns a
stream of state changes. The stream yields `<struct-name><field-name-in-pascal-case>Changed`
structs (e.g., `ControllerStateChanged`) containing `previous` and `new` fields.
* If the field is marked with `#[controller(publish(pub_setter))]`, a public
`set_<field-name>()` method (e.g., `set_state()`) is also generated on the client, allowing
external code to update the field value through the client API.
* For each `signal` method:
* The method body, that broadcasts the signal to all the clients that are listening to it.
* Two client-side types:
* struct, named `<struct-name><method-name-in-pascal-case>Args` (`ControllerPowerErrorArgs`
here), containing all the arguments of this method, as public fields.
* Type named `<struct-name><method-name-in-pascal-case>` (`ControllerPowerError` here) which
implements `futures::Stream`, yielding each signal broadcasted as the args struct described
above.
* `receive_<method-name>()` method (e.g., `receive_power_error()`) that returns a stream of
signal events. The stream yields `<struct-name><method-name-in-pascal-case>Args` structs
(e.g., `ControllerPowerErrorArgs`) containing all signal arguments as public fields.

## Dependencies assumed

Expand Down
Loading