Firmware Controller
This crate provides a macro named controller that makes it easy to decouple interactions between
components in a no_std environment.
Intro
This crate provides a macro named controller that makes it easy to write controller logic for
firmware.
The controller is responsible for control of all the peripherals based on commands it receives from other parts of the code. It also notifies peers about state changes and events via signals. This macro generates all the boilerplate code and client-side API for you.
Usage
It's best described by an example so let's take example of a very simple firmware that controls an LED:
use controller;
use *;
async
// This is just a very silly client that keeps flipping the power state every 1 second.
async
Details
The controller macro will generate the following for you:
Controller struct
- A
newmethod that takes the fields of the struct as arguments and returns the struct. - For each
publishedfield:- Setter for this field, named
set_<field-name>(e.g.,set_state), which broadcasts any changes made to this field.
- Setter for this field, named
- A
runmethod with signaturepub 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 yourmainor run it as a task. - For each
signalmethod:- 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
publishedfield:receive_<field-name>_changed()method (e.g.,receive_state_changed()) that returns a stream of state values. The first value yielded is the current state at subscription time, and subsequent values are emitted when the field changes. The stream yields values of the field type directly (e.g.,State).
- For each field with a
getterattribute (e.g.,#[controller(getter)]or#[controller(getter = "custom_name")]), a getter method is generated on the client. The default name is the field name; a custom name can be specified. - For each field with a
setterattribute (e.g.,#[controller(setter)]or#[controller(setter = "custom_name")]), a public setter method is generated on the client, allowing external code to update the field value through the client API. The default setter name isset_<field-name>(). This can be combined withpublishto also broadcast changes. - For each
signalmethod: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>Argsstructs (e.g.,ControllerPowerErrorArgs) containing all signal arguments as public fields.
Dependencies assumed
The controller macro assumes that you have the following dependencies in your Cargo.toml:
futureswithasync-awaitfeature enabled.embassy-sync
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 attribute to make this configurable in the future.
- The type of all published fields must implement
Clone. - Published field streams yield the current value on first poll, then subsequent changes. Only the latest value is stored; intermediate values may be missed if the stream is not polled between changes.
- Signal streams must be continuously polled. Otherwise notifications will be missed.