diff --git a/CLAUDE.md b/CLAUDE.md index 73608c6..bd52fa0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,8 +100,10 @@ Processes the controller struct definition. Supports three field attributes: * Default name is `set_`; 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`, 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: @@ -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`. diff --git a/README.md b/README.md index e8abec5..7507a2f 100644 --- a/README.md +++ b/README.md @@ -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()); @@ -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`. + 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_` (e.g., `set_state`), which broadcasts any changes made to this field. @@ -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 diff --git a/src/controller/item_struct.rs b/src/controller/item_struct.rs index fd0ce57..9d051fd 100644 --- a/src/controller/item_struct.rs +++ b/src/controller/item_struct.rs @@ -148,25 +148,43 @@ pub(crate) fn expand(mut input: ItemStruct) -> Result { }); 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 { + 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 diff --git a/tests/integration.rs b/tests/integration.rs index 1ced2b6..55e03b4 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -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())); @@ -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; @@ -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); @@ -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); @@ -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 || { @@ -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 });