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
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ Processes the controller struct definition. Supports three field attributes:
* Default name is `set_<field>`; custom name can be specified.
* Can be combined with `publish` to also broadcast changes.

The generated `new()` method initializes both user fields and generated sender fields, and sends
initial values to Watch channels so subscribers get them immediately.
The generated `new()` method returns `Option<Self>`, enforcing singleton semantics via a static
`AtomicBool`. It returns `Some` on the first call and `None` on subsequent calls. It initializes
both user fields and generated sender fields, and sends initial values to Watch channels so
subscribers get them immediately.

### Impl Processing (`src/controller/item_impl.rs`)
Processes the controller impl block. Distinguishes between:
Expand Down Expand Up @@ -161,7 +163,6 @@ testing.

## Key Limitations

* Singleton operation: multiple controller instances interfere with each other.
* Methods must be async and cannot use reference parameters/return types.
* Maximum 16 subscribers per state/signal stream.
* Published fields must implement `Clone`. With `tokio`, they must also implement `Send + Sync`.
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ use controller::*;

#[embassy_executor::main]
async fn main(spawner: embassy_executor::Spawner) {
let mut controller = Controller::new(State::Disabled);
let controller = Controller::new(State::Disabled).unwrap();

// Spawn the client task.
spawner.spawn(client());
Expand Down Expand Up @@ -151,7 +151,9 @@ 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.
* A `new` method that takes the fields of the struct as arguments and returns `Option<Self>`.
Returns `Some` on the first call and `None` on subsequent calls, since only one instance of a
controller can exist at a time.
* For each `published` field:
* Setter for this field, named `set_<field-name>` (e.g., `set_state`), which broadcasts any
changes made to this field.
Expand Down Expand Up @@ -287,10 +289,6 @@ The `controller` macro assumes that you have the following dependencies in your

## Known limitations & Caveats

* Currently only works as a singleton: you can create multiple instances of the controller but
if you run them simultaneously, they'll interfere with each others' operation. We hope to remove
this limitation in the future. Having said that, most firmware applications will only need a
single controller instance.
* Method args/return type can't be reference types.
* Methods must be async.
* The maximum number of subscribers state change and signal streams is 16. We plan to provide an
Expand Down
22 changes: 20 additions & 2 deletions src/controller/item_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,25 +148,43 @@ pub(crate) fn expand(mut input: ItemStruct) -> Result<ExpandedStruct> {
});
let vis = &input.vis;

// Generate a static AtomicBool to enforce singleton construction.
let struct_name_caps = pascal_to_snake_case(&struct_name.to_string()).to_ascii_uppercase();
let created_flag_name = Ident::new(&format!("{struct_name_caps}_CREATED"), struct_name.span());

// Initial value sends are already collected from PublishedFieldCode.

Ok(ExpandedStruct {
tokens: quote! {
static #created_flag_name: core::sync::atomic::AtomicBool =
core::sync::atomic::AtomicBool::new(false);

#vis struct #struct_name {
#(#fields),*,
#sender_fields_declarations
}

impl #struct_name {
/// Creates a new controller instance.
///
/// Returns `None` if an instance has already been created.
/// Only one instance of a controller can exist at a time.
#[allow(clippy::too_many_arguments)]
pub fn new(#(#new_fn_params),*) -> Self {
pub fn new(#(#new_fn_params),*) -> Option<Self> {
if #created_flag_name.swap(
true,
core::sync::atomic::Ordering::SeqCst,
) {
return None;
}

let __self = Self {
#(#field_names),*,
#sender_fields_initializations
};
// Send initial values so subscribers can get them immediately.
#(#initial_value_sends)*
__self
Some(__self)
}

#setters
Expand Down
12 changes: 6 additions & 6 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ use test_controller::*;
#[cfg(feature = "embassy")]
#[test]
fn test_controller_basic_functionality() {
let controller = Controller::new(State::Idle, Mode::Normal, 0);
let controller = Controller::new(State::Idle, Mode::Normal, 0).unwrap();

std::thread::spawn(move || {
let executor = Box::leak(Box::new(embassy_executor::Executor::new()));
Expand All @@ -91,7 +91,7 @@ fn test_controller_basic_functionality() {
#[cfg(feature = "tokio")]
#[tokio::test]
async fn test_controller_basic_functionality() {
let controller = Controller::new(State::Idle, Mode::Normal, 0);
let controller = Controller::new(State::Idle, Mode::Normal, 0).unwrap();
tokio::spawn(controller_task(controller));
tokio::task::yield_now().await;

Expand Down Expand Up @@ -251,7 +251,7 @@ mod visibility_test_controller {
#[cfg(feature = "embassy")]
#[test]
fn test_visibility_on_fields() {
let controller = visibility_test_controller::Controller::new(42, -1, true);
let controller = visibility_test_controller::Controller::new(42, -1, true).unwrap();
assert_eq!(controller.public_field, 42);
assert_eq!(controller.crate_field, -1);

Expand All @@ -272,7 +272,7 @@ fn test_visibility_on_fields() {
#[cfg(feature = "tokio")]
#[tokio::test]
async fn test_visibility_on_fields() {
let controller = visibility_test_controller::Controller::new(42, -1, true);
let controller = visibility_test_controller::Controller::new(42, -1, true).unwrap();
assert_eq!(controller.public_field, 42);
assert_eq!(controller.crate_field, -1);

Expand Down Expand Up @@ -343,7 +343,7 @@ fn poll_methods() {
POLL_B_COUNT.store(0, Ordering::SeqCst);
POLL_C_COUNT.store(0, Ordering::SeqCst);

let controller = poll_test_controller::Controller::new(42);
let controller = poll_test_controller::Controller::new(42).unwrap();
assert_eq!(controller.value, 42);

std::thread::spawn(move || {
Expand All @@ -369,7 +369,7 @@ async fn poll_methods() {
POLL_B_COUNT.store(0, Ordering::SeqCst);
POLL_C_COUNT.store(0, Ordering::SeqCst);

let controller = poll_test_controller::Controller::new(42);
let controller = poll_test_controller::Controller::new(42).unwrap();
assert_eq!(controller.value, 42);

tokio::spawn(async move { controller.run().await });
Expand Down
Loading