Skip to content

Commit 79521b2

Browse files
authored
Merge pull request #26 from zeenix/rel-0.4
Release 0.4
2 parents f2fdd0f + 7dea622 commit 79521b2

File tree

9 files changed

+610
-294
lines changed

9 files changed

+610
-294
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
# 0.4.0 (Nov 26th, 2025)
2+
3+
## Breaking Changes
4+
5+
* Published field streams now yield the current value on first poll, then subsequent changes.
6+
* Published field streams' item type is now the raw field type (e.g., `State`) instead of
7+
`*Changed` struct with `previous` and `new` fields.
8+
* The `pub_setter` sub-attribute on `publish` has been removed. Use the new independent `setter`
9+
attribute instead (e.g., `#[controller(publish, setter)]`).
10+
11+
## New Features
12+
13+
* New `getter` attribute for fields: generates a client-side getter method. Supports custom naming
14+
via `#[controller(getter = "custom_name")]`.
15+
* New `setter` attribute for fields: generates a client-side setter method independent of `publish`.
16+
Supports custom naming via `#[controller(setter = "custom_name")]`. Can be combined with `publish`
17+
to also broadcast changes.
18+
119
# 0.3.0 (Nov 25th, 2025)
220

321
* Macro now operates on a module. This allows the macro to have a visibility on both the struct and

CLAUDE.md

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ This is a procedural macro crate that provides the `#[controller]` attribute mac
88

99
* A controller struct that manages peripheral state.
1010
* Client API for sending commands to the controller.
11-
* Signal mechanism for broadcasting events.
12-
* Pub/sub system for state change notifications.
11+
* Signal mechanism for broadcasting events (PubSubChannel).
12+
* Watch-based subscriptions for state change notifications (yields current value first).
1313

1414
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.
1515

@@ -52,18 +52,31 @@ The `expand_module()` function:
5252
* Combines the generated code back into the module structure along with any other items.
5353

5454
Channel capacities and subscriber limits are also defined here:
55-
* `ALL_CHANNEL_CAPACITY`: 8
56-
* `SIGNAL_CHANNEL_CAPACITY`: 8
57-
* `BROADCAST_MAX_PUBLISHERS`: 1
58-
* `BROADCAST_MAX_SUBSCRIBERS`: 16
55+
* `ALL_CHANNEL_CAPACITY`: 8 (method/getter/setter request channels)
56+
* `SIGNAL_CHANNEL_CAPACITY`: 8 (signal PubSubChannel queue size)
57+
* `BROADCAST_MAX_PUBLISHERS`: 1 (signals only)
58+
* `BROADCAST_MAX_SUBSCRIBERS`: 16 (Watch for published fields, PubSubChannel for signals)
5959

6060
### Struct Processing (`src/controller/item_struct.rs`)
61-
Processes the controller struct definition. For fields marked with `#[controller(publish)]`:
62-
* Adds publisher fields to the struct.
63-
* Generates setters (`set_<field>`) that broadcast changes.
64-
* Creates `<StructName><FieldName>` stream type and `<StructName><FieldName>Changed` event struct.
61+
Processes the controller struct definition. Supports three field attributes:
6562

66-
The generated `new()` method initializes both user fields and generated publisher fields.
63+
**`#[controller(publish)]`** - Enables state change subscriptions:
64+
* Uses `embassy_sync::watch::Watch` channel (stores latest value).
65+
* Generates internal setter (`set_<field>`) that broadcasts changes.
66+
* Creates `<StructName><FieldName>` subscriber stream type.
67+
* Stream yields current value on first poll, then subsequent changes.
68+
69+
**`#[controller(getter)]` or `#[controller(getter = "name")]`**:
70+
* Generates a client-side getter method to read the field value.
71+
* Default name is the field name; custom name can be specified.
72+
73+
**`#[controller(setter)]` or `#[controller(setter = "name")]`**:
74+
* Generates a client-side setter method to update the field value.
75+
* Default name is `set_<field>`; custom name can be specified.
76+
* Can be combined with `publish` to also broadcast changes.
77+
78+
The generated `new()` method initializes both user fields and generated sender fields, and sends
79+
initial values to Watch channels so subscribers get them immediately.
6780

6881
### Impl Processing (`src/controller/item_impl.rs`)
6982
Processes the controller impl block. Distinguishes between:
@@ -75,11 +88,18 @@ Processes the controller impl block. Distinguishes between:
7588

7689
**Signal methods** (marked with `#[controller(signal)]`):
7790
* Methods have no body in the user's impl block.
91+
* Uses `embassy_sync::pubsub::PubSubChannel` for broadcast.
7892
* Generates method implementation that broadcasts to subscribers.
7993
* Creates `<StructName><MethodName>` stream type and `<StructName><MethodName>Args` struct.
8094
* Signal methods are NOT exposed in the client API (controller emits them directly).
8195

82-
The generated `run()` method contains a `select_biased!` loop that receives method calls from clients and dispatches them to the user's implementations.
96+
**Getter/setter methods** (from struct field attributes):
97+
* Receives getter/setter field info from struct processing.
98+
* Generates client-side getter methods that request current field value.
99+
* Generates client-side setter methods that update field value (and broadcast if published).
100+
101+
The generated `run()` method contains a `select_biased!` loop that receives method calls from
102+
clients and dispatches them to the user's implementations.
83103

84104
### Utilities (`src/util.rs`)
85105
Case conversion functions (`pascal_to_snake_case`, `snake_to_pascal_case`) used for generating type and method names.
@@ -97,5 +117,7 @@ Dev dependencies include `embassy-executor` and `embassy-time` for testing.
97117
* Singleton operation: multiple controller instances interfere with each other.
98118
* Methods must be async and cannot use reference parameters/return types.
99119
* Maximum 16 subscribers per state/signal stream.
100-
* Published fields must implement `Clone` and `Debug`.
101-
* Streams must be continuously polled or notifications are missed.
120+
* Published fields must implement `Clone`.
121+
* Published field streams yield current value on first poll; intermediate values may be missed if
122+
not polled between changes.
123+
* Signal streams must be continuously polled or notifications are missed.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "firmware-controller"
33
description = "Controller to decouple interactions between components in a no_std environment."
4-
version = "0.3.0"
4+
version = "0.4.0"
55
edition = "2021"
66
authors = [
77
"Zeeshan Ali Khan <[email protected]>",

README.md

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -109,28 +109,29 @@ async fn client() {
109109
use embassy_time::{Timer, Duration};
110110
111111
let mut client = ControllerClient::new();
112-
let state_changed = client.receive_state_changed().unwrap().map(Either::Left);
112+
let mut state_stream = client.receive_state_changed().unwrap();
113113
let error_stream = client.receive_power_error().unwrap().map(Either::Right);
114+
115+
// First poll returns the current (initial) state.
116+
let initial_state = state_stream.next().await.unwrap();
117+
assert_eq!(initial_state, State::Disabled);
118+
119+
// Now combine streams for event handling.
120+
let state_changed = state_stream.map(Either::Left);
114121
let mut stream = select(state_changed, error_stream);
115122
116123
client.enable_power().await.unwrap();
117124
while let Some(event) = stream.next().await {
118125
match event {
119-
Either::Left(ControllerStateChanged {
120-
new: State::Enabled,
121-
..
122-
}) => {
126+
Either::Left(State::Enabled) => {
123127
// This is fine in this very simple example where we've only one client in a single
124128
// task. In a real-world application, you should ensure that the stream is polled
125129
// continuously. Otherwise, you might miss notifications.
126130
Timer::after(Duration::from_secs(1)).await;
127131
128132
client.disable_power().await.unwrap();
129133
}
130-
Either::Left(ControllerStateChanged {
131-
new: State::Disabled,
132-
..
133-
}) => {
134+
Either::Left(State::Disabled) => {
134135
Timer::after(Duration::from_secs(1)).await;
135136
136137
client.enable_power().await.unwrap();
@@ -169,11 +170,16 @@ methods:
169170
controller and return the results.
170171
* For each `published` field:
171172
* `receive_<field-name>_changed()` method (e.g., `receive_state_changed()`) that returns a
172-
stream of state changes. The stream yields `<struct-name><field-name-in-pascal-case>Changed`
173-
structs (e.g., `ControllerStateChanged`) containing `previous` and `new` fields.
174-
* If the field is marked with `#[controller(publish(pub_setter))]`, a public
175-
`set_<field-name>()` method (e.g., `set_state()`) is also generated on the client, allowing
176-
external code to update the field value through the client API.
173+
stream of state values. The first value yielded is the current state at subscription time,
174+
and subsequent values are emitted when the field changes. The stream yields values of the
175+
field type directly (e.g., `State`).
176+
* For each field with a `getter` attribute (e.g., `#[controller(getter)]` or
177+
`#[controller(getter = "custom_name")]`), a getter method is generated on the client. The default
178+
name is the field name; a custom name can be specified.
179+
* For each field with a `setter` attribute (e.g., `#[controller(setter)]` or
180+
`#[controller(setter = "custom_name")]`), a public setter method is generated on the client,
181+
allowing external code to update the field value through the client API. The default setter
182+
name is `set_<field-name>()`. This can be combined with `publish` to also broadcast changes.
177183
* For each `signal` method:
178184
* `receive_<method-name>()` method (e.g., `receive_power_error()`) that returns a stream of
179185
signal events. The stream yields `<struct-name><method-name-in-pascal-case>Args` structs
@@ -196,6 +202,8 @@ The `controller` macro assumes that you have the following dependencies in your
196202
* Methods must be async.
197203
* The maximum number of subscribers state change and signal streams is 16. We plan to provide an
198204
attribute to make this configurable in the future.
199-
* The type of all published fields must implement `Clone` and `Debug`.
200-
* The signal and published fields' streams must be continuely polled. Otherwise notifications will
201-
be missed.
205+
* The type of all published fields must implement `Clone`.
206+
* Published field streams yield the current value on first poll, then subsequent changes. Only the
207+
latest value is stored; intermediate values may be missed if the stream is not polled between
208+
changes.
209+
* Signal streams must be continuously polled. Otherwise notifications will be missed.

0 commit comments

Comments
 (0)