Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Embedded Controller Service Registration

Embedded Controller components and services in ODP are statically composed at build time but must be registered with the service infrastructure to become discoverable and operational during execution.

This registration model allows each policy domain (e.g. power, thermal, charging) to own and manage the devices associated with it.


Registration Pattern

Pseudocode (concept only)
The registration flow is: construct → init services → register.

let battery = BatteryDevice::new(DeviceId(1));
// ...
// in an async task function
embedded_services::init().await;
embedded_services::power::policy::register_device(&battery).await.unwrap();

This omits static allocation (StaticCell), executor wiring for async tasks, and controller setup, for clarity.

Realistic skeleton (matches the sample projects)

Refer to the Battery implementation example or the examples in the embedded-services repository for more concrete examples.

#![allow(unused)]
fn main() {
// statically allocate single ownership
static BATTERY: StaticCell<MockBatteryDevice> = StaticCell::new();

// Construct a device handle
let battery = BATTERY.init(MockBatteryDevice::new(DeviceId(1)));

// In an async context, initialize services once, then register the device
embedded_services::init().await;
embedded_services::power::policy::register_device(&battery).await.unwrap();

// Controller setup is covered in the next section.
}

Semantics note: In ODP, “Device” types are handles to a single underlying component; the service runtime serializes access. Introducing the controller simply gives policy logic a dedicated handle to act on; it does not create a second owner of the hardware.

Bringing in the Controller

#![allow(unused)]
fn main() {
let controller = CONTROLLER.init(
    MockBatteryController::<&'static mut MockBattery>::new(battery.inner_battery())
);
}

The controller is given a handle to the inner MockBattery (inner_battery()), not a second owner of the hardware. All access is serialized through the service runtime.

What Registration Enables

FeatureEnabled by Registration
Message RoutingThe comms system delivers events to services
Task SpawningServices are polled and run by the executor
Feature ExposureSubfeatures (e.g. fuel_gauge) declared via trait contracts
Test VisibilityServices and devices can be observed in tests
flowchart TD
    A[Component<br/>_Implements Trait_] --> B[Device Wrapper]
    B --> C[Controller<br/>_Implements Service Trait_]
    C --> D[SERVICE_REGISTRY]
    D --> E[Policy Manager<br/>_via Comms_]
    D -->|_Provides_| F[Async Task Execution]

Figure: Service Registration and Runtime Execution

Devices are wrapped and managed by controllers. These are registered into the service registry, which exposes them to both the message dispatcher and the async runtime for polling and task orchestration.

Message Dispatch and Service Binding

Once a controller is registered, the service registry allows the comms system to route incoming events to the correct service based on:

  • The device ID
  • The message type
  • The controller's implementation of the handle() function (as defined by ServiceTraits)

When a message is emitted (e.g. BatteryEvent::UpdateStatus), the comms channel looks up the appropriate service and dispatches the message.

Where ServiceTraits represent the service traits that define a Controller action, implementation may look something like this (in this case, ServiceTraits defines a function named handle, and it calls upon a local function defined in the device implementation):

#![allow(unused)]
fn main() {
impl ServiceTraits for BatteryController {
    async fn handle(&mut self, msg: Message) -> Result<()> {
        match msg {
            Message::Battery(BatteryEvent::UpdateStatus) => {
                self.device.update().await
            }
            _ => Ok(()),
        }
    }
}
}

This provides a flexible pattern where services are matched to message types through trait implementations and static dispatch. No dynamic routing or introspection is used — behavior is known at compile time.

Static Composition, Dynamic Coordination

While all services and components are statically bound into the final binary:

  • Message routing and task polling occur dynamically
  • Controllers only receive messages for devices they were registered to manage
  • Multiple services can be registered independently and coexist without conflict

This pattern supports:

  • Easy testing with mocks or alternate HALs
  • Additive subsystem design (battery, charger, thermal)
  • Isolated debugging of service behavior