diff --git a/Cargo.lock b/Cargo.lock index 7f136223..cca94a6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,6 +221,7 @@ dependencies = [ "heapless", "log", "mctp-rs", + "odp-service-common", "power-policy-interface", "zerocopy", ] diff --git a/battery-service/Cargo.toml b/battery-service/Cargo.toml index 6d867a81..0483e816 100644 --- a/battery-service/Cargo.toml +++ b/battery-service/Cargo.toml @@ -21,6 +21,7 @@ embedded-hal-async.workspace = true embedded-hal.workspace = true embedded-services.workspace = true log = { workspace = true, optional = true } +odp-service-common.workspace = true zerocopy.workspace = true mctp-rs = { workspace = true, features = ["espi"] } heapless.workspace = true diff --git a/battery-service/src/lib.rs b/battery-service/src/lib.rs index 8a739169..9ff84781 100644 --- a/battery-service/src/lib.rs +++ b/battery-service/src/lib.rs @@ -6,7 +6,7 @@ use battery_service_messages::{AcpiBatteryError, AcpiBatteryRequest, AcpiBattery use context::BatteryEvent; use embedded_services::{ comms::{self, EndpointID}, - error, trace, + error, info, trace, }; mod acpi; @@ -15,104 +15,192 @@ pub mod controller; pub mod device; #[cfg(feature = "mock")] pub mod mock; -pub mod task; pub mod wrapper; -/// Standard Battery Service. -pub struct Service { - pub endpoint: comms::Endpoint, - pub context: context::Context, +/// Parameters required to initialize the battery service. +pub struct InitParams<'hw, const N: usize> { + pub devices: [&'hw device::Device; N], + pub config: context::Config, } -impl Service { - /// Create a new battery service instance. - pub const fn new() -> Self { - Self::new_inner(context::Config::new()) - } - - /// Create a new battery service instance with context configuration. - pub const fn new_with_ctx_config(config: context::Config) -> Self { - Self::new_inner(config) - } +/// The main service implementation. +struct ServiceInner { + endpoint: comms::Endpoint, + context: context::Context, +} - const fn new_inner(config: context::Config) -> Self { - Service { +impl ServiceInner { + fn new(config: context::Config) -> Self { + Self { endpoint: comms::Endpoint::uninit(comms::EndpointID::Internal(comms::Internal::Battery)), context: context::Context::new_with_config(config), } } /// Main battery service processing function. - pub async fn process_next(&self) { + async fn process_next(&self) { let event = self.wait_next().await; self.process_event(event).await } /// Wait for next event. - pub async fn wait_next(&self) -> BatteryEvent { + async fn wait_next(&self) -> BatteryEvent { self.context.wait_event().await } /// Process battery service event. - pub async fn process_event(&self, event: BatteryEvent) { + async fn process_event(&self, event: BatteryEvent) { trace!("Battery service: state machine event recvd {:?}", event); self.context.process(event).await } /// Register fuel gauge device with the battery service. - /// - /// Must be done before sending the battery service commands so that hardware device is visible - /// to the battery service. - pub(crate) fn register_fuel_gauge( + fn register_fuel_gauge( &self, device: &'static device::Device, ) -> Result<(), embedded_services::intrusive_list::Error> { self.context.register_fuel_gauge(device)?; - Ok(()) } /// Use the battery service endpoint to send data to other subsystems and services. - pub async fn comms_send(&self, endpoint_id: EndpointID, data: &(impl Any + Send + Sync)) -> Result<(), Infallible> { + async fn comms_send(&self, endpoint_id: EndpointID, data: &(impl Any + Send + Sync)) -> Result<(), Infallible> { self.endpoint.send(endpoint_id, data).await } + /// Send the battery service state machine an event and await a response. + async fn execute_event(&self, event: BatteryEvent) -> context::BatteryResponse { + self.context.execute_event(event).await + } + + /// Wait for a response from the battery service. + async fn wait_for_battery_response(&self) -> context::BatteryResponse { + self.context.wait_response().await + } + + /// Asynchronously query the state from the state machine. + async fn get_state(&self) -> context::State { + self.context.get_state().await + } +} + +/// The memory resources required by the battery service. +#[derive(Default)] +pub struct Resources { + inner: Option>, +} + +/// A task runner for the battery service. Users of the service must run this object in an embassy task or similar async execution context. +pub struct Runner<'hw, const N: usize> { + service: &'hw ServiceInner, +} + +impl<'hw, const N: usize> odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw, N> { + /// Run the service. + async fn run(self) -> embedded_services::Never { + info!("Starting battery-service"); + loop { + self.service.process_next().await; + } + } +} + +/// Control handle for the battery service. Use this to interact with the battery service. +#[derive(Clone, Copy)] +pub struct Service<'hw, const N: usize> { + inner: &'hw ServiceInner, +} + +impl<'hw, const N: usize> Service<'hw, N> { + /// Main battery service processing function. + pub async fn process_next(&self) { + self.inner.process_next().await + } + + /// Wait for next event. + pub async fn wait_next(&self) -> BatteryEvent { + self.inner.wait_next().await + } + + /// Process battery service event. + pub async fn process_event(&self, event: BatteryEvent) { + self.inner.process_event(event).await + } + + /// Use the battery service endpoint to send data to other subsystems and services. + pub async fn comms_send(&self, endpoint_id: EndpointID, data: &(impl Any + Send + Sync)) -> Result<(), Infallible> { + self.inner.comms_send(endpoint_id, data).await + } + /// Send the battery service state machine an event and await a response. /// /// This is an alternative method of interacting with the battery service (instead of using the comms service), /// and is a useful fn if you want to send an event and await a response sequentially. pub async fn execute_event(&self, event: BatteryEvent) -> context::BatteryResponse { - self.context.execute_event(event).await + self.inner.execute_event(event).await } /// Wait for a response from the battery service. /// /// Use this function after sending the battery service a message via the comms system. pub async fn wait_for_battery_response(&self) -> context::BatteryResponse { - self.context.wait_response().await + self.inner.wait_for_battery_response().await } /// Asynchronously query the state from the state machine. pub async fn get_state(&self) -> context::State { - self.context.get_state().await + self.inner.get_state().await } } -impl Default for Service { - fn default() -> Self { - Self::new() +/// Errors that can occur during battery service initialization. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum InitError { + DeviceRegistrationFailed, + CommsRegistrationFailed, +} + +impl<'hw, const N: usize> odp_service_common::runnable_service::Service<'hw> for Service<'hw, N> +where + 'hw: 'static, // TODO relax this 'static requirement when we drop usages of IntrusiveList (including comms) +{ + type Runner = Runner<'hw, N>; + type ErrorType = InitError; + type InitParams = InitParams<'hw, N>; + type Resources = Resources; + + async fn new( + service_storage: &'hw mut Resources, + init_params: Self::InitParams, + ) -> Result<(Self, Runner<'hw, N>), InitError> { + let service = service_storage.inner.insert(ServiceInner::new(init_params.config)); + + for device in init_params.devices { + if service.register_fuel_gauge(device).is_err() { + error!("Failed to register battery device with DeviceId {:?}", device.id()); + return Err(InitError::DeviceRegistrationFailed); + } + } + + if comms::register_endpoint(service, &service.endpoint).await.is_err() { + error!("Failed to register battery service endpoint"); + return Err(InitError::CommsRegistrationFailed); + } + + Ok((Self { inner: service }, Runner { service })) } } -impl embedded_services::relay::mctp::RelayServiceHandlerTypes for Service { +impl embedded_services::relay::mctp::RelayServiceHandlerTypes for Service<'_, N> { type RequestType = AcpiBatteryRequest; type ResultType = AcpiBatteryResult; } -impl embedded_services::relay::mctp::RelayServiceHandler for Service { +impl embedded_services::relay::mctp::RelayServiceHandler for Service<'_, N> { async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { trace!("Battery service: ACPI cmd recvd"); - let response = self.context.process_acpi_cmd(&request).await; + let response = self.inner.context.process_acpi_cmd(&request).await; if let Err(e) = response { error!("Battery service command failed: {:?}", e) } @@ -120,7 +208,7 @@ impl embedded_services::relay::mctp::RelayServiceHandler for Service { } } -impl comms::MailboxDelegate for Service { +impl comms::MailboxDelegate for ServiceInner { fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { if let Some(event) = message.data.get::() { self.context.send_event_no_wait(*event).map_err(|e| match e { diff --git a/battery-service/src/mock.rs b/battery-service/src/mock.rs index 2fafe5e5..25ba29de 100644 --- a/battery-service/src/mock.rs +++ b/battery-service/src/mock.rs @@ -6,7 +6,9 @@ use embedded_batteries_async::{ use embedded_services::{GlobalRawMutex, error, info}; // Convenience fns -pub async fn init_state_machine(battery_service: &'static crate::Service) -> Result<(), crate::context::ContextError> { +pub async fn init_state_machine( + battery_service: &crate::Service<'_, N>, +) -> Result<(), crate::context::ContextError> { battery_service .execute_event(crate::context::BatteryEvent { event: crate::context::BatteryEventInner::DoInit, @@ -34,7 +36,7 @@ pub async fn init_state_machine(battery_service: &'static crate::Service) -> Res Ok(()) } -pub async fn recover_state_machine(battery_service: &'static crate::Service) -> Result<(), ()> { +pub async fn recover_state_machine(battery_service: &crate::Service<'_, N>) -> Result<(), ()> { loop { match battery_service .execute_event(crate::context::BatteryEvent { diff --git a/battery-service/src/task.rs b/battery-service/src/task.rs deleted file mode 100644 index 8f38285a..00000000 --- a/battery-service/src/task.rs +++ /dev/null @@ -1,42 +0,0 @@ -use battery_service_messages::DeviceId; -use embedded_services::{comms, error, info}; - -use crate::{Service, device::Device}; - -/// Standard dynamic battery data cache -#[derive(Debug, Clone)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum InitError { - DeviceRegistrationFailed(heapless::Vec), - CommsRegistrationFailed, -} - -/// Battery service task. -pub async fn task( - service: &'static Service, - devices: [&'static Device; N], -) -> Result<(), InitError> { - info!("Starting battery-service task"); - - let mut failed_devices = heapless::Vec::new(); - for device in devices { - if service.register_fuel_gauge(device).is_err() { - error!("Failed to register battery device with DeviceId {:?}", device.id()); - // Infallible as the Vec is as large as the list of devices passed in. - let _ = failed_devices.push(device.id()); - } - } - - if !failed_devices.is_empty() { - return Err(InitError::DeviceRegistrationFailed(failed_devices)); - } - - if comms::register_endpoint(service, &service.endpoint).await.is_err() { - error!("Failed to register battery service endpoint"); - return Err(InitError::CommsRegistrationFailed); - } - - loop { - service.process_next().await; - } -} diff --git a/examples/pico-de-gallo/Cargo.lock b/examples/pico-de-gallo/Cargo.lock index 87248daf..60b6d131 100644 --- a/examples/pico-de-gallo/Cargo.lock +++ b/examples/pico-de-gallo/Cargo.lock @@ -181,6 +181,7 @@ dependencies = [ "heapless 0.8.0", "log", "mctp-rs", + "odp-service-common", "power-policy-interface", "zerocopy", ] @@ -1169,6 +1170,14 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "odp-service-common" +version = "0.1.0" +dependencies = [ + "embedded-services", + "static_cell", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1208,6 +1217,7 @@ dependencies = [ "embedded-services", "env_logger", "log", + "odp-service-common", "pico-de-gallo-hal", "static_cell", "tokio", diff --git a/examples/pico-de-gallo/Cargo.toml b/examples/pico-de-gallo/Cargo.toml index 2528ce37..f86ebbcf 100644 --- a/examples/pico-de-gallo/Cargo.toml +++ b/examples/pico-de-gallo/Cargo.toml @@ -25,6 +25,7 @@ embassy-futures = "0.1.2" embedded-batteries-async = "0.3" battery-service = { path = "../../battery-service", features = ["log"] } +odp-service-common = { path = "../../odp-service-common" } pico-de-gallo-hal = "0.1.0" diff --git a/examples/pico-de-gallo/src/bin/battery.rs b/examples/pico-de-gallo/src/bin/battery.rs index e79b2edd..b4d165c0 100644 --- a/examples/pico-de-gallo/src/bin/battery.rs +++ b/examples/pico-de-gallo/src/bin/battery.rs @@ -19,6 +19,7 @@ use battery_service as bs; use bq40z50_rx::{BQ40Z50Error, Bq40z50R5}; use embedded_batteries_async::smart_battery::{BatteryModeFields, SmartBattery}; +use odp_service_common::runnable_service::{Service, ServiceRunner}; use static_cell::StaticCell; /// Platform specific battery errors. @@ -151,35 +152,7 @@ impl bs::controller::Controller for Battery { } } -async fn init_and_run_service( - battery_service: &'static battery_service::Service, - i2c: pico_de_gallo_hal::I2c, - delay: pico_de_gallo_hal::Delay, -) -> ! { - embedded_services::debug!("Initializing battery service"); - embedded_services::init().await; - - static BATTERY_DEVICE: StaticCell = StaticCell::new(); - static BATTERY_WRAPPER: StaticCell> = StaticCell::new(); - let device = BATTERY_DEVICE.init(bs::device::Device::new(bs::device::DeviceId(0))); - - let wrapper = BATTERY_WRAPPER.init(bs::wrapper::Wrapper::new( - device, - Battery { - driver: Bq40z50R5::new(i2c, delay), - }, - )); - - // Run battery service - let _ = embassy_futures::join::join( - tokio::spawn(battery_service::task::task(battery_service, [device])), - tokio::spawn(wrapper.process()), - ) - .await; - unreachable!() -} - -async fn init_state_machine(battery_service: &'static bs::Service) -> Result<(), bs::context::ContextError> { +async fn init_state_machine(battery_service: &bs::Service<'static, 1>) -> Result<(), bs::context::ContextError> { battery_service .execute_event(battery_service::context::BatteryEvent { event: battery_service::context::BatteryEventInner::DoInit, @@ -207,7 +180,7 @@ async fn init_state_machine(battery_service: &'static bs::Service) -> Result<(), Ok(()) } -async fn recover_state_machine(battery_service: &'static battery_service::Service) -> Result<(), ()> { +async fn recover_state_machine(battery_service: &battery_service::Service<'static, 1>) -> Result<(), ()> { loop { match battery_service .execute_event(battery_service::context::BatteryEvent { @@ -238,10 +211,10 @@ async fn recover_state_machine(battery_service: &'static battery_service::Servic } } -pub async fn run_app(battery_service: &'static battery_service::Service) { +pub async fn run_app(battery_service: battery_service::Service<'static, 1>) { // Initialize battery state machine. let mut retries = 5; - while let Err(e) = init_state_machine(battery_service).await { + while let Err(e) = init_state_machine(&battery_service).await { retries -= 1; if retries <= 0 { embedded_services::error!("Failed to initialize Battery: {:?}", e); @@ -281,7 +254,7 @@ pub async fn run_app(battery_service: &'static battery_service::Service) { failures = 0; count = 0; embedded_services::error!("FG: Too many errors, timing out and starting recovery..."); - if recover_state_machine(battery_service).await.is_err() { + if recover_state_machine(&battery_service).await.is_err() { embedded_services::error!("FG: Fatal error"); return; } @@ -296,13 +269,39 @@ async fn main() { env_logger::builder().filter_level(log::LevelFilter::Info).init(); embedded_services::info!("host: battery example started"); - static BATTERY_SERVICE: bs::Service = bs::Service::new(); + embedded_services::debug!("Initializing battery service"); + embedded_services::init().await; let p = pico_de_gallo_hal::Hal::new(); - let _ = embassy_futures::join::join( - tokio::spawn(run_app(&BATTERY_SERVICE)), - tokio::spawn(init_and_run_service(&BATTERY_SERVICE, p.i2c(), p.delay())), + static BATTERY_DEVICE: StaticCell = StaticCell::new(); + let device = BATTERY_DEVICE.init(bs::device::Device::new(bs::device::DeviceId(0))); + + static BATTERY_WRAPPER: StaticCell> = StaticCell::new(); + let wrapper = BATTERY_WRAPPER.init(bs::wrapper::Wrapper::new( + device, + Battery { + driver: Bq40z50R5::new(p.i2c(), p.delay()), + }, + )); + + static BATTERY_SERVICE: StaticCell> = StaticCell::new(); + let (battery_service, runner) = bs::Service::new( + BATTERY_SERVICE.init(Default::default()), + bs::InitParams { + config: Default::default(), + devices: [device], + }, + ) + .await + .expect("failed to initialize battery service"); + + // Run battery service + let _ = embassy_futures::join::join3( + tokio::spawn(runner.run()), + tokio::spawn(wrapper.process()), + tokio::spawn(run_app(battery_service)), ) .await; + unreachable!(); } diff --git a/examples/rt633/Cargo.lock b/examples/rt633/Cargo.lock index 10c51c85..2776343e 100644 --- a/examples/rt633/Cargo.lock +++ b/examples/rt633/Cargo.lock @@ -52,6 +52,7 @@ dependencies = [ "embedded-services", "heapless", "mctp-rs", + "odp-service-common", "power-policy-interface", "zerocopy", ] diff --git a/examples/std/Cargo.lock b/examples/std/Cargo.lock index aa554b46..d32e7153 100644 --- a/examples/std/Cargo.lock +++ b/examples/std/Cargo.lock @@ -166,6 +166,7 @@ dependencies = [ "heapless", "log", "mctp-rs", + "odp-service-common", "power-policy-interface", "zerocopy", ] @@ -1319,6 +1320,14 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "odp-service-common" +version = "0.1.0" +dependencies = [ + "embedded-services", + "static_cell", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1667,6 +1676,7 @@ dependencies = [ "env_logger", "heapless", "log", + "odp-service-common", "power-policy-interface", "power-policy-service", "static_cell", diff --git a/examples/std/Cargo.toml b/examples/std/Cargo.toml index b63e125f..21aa1c4a 100644 --- a/examples/std/Cargo.toml +++ b/examples/std/Cargo.toml @@ -26,6 +26,7 @@ defmt = "0.3" embedded-usb-pd = { git = "https://github.com/OpenDevicePartnership/embedded-usb-pd" } embedded-services = { path = "../../embedded-service", features = ["log"] } +odp-service-common = { path = "../../odp-service-common" } power-policy-service = { path = "../../power-policy-service", features = [ "log", ] } diff --git a/examples/std/src/bin/battery.rs b/examples/std/src/bin/battery.rs index abd2d0df..0e3ea6f2 100644 --- a/examples/std/src/bin/battery.rs +++ b/examples/std/src/bin/battery.rs @@ -7,45 +7,46 @@ use embassy_executor::{Executor, Spawner}; use embassy_time::{Duration, Timer}; use static_cell::StaticCell; -#[embassy_executor::task] -async fn battery_service_task( - service: &'static battery_service::Service, - devices: [&'static battery_service::device::Device; 1], -) { - battery_service::task::task(service, devices) - .await - .expect("Failed to init battery service"); -} +use odp_service_common::runnable_service::spawn_service; #[embassy_executor::task] -async fn battery_wrapper_process(battery_wrapper: &'static battery_service::mock::MockBattery<'static>) { - battery_wrapper.process().await -} - -#[embassy_executor::task] -async fn init_and_run_service(spawner: Spawner, battery_service: &'static battery_service::Service) { +async fn embassy_main(spawner: Spawner) { embedded_services::debug!("Initializing battery service"); embedded_services::init().await; static BATTERY_DEVICE: StaticCell = StaticCell::new(); - static BATTERY_WRAPPER: StaticCell = StaticCell::new(); - let device = BATTERY_DEVICE.init(bs::device::Device::new(bs::device::DeviceId::default())); + let device = BATTERY_DEVICE.init(bs::device::Device::new(Default::default())); + let battery_service = spawn_service!( + spawner, + battery_service::Service<'static, 1>, + battery_service::InitParams { + config: Default::default(), + devices: [device], + } + ) + .expect("Failed to initialize battery service"); + + static BATTERY_WRAPPER: StaticCell = StaticCell::new(); let wrapper = BATTERY_WRAPPER.init(bs::wrapper::Wrapper::new( device, battery_service::mock::MockBatteryDriver::new(), )); - // Run battery service - spawner.must_spawn(battery_service_task(battery_service, [device])); + #[embassy_executor::task] + async fn battery_wrapper_process(battery_wrapper: &'static battery_service::mock::MockBattery<'static>) { + battery_wrapper.process().await + } + spawner.must_spawn(battery_wrapper_process(wrapper)); + spawner.must_spawn(run_app(battery_service)); } #[embassy_executor::task] -pub async fn run_app(battery_service: &'static battery_service::Service) { +pub async fn run_app(battery_service: battery_service::Service<'static, 1>) { // Initialize battery state machine. let mut retries = 5; - while let Err(e) = bs::mock::init_state_machine(battery_service).await { + while let Err(e) = bs::mock::init_state_machine(&battery_service).await { retries -= 1; if retries <= 0 { embedded_services::error!("Failed to initialize Battery: {:?}", e); @@ -85,7 +86,7 @@ pub async fn run_app(battery_service: &'static battery_service::Service) { failures = 0; count = 0; embedded_services::error!("FG: Too many errors, timing out and starting recovery..."); - if bs::mock::recover_state_machine(battery_service).await.is_err() { + if bs::mock::recover_state_machine(&battery_service).await.is_err() { embedded_services::error!("FG: Fatal error"); return; } @@ -99,13 +100,10 @@ fn main() { env_logger::builder().filter_level(log::LevelFilter::Debug).init(); embedded_services::info!("battery example started"); - static BATTERY_SERVICE: bs::Service = bs::Service::new(); - static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); // Run battery service executor.run(|spawner| { - spawner.must_spawn(run_app(&BATTERY_SERVICE)); - spawner.must_spawn(init_and_run_service(spawner, &BATTERY_SERVICE)); + spawner.must_spawn(embassy_main(spawner)); }); }