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

Battery Service Preparation

We've successfully exposed and proven our implementation of battery traits and their values for our mock battery, and built for an embedded target. In this step, we'll continue our integration by connecting to a battery service, but that requires some setup to cover first.

Battery-service

The ODP repository embedded-services has the battery-service we need for this, as well as the power-policy infrastructure support that uses it.

We already have our embedded-batteries submodule in our project space from the first steps. We'll do the same thing to bring in what we need from embedded-services.

We will also need the repositories embedded-cfu, and embedded-usb-pd although we won't really be using the features of these while we are in a non-embedded (std) build environment, the dependencies are still needed for reference by the other dependencies.

The same is also true for Embassy, since some of the embassy_time traits are used by ODP signatures we will be attaching to.

In the battery_project directory:

git submodule add https://github.com/OpenDevicePartnership/embedded-services
git submodule add https://github.com/OpenDevicePartnership/embedded-cfu
git submodule add https://github.com/OpenDevicePartnership/embedded-usb-pd
git submodule add https://github.com/embassy-rs/embassy.git 

Checking the repository examples

Within the embedded-services repository files, you will find a directory named examples. We can find files in the examples/std/src/bin/ folder that speak to battery and power_policy implementations, as well as other concerns. You should familiarize yourself with these examples.

In this exercise we will be borrowing from those designs in a curated fashion. If at any time there is question about the implementation presented in this exercise, please consult the examples in the repository, as they may contain updated information.

A Mock Battery Device

To fit the design of the ODP battery service, we first need to create a wrapper that contains our MockBattery and a Device Trait. We need to implement DeviceContainer for this wrapper and reference that Device. Then we will register the wrapper with register_device(...) and we will have an async loop that awaits commands on the Device's channel, executes them, and updates state.

Import the battery-service from the ODP crate

One of the service definitions from the embedded-services repository we brought into scope is the battery-service. We now need to update our Cargo.toml to know where to find it. Open the Cargo.toml file of your mock-battery project and add the dependency to the battery-service path. We will also need a reference to embedded-services itself for various support needs. We will no longer be requiring tokio, so you can remove that dependency, but we do need to import crate references from embassy. Update your mock_battery/Cargo.toml so that your [dependencies] section now looks like this to include references we will need.

Your new [dependencies] section should now look like this:

[dependencies]
embedded-batteries-async = { path = "../embedded-batteries/embedded-batteries-async" }
battery-service = { path = "../embedded-services/battery-service" }
embedded-services = { path = "../embedded-services/embedded-service" }
embassy-executor = { workspace = true }
embassy-time = { workspace = true, features=["std"] }
embassy-sync = { workspace = true, features=["std"] }
critical-section = {version = "1.0", features = ["std"] }
async-trait = "0.1"
# tokio = { version = "1.45", features = ["full"] }
static_cell = "1.0"
once_cell = { workspace = true }

This will allow us to import what we need for the next steps.

Top-level Cargo.toml

Note that some of these dependencies say 'workspace = true'. This implies they are in the workspace as configured by our top-level Cargo.toml, at battery_project/Cargo.toml. We need to update our top-level Cargo.toml to include these. In battery_project/Cargo.toml add this section and settings:

[workspace.dependencies]
embassy-executor = { path = "embassy/embassy-executor", features = ["arch-std", "executor-thread"], default-features = false }
embassy-time = { path = "embassy/embassy-time" }
embassy-futures = "0.1.0"
embassy-sync = "0.7.0"
embassy-time-driver = "0.2.0"
embedded-hal = "1.0"
embedded-hal-async = "1.0"

and you will want to add this section as well. This tells cargo to use our local submodule version of embassy rather than reaching out to crates-io for a version:

[patch.crates-io]
embassy-executor = { path = "embassy/embassy-executor"}
embassy-time = { path = "embassy/embassy-time" }
embassy-time-driver = { path = "embassy/embassy-time-driver" }
embassy-time-queue-utils = { path = "embassy/embassy-time-queue-utils" }

But we are not done yet. If we execute cargo build at this point, we will likely get an error that says there was an "error inheriting once_cell from workspace root manifest's workspace.dependencies.once_cell

We can solve that by adding that reference to [workspace.dependencies]

once_cell = "1.19"

Still not done. If we execute cargo build at this point, we will likely get an error that says there was an "error inheriting defmt from workspace root manifest's workspace.dependencies.defmt" and "workspace.dependencies was not defined".

This is because these dependencies are used by the dependencies that we have included, even if we aren't using them ourselves. In many cases, such as those dependencies that are relying on packages like embassy for embedded support, we won't be using at all in our 'std' build environment, and these will be compiled out of our build as a result, but they must still be referenced to satisfy the dependency chain.

To remedy this, we must edit the top-level Cargo.toml (battery_project/cargo.toml) to include a reference to defmt, such as

[workspace.dependencies]
defmt = "1.0"

and when you try again, you will get another error specifying the next missing dependency reference. Add these placeholder references in the same way. For now, don't worry about the version. Make each reference = "1.0".

For references to dependencies we are using in our project (embedded-batteries, embedded-batteries-async, embedded-services, battery-service), specify these by providing their path, as in:

embedded-batteries-async = { path = "embedded-batteries/embedded-batteries-async" }
embedded-services = { path = "embedded-services/embedded-service" }
battery-service = { path = "embedded-services/battery-service" }
embedded-cfu-protocol = { path = "embedded-cfu" }
embedded-usb-pd = { path = "embedded-usb-pd" }

Once all the dependencies have been named, cargo build will start to complain about acceptable version numbers for those where the "1.0" placeholder will not suffice. For example:

error: failed to select a version for the requirement `embassy-executor = "^1.0"`
candidate versions found which didn't match: 0.7.0, 0.6.3, 0.6.2, ...

So in these cases, change the "1.0" to one of the versions from the list ("0.7.0")

After doing all of this, your [workspace.dependencies] section will look something like this:

[workspace.dependencies]
embassy-executor = { path = "embassy/embassy-executor", features = ["arch-std", "executor-thread"], default-features = false }
embassy-time = { path = "embassy/embassy-time" }
embassy-futures = "0.1.0"
embassy-sync = "0.7.0"
embassy-time-driver = "0.2.0"
embedded-hal = "1.0"
embedded-hal-async = "1.0"
once_cell = "1.19"
defmt = "1.0"
log = "0.4.27"
bitfield = "0.19.1"
bitflags = "1.0"
bitvec = "1.0"
cfg-if = "1.0"
chrono = "0.4.41"
critical-section = {version = "1.0", features = ["std"] }
document-features = "0.2.11"
embedded-hal-nb = "1.0"
embedded-io = "0.6.1"
embedded-io-async = "0.6.1"
embedded-storage = "0.3.1"
embedded-storage-async = "0.4.1"
fixed = "1.0"
heapless = "0.8.0"
postcard = "1.0"
rand_core = "0.9.3"
serde = "1.0"
cortex-m = "0.7.7"
cortex-m-rt = "0.7.5"
embedded-batteries = { path = "embedded-batteries/embedded-batteries" }
embedded-batteries-async = { path = "embedded-batteries/embedded-batteries-async" }
embedded-services = { path = "embedded-services/embedded-service" }
battery-service = { path = "embedded-services/battery-service" }
embedded-cfu-protocol = { path = "embedded-cfu" }
embedded-usb-pd = { path = "embedded-usb-pd" }

(Note: the entries above also include dependencies for some items we will need in upcoming steps and haven't encountered yet)

Insure cargo clean and cargo build succeeds with your dependencies referenced accordingly before proceeding to the next step.

Define the MockBatteryDevice wrapper

In your mock_battery project src folder, create a new file named mock_battery_device.rs and give it this content:

#![allow(unused)]
fn main() {
use crate::mock_battery::MockBattery;
use embedded_services::power::policy::DeviceId;
use embedded_services::power::policy::action::device::AnyState;
use embedded_services::power::policy::device::{
    Device, DeviceContainer, CommandData, ResponseData//, State
};


pub struct MockBatteryDevice {
    #[allow(dead_code)] // Prevent unused warning for MockBattery -- not used yet   
    battery: MockBattery,
    device: Device,
}

impl MockBatteryDevice {
    pub fn new(id: DeviceId) -> Self {
    Self {
            battery: MockBattery::new(),
            device: Device::new(id)
        }
    }

    pub fn device(&self) -> &Device {
        &self.device
    }

    pub fn inner_battery(&mut self) -> &mut MockBattery {
        &mut self.battery
    }   

    pub async fn run(&self) {
        loop {
            let cmd = self.device.receive().await;

            // Access command using the correct method
            let request = &cmd.command; 

            match request {
                CommandData::ConnectAsConsumer(cap) => {
                    println!("Received ConnectConsumer for {}mA @ {}mV", cap.current_ma, cap.voltage_mv);

                    // Safe placeholder: detach any existing state
                    match self.device.device_action().await {
                        AnyState::ConnectedProvider(dev) => {
                            if let Err(e) = dev.detach().await {
                                println!("Detach failed: {:?}", e);
                            }
                        }
                        AnyState::ConnectedConsumer(dev) => {
                            if let Err(e) = dev.detach().await {
                                println!("Detach failed: {:?}", e);
                            }
                        }
                        _ => (),
                    }

                    cmd.respond(Ok(ResponseData::Complete));
                }

                CommandData::ConnectAsProvider(cap) => {
                    println!("Received ConnectProvider for {}mA @ {}mV", cap.current_ma, cap.voltage_mv);

                    match self.device.device_action().await {
                        AnyState::ConnectedProvider(dev) => {
                            if let Err(e) = dev.detach().await {
                                println!("Detach failed: {:?}", e);
                            }
                        }
                        AnyState::ConnectedConsumer(dev) => {
                            if let Err(e) = dev.detach().await {
                                println!("Detach failed: {:?}", e);
                            }
                        }
                        _ => (),
                    }

                    cmd.respond(Ok(ResponseData::Complete));
                }

                CommandData::Disconnect => {
                    println!("Received Disconnect");

                    match self.device.device_action().await {
                        AnyState::ConnectedProvider(dev) => {
                            if let Err(e) = dev.detach().await {
                                println!("Detach failed: {:?}", e);
                            }
                        }
                        AnyState::ConnectedConsumer(dev) => {
                            if let Err(e) = dev.detach().await {
                                println!("Detach failed: {:?}", e);
                            }
                        }
                        _ => {
                            println!("Already disconnected or idle");
                        }
                    }

                    cmd.respond(Ok(ResponseData::Complete));
                }
            }
        }
    }
}

impl DeviceContainer for MockBatteryDevice {
    fn get_power_policy_device(&self) -> &Device {
        &self.device
    }
}
}

What we've done here is:

  • Imported what we need from the ODP repositories for both the SmartBattery definition from embedded-batteries_async and the battery service components from embedded-services crates as as our own local MockBattery definition.

  • Defined and implemented our MockBatteryDevice

  • implemented a run loop for our MockBatteryDevice

Note we have some println! statements here to echo when certain events occur. These won't be seen until later, but we want feedback when we do hook things up in our pre-test example.

Including mock_battery_device

Just like we had to inform the build of our mock_battery and virtual_battery, we need to do likewise with mock_battery_device. So edit lib.rs into this:

#![allow(unused)]
fn main() {
pub mod mock_battery;
pub mod virtual_battery;
pub mod mock_battery_device;```

After you've done all that,  you should be able to build with 
}

cargo build

and get a clean result
_Note: if you commented out or removed the reference to `tokio` in your `Cargo.toml` you may need to put that back to compile against the existing `main.rs`, but we will be replacing `main.rs` shortly._

Next we will work to put this battery to use.