diff --git a/Cargo.lock b/Cargo.lock index 3acdfc114..54d806fa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2266,25 +2266,32 @@ dependencies = [ "embassy-sync", "embassy-time", "embedded-fans-async", - "embedded-hal 1.0.0", - "embedded-hal-async", "embedded-sensors-hal-async", "embedded-services", "heapless", "log", - "mctp-rs", "odp-service-common", - "thermal-service-messages", - "uuid", + "thermal-service-interface", +] + +[[package]] +name = "thermal-service-interface" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embassy-time", + "embedded-fans-async", + "embedded-sensors-hal-async", ] [[package]] -name = "thermal-service-messages" +name = "thermal-service-relay" version = "0.1.0" dependencies = [ "defmt 0.3.100", "embedded-services", "num_enum", + "thermal-service-interface", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index d3558a57d..2cb66d7f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ members = [ "battery-service", "battery-service-messages", "thermal-service", - "thermal-service-messages", + "thermal-service-interface", + "thermal-service-relay", "cfu-service", "embedded-service", "espi-service", @@ -105,7 +106,8 @@ rstest = { version = "0.26.1", default-features = false } serde = { version = "1.0.*", default-features = false } static_cell = "2.1.0" toml = { version = "0.8", default-features = false } -thermal-service-messages = { path = "./thermal-service-messages" } +thermal-service-interface = { path = "./thermal-service-interface" } +thermal-service-relay = { path = "./thermal-service-relay" } time-alarm-service-interface = { path = "./time-alarm-service-interface" } time-alarm-service-relay = { path = "./time-alarm-service-relay" } type-c-interface = { path = "./type-c-interface" } diff --git a/embedded-service/src/event.rs b/embedded-service/src/event.rs index ec02e7c53..a24736761 100644 --- a/embedded-service/src/event.rs +++ b/embedded-service/src/event.rs @@ -4,7 +4,8 @@ use core::{future::ready, marker::PhantomData}; use crate::error; use embassy_sync::{ - channel::{DynamicReceiver, DynamicSender}, + blocking_mutex::raw::RawMutex, + channel::{DynamicReceiver, DynamicSender, Receiver as ChannelReceiver, Sender as ChannelSender}, pubsub::{DynImmediatePublisher, DynSubscriber, WaitResult}, }; @@ -84,6 +85,26 @@ impl Receiver for DynSubscriber<'_, E> { } } +impl Sender for ChannelSender<'_, M, E, N> { + fn try_send(&mut self, event: E) -> Option<()> { + ChannelSender::try_send(self, event).ok() + } + + fn send(&mut self, event: E) -> impl Future { + ChannelSender::send(self, event) + } +} + +impl Receiver for ChannelReceiver<'_, M, E, N> { + fn try_next(&mut self) -> Option { + ChannelReceiver::try_receive(self).ok() + } + + fn wait_next(&mut self) -> impl Future { + ChannelReceiver::receive(self) + } +} + /// A sender that discards all events sent to it. pub struct NoopSender; diff --git a/examples/std/Cargo.lock b/examples/std/Cargo.lock index a4c0d09dd..f6f4d256d 100644 --- a/examples/std/Cargo.lock +++ b/examples/std/Cargo.lock @@ -1660,7 +1660,7 @@ dependencies = [ "power-policy-service", "static_cell", "thermal-service", - "thermal-service-messages", + "thermal-service-interface", "type-c-interface", "type-c-service", ] @@ -1719,25 +1719,21 @@ dependencies = [ "embassy-sync", "embassy-time", "embedded-fans-async", - "embedded-hal 1.0.0", - "embedded-hal-async", "embedded-sensors-hal-async", "embedded-services", "heapless", "log", - "mctp-rs", "odp-service-common", - "thermal-service-messages", - "uuid", + "thermal-service-interface", ] [[package]] -name = "thermal-service-messages" +name = "thermal-service-interface" version = "0.1.0" dependencies = [ - "embedded-services", - "num_enum", - "uuid", + "embassy-time", + "embedded-fans-async", + "embedded-sensors-hal-async", ] [[package]] diff --git a/examples/std/Cargo.toml b/examples/std/Cargo.toml index ce6047a7e..dd60ffb6c 100644 --- a/examples/std/Cargo.toml +++ b/examples/std/Cargo.toml @@ -44,7 +44,7 @@ type-c-interface = { path = "../../type-c-interface", features = ["log"] } embedded-sensors-hal-async = "0.3.0" embedded-fans-async = "0.2.0" thermal-service = { path = "../../thermal-service", features = ["log", "mock"] } -thermal-service-messages = { path = "../../thermal-service-messages" } +thermal-service-interface = { path = "../../thermal-service-interface" } env_logger = "0.11.8" log = "0.4.14" diff --git a/examples/std/src/bin/thermal.rs b/examples/std/src/bin/thermal.rs index f7585071e..fa3075494 100644 --- a/examples/std/src/bin/thermal.rs +++ b/examples/std/src/bin/thermal.rs @@ -1,47 +1,83 @@ use embassy_executor::{Executor, Spawner}; -use embassy_sync::once_lock::OnceLock; +use embassy_sync::channel::{Channel, Receiver as ChannelReceiver, Sender as ChannelSender}; use embassy_time::Timer; -use embedded_services::{error, info}; +use embedded_services::GlobalRawMutex; +use embedded_services::{info, warn}; use static_cell::StaticCell; use thermal_service as ts; +use thermal_service_interface::ThermalService; +use thermal_service_interface::fan::FanService; +use thermal_service_interface::sensor; +use thermal_service_interface::sensor::SensorService; + +// More readable type aliases for sensor, fan, and thermal services used in this example +type MockSensorService = ts::sensor::Service< + 'static, + ts::mock::sensor::MockSensor, + ChannelSender<'static, GlobalRawMutex, sensor::Event, 4>, + 16, +>; +type MockFanService = + ts::fan::Service<'static, ts::mock::fan::MockFan, MockSensorService, embedded_services::event::NoopSender, 16>; +type MockThermalService = ts::Service<'static, MockSensorService, MockFanService>; #[embassy_executor::task] async fn run(spawner: Spawner) { embedded_services::init().await; - static SENSOR: StaticCell = StaticCell::new(); - let sensor = SENSOR.init(ts::mock::new_sensor()); - - static FAN: StaticCell = StaticCell::new(); - let fan = FAN.init(ts::mock::new_fan()); - - static SENSORS: StaticCell<[&'static ts::sensor::Device; 1]> = StaticCell::new(); - let sensors = SENSORS.init([sensor.device()]); + // Create a backing channel for sensor events to be sent on + static SENSOR_EVENT_CHANNEL: StaticCell> = StaticCell::new(); + let sensor_event_channel = SENSOR_EVENT_CHANNEL.init(Channel::new()); - static FANS: StaticCell<[&'static ts::fan::Device; 1]> = StaticCell::new(); - let fans = FANS.init([fan.device()]); + // Then create the list of senders for the sensor service to use + // Though we are only using one sender in this example, an abitrary number could be used + static SENSOR_SENDERS: StaticCell<[ChannelSender<'static, GlobalRawMutex, sensor::Event, 4>; 1]> = + StaticCell::new(); + let event_senders = SENSOR_SENDERS.init([sensor_event_channel.sender()]); - static STORAGE: OnceLock> = OnceLock::new(); - let thermal_service = ts::Service::init(&STORAGE, sensors, fans).await; - - let _fan_service = odp_service_common::spawn_service!( + // Spawn the sensor service which will begin running and generating events + let sensor_service = odp_service_common::spawn_service!( spawner, - ts::fan::Service<'static, ts::mock::fan::MockFan, 16>, - ts::fan::InitParams { fan, thermal_service } + MockSensorService, + ts::sensor::InitParams { + driver: ts::mock::sensor::MockSensor::new(), + config: ts::mock::sensor::MockSensor::config(), + event_senders, + } ) - .expect("Failed to spawn fan service"); + .expect("Failed to spawn sensor service"); - let _sensor_service = odp_service_common::spawn_service!( + // Spawn the fan service which uses the above sensor service for automatic speed control + // In this example, we use an empty event sender list since the fan won't generate any events + let fan_service = odp_service_common::spawn_service!( spawner, - ts::sensor::Service<'static, ts::mock::sensor::MockSensor, 16>, - ts::sensor::InitParams { - sensor, - thermal_service + MockFanService, + ts::fan::InitParams { + driver: ts::mock::fan::MockFan::new(), + config: ts::mock::fan::MockFan::config(), + sensor_service, + event_senders: &mut [], } ) - .expect("Failed to spawn sensor service"); + .expect("Failed to spawn fan service"); + + // The thermal service accepts slices of associated sensors and fans, + // so we need static lifetime here since the thermal service handle is passed to task + static SENSORS: StaticCell<[MockSensorService; 1]> = StaticCell::new(); + let sensors = SENSORS.init([sensor_service]); + + static FANS: StaticCell<[MockFanService; 1]> = StaticCell::new(); + let fans = FANS.init([fan_service]); + + // The thermal service handle mainly exists for host relaying, but this example does not make use of that + // + // However, we can still use the thermal service handle to access registered sensors and fans by id + static RESOURCES: StaticCell> = StaticCell::new(); + let resources = RESOURCES.init(ts::Resources::default()); + let thermal_service = ts::Service::init(resources, ts::InitParams { sensors, fans }); spawner.must_spawn(monitor(thermal_service)); + spawner.must_spawn(sensor_event_listener(sensor_event_channel.receiver())); } fn main() { @@ -55,21 +91,24 @@ fn main() { } #[embassy_executor::task] -async fn monitor(service: &'static ts::Service<'static>) { +async fn sensor_event_listener(receiver: ChannelReceiver<'static, GlobalRawMutex, sensor::Event, 4>) { + loop { + let event = receiver.receive().await; + warn!("Sensor event: {:?}", event); + } +} + +#[embassy_executor::task] +async fn monitor(service: MockThermalService) { loop { - match service - .execute_sensor_request(ts::mock::MOCK_SENSOR_ID, ts::sensor::Request::GetTemp) - .await - { - Ok(ts::sensor::ResponseData::Temp(temp)) => info!("Mock sensor temp: {} C", temp), - _ => error!("Failed to monitor mock sensor temp"), + if let Some(sensor) = service.sensor(0) { + let temp = sensor.temperature().await; + info!("Mock sensor temp: {} C", temp); } - match service - .execute_fan_request(ts::mock::MOCK_FAN_ID, ts::fan::Request::GetRpm) - .await - { - Ok(ts::fan::ResponseData::Rpm(rpm)) => info!("Mock fan RPM: {}", rpm), - _ => error!("Failed to monitor mock fan RPM"), + + if let Some(fan) = service.fan(0) { + let rpm = fan.rpm().await; + info!("Mock fan RPM: {}", rpm); } Timer::after_secs(1).await; diff --git a/thermal-service-interface/Cargo.toml b/thermal-service-interface/Cargo.toml new file mode 100644 index 000000000..f3ff7885a --- /dev/null +++ b/thermal-service-interface/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "thermal-service-interface" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +defmt = { workspace = true, optional = true } +embassy-time.workspace = true +embedded-fans-async = "0.2.0" +embedded-sensors-hal-async = "0.3.0" + +[features] +defmt = [ + "dep:defmt", + "embassy-time/defmt", + "embedded-fans-async/defmt", + "embedded-sensors-hal-async/defmt", +] diff --git a/thermal-service-interface/src/fan.rs b/thermal-service-interface/src/fan.rs new file mode 100644 index 000000000..86a23a263 --- /dev/null +++ b/thermal-service-interface/src/fan.rs @@ -0,0 +1,133 @@ +use core::future::Future; +use embassy_time::Duration; +use embedded_fans_async::{Fan, RpmSense}; +use embedded_sensors_hal_async::temperature::DegreesCelsius; + +/// Ensures all necessary traits are implemented for the underlying fan driver. +pub trait Driver: Fan + RpmSense {} + +/// Fan error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Error { + /// Fan encountered a hardware failure. + Hardware, +} + +/// Fan event. +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Event { + /// Fan encountered a failure. + Failure(Error), +} + +/// Fan on (running) state. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum OnState { + /// Fan is on and running at its minimum speed. + Min, + /// Fan is ramping up or down along a curve in response to a temperature change. + Ramping, + /// Fan is running at its maximum speed. + Max, +} + +/// Fan state. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum State { + /// Fan is off. + Off, + /// Fan is on in the specified [`OnState`]. + On(OnState), +} + +/// Fan service interface trait. +pub trait FanService { + /// Enable automatic fan control. + /// + /// This allows the fan to automatically change [`State`] based on periodic readings from an associated temperature sensor. + fn enable_auto_control(&self) -> impl Future>; + /// Returns the most recently sampled RPM measurement. + fn rpm(&self) -> impl Future; + /// Returns the minimum RPM supported by the fan. + fn min_rpm(&self) -> impl Future; + /// Returns the maximum RPM supported by the fan. + fn max_rpm(&self) -> impl Future; + /// Returns the average RPM over a sampling period. + fn rpm_average(&self) -> impl Future; + /// Immediately samples the fan for an RPM measurement and returns the result. + fn rpm_immediate(&self) -> impl Future>; + /// Sets the fan to run at the specified RPM (and disables automatic control). + fn set_rpm(&self, rpm: u16) -> impl Future>; + /// Sets the fan to run at the specified duty cycle percentage (and disables automatic control). + fn set_duty_percent(&self, duty: u8) -> impl Future>; + /// Stops the fan (and disables automatic control). + fn stop(&self) -> impl Future>; + /// Set the rate at which RPM measurements are sampled. + fn set_rpm_sampling_period(&self, period: Duration) -> impl Future; + /// Set the rate at which the fan will update its RPM in response to a temperature change when in automatic control mode. + fn set_rpm_update_period(&self, period: Duration) -> impl Future; + /// Returns the temperature at which the fan will change to the specified [`OnState`] when in automatic control mode. + fn state_temp(&self, state: OnState) -> impl Future; + /// Sets the temperature at which the fan will change to the specified [`OnState`] when in automatic control mode. + fn set_state_temp(&self, state: OnState, temp: DegreesCelsius) -> impl Future; +} + +impl FanService for &T { + fn enable_auto_control(&self) -> impl Future> { + T::enable_auto_control(self) + } + + fn rpm(&self) -> impl Future { + T::rpm(self) + } + + fn min_rpm(&self) -> impl Future { + T::min_rpm(self) + } + + fn max_rpm(&self) -> impl Future { + T::max_rpm(self) + } + + fn rpm_average(&self) -> impl Future { + T::rpm_average(self) + } + + fn rpm_immediate(&self) -> impl Future> { + T::rpm_immediate(self) + } + + fn set_rpm(&self, rpm: u16) -> impl Future> { + T::set_rpm(self, rpm) + } + + fn set_duty_percent(&self, duty: u8) -> impl Future> { + T::set_duty_percent(self, duty) + } + + fn stop(&self) -> impl Future> { + T::stop(self) + } + + fn set_rpm_sampling_period(&self, period: Duration) -> impl Future { + T::set_rpm_sampling_period(self, period) + } + + fn set_rpm_update_period(&self, period: Duration) -> impl Future { + T::set_rpm_update_period(self, period) + } + + fn state_temp(&self, state: OnState) -> impl Future { + T::state_temp(self, state) + } + + fn set_state_temp(&self, state: OnState, temp: DegreesCelsius) -> impl Future { + T::set_state_temp(self, state, temp) + } +} diff --git a/thermal-service-interface/src/lib.rs b/thermal-service-interface/src/lib.rs new file mode 100644 index 000000000..b1e56a100 --- /dev/null +++ b/thermal-service-interface/src/lib.rs @@ -0,0 +1,17 @@ +#![no_std] + +pub mod fan; +pub mod sensor; + +/// Thermal service interface trait. +pub trait ThermalService { + /// Associated type for registered sensor services. + type Sensor: sensor::SensorService; + /// Associated type for registered fan services. + type Fan: fan::FanService; + + /// Retrieve a handle to the sensor service with the specified instance ID, if it exists. + fn sensor(&self, id: u8) -> Option; + /// Retrieve a handle to the fan service with the specified instance ID, if it exists. + fn fan(&self, id: u8) -> Option; +} diff --git a/thermal-service-interface/src/sensor.rs b/thermal-service-interface/src/sensor.rs new file mode 100644 index 000000000..70769e1bb --- /dev/null +++ b/thermal-service-interface/src/sensor.rs @@ -0,0 +1,98 @@ +use core::future::Future; +use embassy_time::Duration; +use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor}; + +/// Ensures all necessary traits are implemented for the underlying sensor driver. +pub trait Driver: TemperatureSensor {} + +/// Sensor error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Error { + /// Sensor encountered a hardware failure. + Hardware, + /// Retry attempts to communicate with sensor exhausted. + RetryExhausted, +} + +/// Sensor event. +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Event { + /// A sensor threshold was exceeded. + ThresholdExceeded(Threshold), + /// A sensor threshold which was previously exceeded is now cleared. + ThresholdCleared(Threshold), + /// Sensor encountered a failure. + Failure(Error), +} + +/// Sensor threshold types. +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Threshold { + /// The temperature threshold below which a warning event is generated. + WarnLow, + /// The temperature threshold above which a warning event is generated. + WarnHigh, + /// The temperature threshold above which a prochot event is generated. + Prochot, + /// The temperature threshold above which a critical event is generated. + Critical, +} + +/// Sensor service interface trait +pub trait SensorService { + /// Returns the most recently sampled temperature measurement in degrees Celsius. + fn temperature(&self) -> impl Future; + /// Returns the average temperature over a sampling period in degrees Celsius. + fn temperature_average(&self) -> impl Future; + /// Immediately samples the sensor for a temperature measurement and returns the result in degrees Celsius. + fn temperature_immediate(&self) -> impl Future>; + /// Sets the temperature for which a sensor event will be generated when the threshold is exceeded, in degrees Celsius. + fn set_threshold(&self, threshold: Threshold, value: DegreesCelsius) -> impl Future; + /// Returns the temperature threshold value for the specified threshold type in degrees Celsius. + fn threshold(&self, threshold: Threshold) -> impl Future; + /// Sets the rate at which temperature measurements are sampled. + fn set_sample_period(&self, period: Duration) -> impl Future; + /// Enable periodic temperature sampling. + fn enable_sampling(&self) -> impl Future; + /// Disable periodic temperature sampling. + fn disable_sampling(&self) -> impl Future; +} + +impl SensorService for &T { + async fn temperature(&self) -> DegreesCelsius { + T::temperature(self).await + } + + async fn temperature_average(&self) -> DegreesCelsius { + T::temperature_average(self).await + } + + async fn temperature_immediate(&self) -> Result { + T::temperature_immediate(self).await + } + + async fn set_threshold(&self, threshold: Threshold, value: DegreesCelsius) { + T::set_threshold(self, threshold, value).await + } + + async fn threshold(&self, threshold: Threshold) -> DegreesCelsius { + T::threshold(self, threshold).await + } + + async fn set_sample_period(&self, period: Duration) { + T::set_sample_period(self, period).await + } + + async fn enable_sampling(&self) { + T::enable_sampling(self).await + } + + async fn disable_sampling(&self) { + T::disable_sampling(self).await + } +} diff --git a/thermal-service-messages/Cargo.toml b/thermal-service-relay/Cargo.toml similarity index 81% rename from thermal-service-messages/Cargo.toml rename to thermal-service-relay/Cargo.toml index 2d9dbc37e..43f7c1dbd 100644 --- a/thermal-service-messages/Cargo.toml +++ b/thermal-service-relay/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "thermal-service-messages" +name = "thermal-service-relay" version.workspace = true edition.workspace = true license.workspace = true @@ -8,6 +8,7 @@ repository.workspace = true [dependencies] defmt = { workspace = true, optional = true } embedded-services.workspace = true +thermal-service-interface.workspace = true num_enum.workspace = true uuid.workspace = true diff --git a/thermal-service-relay/src/lib.rs b/thermal-service-relay/src/lib.rs new file mode 100644 index 000000000..fceae864d --- /dev/null +++ b/thermal-service-relay/src/lib.rs @@ -0,0 +1,223 @@ +#![no_std] + +mod serialization; + +pub use serialization::{ThermalError, ThermalRequest, ThermalResponse, ThermalResult}; +use thermal_service_interface::ThermalService; +use thermal_service_interface::fan::{self, FanService}; +use thermal_service_interface::sensor::{self, SensorService}; + +/// DeciKelvin temperature representation. +/// +/// This exists because the host to EC interface expects DeciKelvin, +/// though internally we still use Celsius for ease of use. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DeciKelvin(pub u32); + +impl DeciKelvin { + /// Convert from degrees Celsius to DeciKelvin. + pub const fn from_celsius(c: f32) -> Self { + Self(((c + 273.15) * 10.0) as u32) + } + + /// Convert from DeciKelvin to degrees Celsius. + pub const fn to_celsius(self) -> f32 { + (self.0 as f32 / 10.0) - 273.15 + } +} + +/// MPTF Standard UUIDs which the thermal service understands. +pub mod uuid_standard { + /// The critical temperature threshold of a sensor. + pub const CRT_TEMP: uuid::Bytes = uuid::uuid!("218246e7-baf6-45f1-aa13-07e4845256b8").to_bytes_le(); + /// The prochot temperature threshold of a sensor. + pub const PROC_HOT_TEMP: uuid::Bytes = uuid::uuid!("22dc52d2-fd0b-47ab-95b8-26552f9831a5").to_bytes_le(); + /// The temperature threshold at which a fan should turn on and begin running at its minimum RPM. + pub const FAN_MIN_TEMP: uuid::Bytes = uuid::uuid!("ba17b567-c368-48d5-bc6f-a312a41583c1").to_bytes_le(); + /// The temperature threshold at which a fan should start ramping up. + pub const FAN_RAMP_TEMP: uuid::Bytes = uuid::uuid!("3a62688c-d95b-4d2d-bacc-90d7a5816bcd").to_bytes_le(); + /// The temperature threshold at which a fan should be at max speed. + pub const FAN_MAX_TEMP: uuid::Bytes = uuid::uuid!("dcb758b1-f0fd-4ec7-b2c0-ef1e2a547b76").to_bytes_le(); + /// The minimum RPM a fan is capable of running at reliably. + pub const FAN_MIN_RPM: uuid::Bytes = uuid::uuid!("db261c77-934b-45e2-9742-256c62badb7a").to_bytes_le(); + /// The maximum RPM a fan is capable of running at reliably. + pub const FAN_MAX_RPM: uuid::Bytes = uuid::uuid!("5cf839df-8be7-42b9-9ac5-3403ca2c8a6a").to_bytes_le(); + /// The current RPM of a fan. + pub const FAN_CURRENT_RPM: uuid::Bytes = uuid::uuid!("adf95492-0776-4ffc-84f3-b6c8b5269683").to_bytes_le(); +} + +/// Thermal service relay handler which wraps a thermal service instance. +pub struct ThermalServiceRelayHandler { + service: T, +} + +impl ThermalServiceRelayHandler { + /// Create a new thermal service relay handler. + pub fn new(service: T) -> Self { + Self { service } + } + + async fn sensor_get_tmp(&self, instance_id: u8) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + let temp = sensor.temperature().await; + Ok(ThermalResponse::ThermalGetTmpResponse { + temperature: DeciKelvin::from_celsius(temp), + }) + } + + async fn sensor_set_warn_thrs( + &self, + instance_id: u8, + _timeout: u32, + low: DeciKelvin, + high: DeciKelvin, + ) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + sensor.set_threshold(sensor::Threshold::WarnLow, low.to_celsius()).await; + sensor + .set_threshold(sensor::Threshold::WarnHigh, high.to_celsius()) + .await; + Ok(ThermalResponse::ThermalSetThrsResponse) + } + + async fn get_var_handler(&self, instance_id: u8, var_uuid: uuid::Bytes) -> ThermalResult { + match var_uuid { + uuid_standard::CRT_TEMP => self.sensor_get_thrs(instance_id, sensor::Threshold::Critical).await, + uuid_standard::PROC_HOT_TEMP => self.sensor_get_thrs(instance_id, sensor::Threshold::Prochot).await, + uuid_standard::FAN_MIN_TEMP => self.fan_get_state_temp(instance_id, fan::OnState::Min).await, + uuid_standard::FAN_RAMP_TEMP => self.fan_get_state_temp(instance_id, fan::OnState::Ramping).await, + uuid_standard::FAN_MAX_TEMP => self.fan_get_state_temp(instance_id, fan::OnState::Max).await, + uuid_standard::FAN_MIN_RPM => self.fan_get_min_rpm(instance_id).await, + uuid_standard::FAN_MAX_RPM => self.fan_get_max_rpm(instance_id).await, + uuid_standard::FAN_CURRENT_RPM => self.fan_get_rpm(instance_id).await, + _ => Err(ThermalError::InvalidParameter), + } + } + + async fn set_var_handler(&self, instance_id: u8, var_uuid: uuid::Bytes, set_var: u32) -> ThermalResult { + match var_uuid { + uuid_standard::CRT_TEMP => { + self.sensor_set_thrs(instance_id, sensor::Threshold::Critical, set_var) + .await + } + uuid_standard::PROC_HOT_TEMP => { + self.sensor_set_thrs(instance_id, sensor::Threshold::Prochot, set_var) + .await + } + uuid_standard::FAN_MIN_TEMP => { + self.fan_set_state_temp(instance_id, fan::OnState::Min, DeciKelvin(set_var)) + .await + } + uuid_standard::FAN_RAMP_TEMP => { + self.fan_set_state_temp(instance_id, fan::OnState::Ramping, DeciKelvin(set_var)) + .await + } + uuid_standard::FAN_MAX_TEMP => { + self.fan_set_state_temp(instance_id, fan::OnState::Max, DeciKelvin(set_var)) + .await + } + uuid_standard::FAN_CURRENT_RPM => { + let rpm = u16::try_from(set_var).map_err(|_| ThermalError::InvalidParameter)?; + self.fan_set_rpm(instance_id, rpm).await + } + _ => Err(ThermalError::InvalidParameter), + } + } + + async fn fan_get_state_temp(&self, instance_id: u8, state: fan::OnState) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + let temp = fan.state_temp(state).await; + Ok(ThermalResponse::ThermalGetVarResponse { + val: DeciKelvin::from_celsius(temp).0, + }) + } + + async fn fan_get_rpm(&self, instance_id: u8) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + let rpm = fan.rpm().await; + Ok(ThermalResponse::ThermalGetVarResponse { val: rpm.into() }) + } + + async fn fan_get_min_rpm(&self, instance_id: u8) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + let rpm = fan.min_rpm().await; + Ok(ThermalResponse::ThermalGetVarResponse { val: rpm.into() }) + } + + async fn fan_get_max_rpm(&self, instance_id: u8) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + let rpm = fan.max_rpm().await; + Ok(ThermalResponse::ThermalGetVarResponse { val: rpm.into() }) + } + + async fn sensor_set_thrs(&self, instance_id: u8, threshold: sensor::Threshold, threshold_dk: u32) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + sensor + .set_threshold(threshold, DeciKelvin(threshold_dk).to_celsius()) + .await; + Ok(ThermalResponse::ThermalSetVarResponse) + } + + async fn sensor_get_thrs(&self, instance_id: u8, threshold: sensor::Threshold) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + let temp = sensor.threshold(threshold).await; + Ok(ThermalResponse::ThermalGetVarResponse { + val: DeciKelvin::from_celsius(temp).0, + }) + } + + async fn sensor_get_warn_thrs(&self, instance_id: u8) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + let low = sensor.threshold(sensor::Threshold::WarnLow).await; + let high = sensor.threshold(sensor::Threshold::WarnHigh).await; + Ok(ThermalResponse::ThermalGetThrsResponse { + timeout: 0, + low: DeciKelvin::from_celsius(low), + high: DeciKelvin::from_celsius(high), + }) + } + + async fn fan_set_state_temp(&self, instance_id: u8, state: fan::OnState, temp: DeciKelvin) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + fan.set_state_temp(state, temp.to_celsius()).await; + Ok(ThermalResponse::ThermalSetVarResponse) + } + + async fn fan_set_rpm(&self, instance_id: u8, rpm: u16) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + fan.set_rpm(rpm).await.map_err(|_| ThermalError::HardwareError)?; + Ok(ThermalResponse::ThermalSetVarResponse) + } +} + +impl embedded_services::relay::mctp::RelayServiceHandlerTypes for ThermalServiceRelayHandler { + type RequestType = ThermalRequest; + type ResultType = ThermalResult; +} + +impl embedded_services::relay::mctp::RelayServiceHandler for ThermalServiceRelayHandler { + async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { + match request { + ThermalRequest::ThermalGetTmpRequest { instance_id } => self.sensor_get_tmp(instance_id).await, + ThermalRequest::ThermalSetThrsRequest { + instance_id, + timeout, + low, + high, + } => self.sensor_set_warn_thrs(instance_id, timeout, low, high).await, + ThermalRequest::ThermalGetThrsRequest { instance_id } => self.sensor_get_warn_thrs(instance_id).await, + // Revisit: Don't currently have a good strategy for handling this request + ThermalRequest::ThermalSetScpRequest { .. } => Err(ThermalError::InvalidParameter), + ThermalRequest::ThermalGetVarRequest { + instance_id, var_uuid, .. + } => self.get_var_handler(instance_id, var_uuid).await, + ThermalRequest::ThermalSetVarRequest { + instance_id, + var_uuid, + set_var, + .. + } => self.set_var_handler(instance_id, var_uuid, set_var).await, + } + } +} diff --git a/thermal-service-messages/src/lib.rs b/thermal-service-relay/src/serialization.rs similarity index 89% rename from thermal-service-messages/src/lib.rs rename to thermal-service-relay/src/serialization.rs index c2b371ab9..6b1a7a63d 100644 --- a/thermal-service-messages/src/lib.rs +++ b/thermal-service-relay/src/serialization.rs @@ -1,20 +1,7 @@ -#![no_std] - +use crate::DeciKelvin; use embedded_services::relay::{MessageSerializationError, SerializableMessage}; -/// 16-bit variable length -pub type VarLen = u16; - -/// Instance ID -pub type InstanceId = u8; - -/// Time in milliseconds -pub type Milliseconds = u32; - -/// MPTF expects temperatures in tenth Kelvins -pub type DeciKelvin = u32; - -/// Standard MPTF requests expected by the thermal subsystem +// Standard MPTF requests expected by the thermal subsystem #[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] #[repr(u16)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -67,7 +54,7 @@ pub enum ThermalRequest { }, ThermalSetThrsRequest { instance_id: u8, - timeout: Milliseconds, + timeout: u32, low: DeciKelvin, high: DeciKelvin, }, @@ -82,18 +69,17 @@ pub enum ThermalRequest { }, ThermalGetVarRequest { instance_id: u8, - len: VarLen, // TODO why is there a len here? as far as I can tell we're always discarding it, and I think values are only u32? + len: u16, var_uuid: uuid::Bytes, }, ThermalSetVarRequest { instance_id: u8, - len: VarLen, // TODO why is there a len here? as far as I can tell we're always discarding it, and I think values are only u32? + len: u16, var_uuid: uuid::Bytes, set_var: u32, }, } -// TODO this is essentially a hand-written reinterpret_cast - can we codegen some of this instead? impl SerializableMessage for ThermalRequest { fn serialize(self, buffer: &mut [u8]) -> Result { match self { @@ -105,8 +91,8 @@ impl SerializableMessage for ThermalRequest { high, } => Ok(safe_put_u8(buffer, 0, instance_id)? + safe_put_dword(buffer, 1, timeout)? - + safe_put_dword(buffer, 5, low)? - + safe_put_dword(buffer, 9, high)?), + + safe_put_dword(buffer, 5, low.0)? + + safe_put_dword(buffer, 9, high.0)?), Self::ThermalGetThrsRequest { instance_id } => safe_put_u8(buffer, 0, instance_id), Self::ThermalSetScpRequest { instance_id, @@ -147,8 +133,8 @@ impl SerializableMessage for ThermalRequest { ThermalCmd::SetThrs => Self::ThermalSetThrsRequest { instance_id: safe_get_u8(buffer, 0)?, timeout: safe_get_dword(buffer, 1)?, - low: safe_get_dword(buffer, 5)?, - high: safe_get_dword(buffer, 9)?, + low: DeciKelvin(safe_get_dword(buffer, 5)?), + high: DeciKelvin(safe_get_dword(buffer, 9)?), }, ThermalCmd::GetThrs => Self::ThermalGetThrsRequest { instance_id: safe_get_u8(buffer, 0)?, @@ -188,7 +174,7 @@ pub enum ThermalResponse { }, ThermalSetThrsResponse, ThermalGetThrsResponse { - timeout: Milliseconds, + timeout: u32, low: DeciKelvin, high: DeciKelvin, }, @@ -202,10 +188,10 @@ pub enum ThermalResponse { impl SerializableMessage for ThermalResponse { fn serialize(self, buffer: &mut [u8]) -> Result { match self { - Self::ThermalGetTmpResponse { temperature } => safe_put_dword(buffer, 0, temperature), + Self::ThermalGetTmpResponse { temperature } => safe_put_dword(buffer, 0, temperature.0), Self::ThermalGetThrsResponse { timeout, low, high } => Ok(safe_put_dword(buffer, 0, timeout)? - + safe_put_dword(buffer, 4, low)? - + safe_put_dword(buffer, 8, high)?), + + safe_put_dword(buffer, 4, low.0)? + + safe_put_dword(buffer, 8, high.0)?), Self::ThermalGetVarResponse { val } => safe_put_dword(buffer, 0, val), Self::ThermalSetVarResponse | Self::ThermalSetScpResponse | Self::ThermalSetThrsResponse => Ok(0), } @@ -217,13 +203,13 @@ impl SerializableMessage for ThermalResponse { .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))? { ThermalCmd::GetTmp => Self::ThermalGetTmpResponse { - temperature: safe_get_dword(buffer, 0)?, + temperature: DeciKelvin(safe_get_dword(buffer, 0)?), }, ThermalCmd::SetThrs => Self::ThermalSetThrsResponse, ThermalCmd::GetThrs => Self::ThermalGetThrsResponse { timeout: safe_get_dword(buffer, 0)?, - low: safe_get_dword(buffer, 4)?, - high: safe_get_dword(buffer, 8)?, + low: DeciKelvin(safe_get_dword(buffer, 4)?), + high: DeciKelvin(safe_get_dword(buffer, 8)?), }, ThermalCmd::SetScp => Self::ThermalSetScpResponse, ThermalCmd::GetVar => Self::ThermalGetVarResponse { diff --git a/thermal-service/Cargo.toml b/thermal-service/Cargo.toml index 1da4f8e66..aaa7f6290 100644 --- a/thermal-service/Cargo.toml +++ b/thermal-service/Cargo.toml @@ -13,16 +13,12 @@ log = { workspace = true, optional = true } embassy-futures.workspace = true embassy-sync.workspace = true embassy-time.workspace = true -embedded-hal-async.workspace = true -embedded-hal.workspace = true embedded-services.workspace = true heapless.workspace = true odp-service-common.workspace = true -thermal-service-messages.workspace = true -uuid.workspace = true +thermal-service-interface.workspace = true embedded-fans-async = "0.2.0" embedded-sensors-hal-async = "0.3.0" -mctp-rs = { workspace = true, features = ["espi"] } [features] default = [] @@ -33,8 +29,7 @@ defmt = [ "embassy-sync/defmt", "embedded-fans-async/defmt", "embedded-sensors-hal-async/defmt", - "mctp-rs/defmt", - "thermal-service-messages/defmt", + "thermal-service-interface/defmt", ] log = [ "dep:log", diff --git a/thermal-service/src/context.rs b/thermal-service/src/context.rs deleted file mode 100644 index fa1cc442d..000000000 --- a/thermal-service/src/context.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Thermal service context -use crate::{Event, fan, sensor}; -use embassy_sync::channel::Channel; -use embedded_services::GlobalRawMutex; - -pub(crate) struct Context<'hw> { - // Registered temperature sensors - sensors: &'hw [&'hw sensor::Device], - // Registered fans - fans: &'hw [&'hw fan::Device], - // Event queue - events: Channel, -} - -impl<'hw> Context<'hw> { - pub(crate) const fn new(sensors: &'hw [&'hw sensor::Device], fans: &'hw [&'hw fan::Device]) -> Self { - Self { - sensors, - fans, - events: Channel::new(), - } - } - - pub(crate) fn sensors(&self) -> &[&sensor::Device] { - self.sensors - } - - pub(crate) fn get_sensor(&self, id: sensor::DeviceId) -> Option<&sensor::Device> { - self.sensors.iter().find(|sensor| sensor.id() == id).copied() - } - - pub(crate) async fn execute_sensor_request( - &self, - id: sensor::DeviceId, - request: sensor::Request, - ) -> sensor::Response { - let sensor = self.get_sensor(id).ok_or(sensor::Error::InvalidRequest)?; - sensor.execute_request(request).await - } - - pub(crate) fn fans(&self) -> &[&fan::Device] { - self.fans - } - - pub(crate) fn get_fan(&self, id: fan::DeviceId) -> Option<&fan::Device> { - self.fans.iter().find(|fan| fan.id() == id).copied() - } - - pub(crate) async fn execute_fan_request(&self, id: fan::DeviceId, request: fan::Request) -> fan::Response { - let fan = self.get_fan(id).ok_or(fan::Error::InvalidRequest)?; - fan.execute_request(request).await - } - - pub(crate) async fn send_event(&self, event: Event) { - self.events.send(event).await - } - - pub(crate) async fn wait_event(&self) -> Event { - self.events.receive().await - } -} diff --git a/thermal-service/src/fan.rs b/thermal-service/src/fan.rs index 7ba11fafe..3e0b24715 100644 --- a/thermal-service/src/fan.rs +++ b/thermal-service/src/fan.rs @@ -1,606 +1,405 @@ -//! Fan Device -use crate::Event; use crate::utils::SampleBuf; +use core::marker::PhantomData; use embassy_sync::mutex::Mutex; use embassy_sync::signal::Signal; -use embassy_time::Timer; -use embedded_fans_async::{self as fan_traits, Error as HardwareError}; +use embassy_time::{Duration, Timer}; +use embedded_fans_async::Error as _; use embedded_sensors_hal_async::temperature::DegreesCelsius; -use embedded_services::GlobalRawMutex; -use embedded_services::ipc::deferred as ipc; -use embedded_services::{error, trace}; - -/// Convenience type for Fan response result -pub type Response = Result; - -/// Allows OEM to implement custom requests -/// -/// The default response is to return an error on unrecognized requests -pub trait CustomRequestHandler { - fn handle_custom_request(&self, _request: Request) -> impl core::future::Future { - async { Err(Error::InvalidRequest) } +use embedded_services::event::Sender; +use embedded_services::{GlobalRawMutex, error, trace}; +use thermal_service_interface::{fan, sensor}; + +/// Fan service configuration parameters. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Config { + /// Rate at which to sample the fan RPM. + pub sample_period: Duration, + /// Rate at which to update the fan state based on temperature readings when auto control is enabled. + pub update_period: Duration, + /// Whether automatic fan control based on temperature is enabled. + pub auto_control: bool, + /// Hysteresis value to prevent rapid toggling between fan states when temperature is around a state transition point. + pub hysteresis: DegreesCelsius, + /// Temperature at which the fan will turn on and begin running at its minimum RPM. + pub min_temp: DegreesCelsius, + /// Temperature at which the fan will follow a speed curve between its minimum and maximum RPM. + pub ramp_temp: DegreesCelsius, + /// Temperature at which the fan will run at its maximum RPM. + pub max_temp: DegreesCelsius, +} + +impl Default for Config { + fn default() -> Self { + Self { + sample_period: Duration::from_secs(1), + update_period: Duration::from_secs(1), + auto_control: true, + hysteresis: 2.0, + min_temp: 25.0, + ramp_temp: 35.0, + max_temp: 45.0, + } } } -/// Allows OEMs to override the default linear response ramp response of fan -pub trait RampResponseHandler: fan_traits::Fan + fan_traits::RpmSense { - fn handle_ramp_response( - &mut self, - profile: &Profile, - temp: DegreesCelsius, - ) -> impl core::future::Future> { - let fan_ramp_temp = profile.ramp_temp; - let fan_max_temp = profile.max_temp; - let min_rpm = self.min_start_rpm(); - let max_rpm = self.max_rpm(); +struct ServiceInner { + driver: Mutex, + state: Mutex, + en_signal: Signal, + config: Mutex, + samples: Mutex>, +} - // Provide a linear fan response between its min and max RPM relative to temperature between ramp start and max temp - let rpm = if temp <= fan_ramp_temp { - min_rpm - } else if temp >= fan_max_temp { - max_rpm - } else { - let ratio = (temp - fan_ramp_temp) / (fan_max_temp - fan_ramp_temp); - let range = (max_rpm - min_rpm) as f32; - min_rpm + (ratio * range) as u16 - }; +impl ServiceInner { + fn new(driver: T, config: Config) -> Self { + Self { + driver: Mutex::new(driver), + state: Mutex::new(fan::State::Off), + en_signal: Signal::new(), + config: Mutex::new(config), + samples: Mutex::new(SampleBuf::create()), + } + } - async move { - self.set_speed_rpm(rpm).await?; - Ok(()) + async fn handle_sampling(&self) { + loop { + match self.driver.lock().await.rpm().await { + Ok(rpm) => self.samples.lock().await.push(rpm), + Err(e) => error!("Fan error sampling fan rpm: {:?}", e.kind()), + } + + let period = self.config.lock().await.sample_period; + Timer::after(period).await; } } -} -/// Ensures all necessary traits are implemented for the controlling driver -pub trait Controller: RampResponseHandler + CustomRequestHandler {} + async fn change_state(&self, to: fan::State) -> Result<(), fan::Error> { + let mut driver = self.driver.lock().await; + match to { + fan::State::Off => { + driver.stop().await.map_err(|_| fan::Error::Hardware)?; + } + fan::State::On(fan::OnState::Min) => { + driver.start().await.map_err(|_| fan::Error::Hardware)?; + } + fan::State::On(fan::OnState::Ramping) => { + // Ramp state will continuously update RPM according to its ramp response function + } + fan::State::On(fan::OnState::Max) => { + let max_rpm = driver.max_rpm(); + let _ = driver.set_speed_rpm(max_rpm).await.map_err(|_| fan::Error::Hardware)?; + } + } + drop(driver); -/// Fan error type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Error { - /// Invalid request - InvalidRequest, - /// Device encountered a hardware failure - Hardware, -} + let mut state = self.state.lock().await; + trace!("Fan transitioned to {:?} state from {:?} state", to, *state); + *state = to; -/// Fan request -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Request { - /// Most recent RPM measurement - GetRpm, - /// Average RPM measurement - GetAvgRpm, - /// Get Min RPM - GetMinRpm, - /// Get Max RPM - GetMaxRpm, - /// Set RPM manually and disable temperature-based control - SetRpm(u16), - /// Set duty cycle manually (in percent) and disable temperature-based control - SetDuty(u8), - /// Stop the fan and disable temperature-based control - Stop, - /// Enable temperature-based control - EnableAutoControl, - /// Set RPM sampling period (in ms) - SetSamplingPeriod(u64), - /// Set speed update period - SetSpeedUpdatePeriod(u64), - /// Get temperature which fan will turn on to minimum RPM (in degrees Celsius) - GetOnTemp, - /// Get temperature which fan will begin ramping (in degrees Celsius) - GetRampTemp, - /// Get temperature which fan will reach its max RPM (in degrees Celsius) - GetMaxTemp, - /// Set temperature which fan will turn on to minimum RPM (in degrees Celsius) - SetOnTemp(DegreesCelsius), - /// Set temperature which fan will begin ramping (in degrees Celsius) - SetRampTemp(DegreesCelsius), - /// Set temperature which fan will reach its max RPM (in degrees Celsius) - SetMaxTemp(DegreesCelsius), - /// Set hysteresis value between fan on and fan off (in degrees Celsius) - SetHysteresis(DegreesCelsius), - /// Get the profile associated with this fan - GetProfile, - /// Set the profile associated with this fan - SetProfile(Profile), - /// Custom-implemented command - Custom(u8, &'static [u8]), + Ok(()) + } } -/// Fan response -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ResponseData { - /// Response for any request that is successful but does not require data - Success, - /// RPM - Rpm(u16), - /// Temperature - Temp(DegreesCelsius), - /// Profile - Profile(Profile), - /// Custom-implemented response - Custom(&'static [u8]), +/// Fan service control handle. +pub struct Service<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender, const SAMPLE_BUF_LEN: usize> { + inner: &'hw ServiceInner, + _phantom: PhantomData<(S, E)>, } -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -enum FanState { - Off, - On, - Ramping, - Max, +// Note: We can't derive these traits because the compiler thinks our generics then need to be Copy + Clone, +// but we only hold a reference and don't actually need to be that strict +impl, const SAMPLE_BUF_LEN: usize> Clone + for Service<'_, T, S, E, SAMPLE_BUF_LEN> +{ + fn clone(&self) -> Self { + *self + } } -/// Fan device ID new type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DeviceId(pub u8); - -/// Fan device struct -pub struct Device { - // Device ID - id: DeviceId, - // Channel for IPC requests and responses - ipc: ipc::Channel, - // Signal for auto-control enable - auto_control_enable: Signal, +impl, const SAMPLE_BUF_LEN: usize> Copy + for Service<'_, T, S, E, SAMPLE_BUF_LEN> +{ } -impl Device { - /// Create a new fan device - pub fn new(id: DeviceId) -> Self { - Self { - id, - ipc: ipc::Channel::new(), - auto_control_enable: Signal::new(), - } +impl<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender, const SAMPLE_BUF_LEN: usize> fan::FanService + for Service<'hw, T, S, E, SAMPLE_BUF_LEN> +{ + async fn enable_auto_control(&self) -> Result<(), fan::Error> { + self.inner.change_state(fan::State::Off).await?; + self.inner.config.lock().await.auto_control = true; + self.inner.en_signal.signal(()); + Ok(()) } - /// Get the device ID - pub fn id(&self) -> DeviceId { - self.id + async fn rpm(&self) -> u16 { + self.inner.samples.lock().await.recent() } - /// Execute request and wait for response - pub async fn execute_request(&self, request: Request) -> Response { - self.ipc.execute(request).await + async fn min_rpm(&self) -> u16 { + self.inner.driver.lock().await.min_rpm() } -} -/// Fan profile -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Profile { - /// Profile ID - pub id: usize, - /// ID of sensor this fan will query for auto control - pub sensor_id: crate::sensor::DeviceId, - /// Period (in ms) fan will sample its RPM - pub sample_period: u64, - /// Period (in ms) fan will update its state during auto control - pub update_period: u64, - /// Whether fan is under automatic temperature-based control or not - pub auto_control: bool, - /// Hysteresis value (in degrees Celsius) preventing fan from rapidly switching between states - pub hysteresis: DegreesCelsius, - /// Temperature (in degrees Celsius) at which fan will turn on - pub on_temp: DegreesCelsius, - /// Temperature (in degrees Celsius) at which fan will begin its ramp response - pub ramp_temp: DegreesCelsius, - /// Temperature (in degrees Celsius) at which fan will run at its max speed - pub max_temp: DegreesCelsius, -} - -impl Default for Profile { - fn default() -> Self { - Self { - id: 0, - sensor_id: crate::sensor::DeviceId(0), - sample_period: 1000, - update_period: 1000, - auto_control: true, - hysteresis: 2.0, - on_temp: 39.0, - ramp_temp: 40.0, - max_temp: 44.0, - } + async fn max_rpm(&self) -> u16 { + self.inner.driver.lock().await.max_rpm() } -} -/// Fan struct containing device for comms and driver -pub struct Fan { - // Underlying device - device: Device, - // Underlying controller - controller: Mutex, - // Fan profile - profile: Mutex, - // RPM samples - samples: Mutex>, - // State - state: Mutex, -} + async fn rpm_average(&self) -> u16 { + self.inner.samples.lock().await.average() + } -impl Fan { - /// New fan - /// - /// Sample buffer length MUST be a power of two - pub fn new(id: DeviceId, controller: T, profile: Profile) -> Self { - Self { - device: Device::new(id), - controller: Mutex::new(controller), - profile: Mutex::new(profile), - samples: Mutex::new(SampleBuf::create()), - state: Mutex::new(FanState::Off), - } + async fn rpm_immediate(&self) -> Result { + self.inner + .driver + .lock() + .await + .rpm() + .await + .map_err(|_| fan::Error::Hardware) } - /// Retrieve a reference to underlying device for registration with services - pub fn device(&self) -> &Device { - &self.device + async fn set_rpm(&self, rpm: u16) -> Result<(), fan::Error> { + self.inner + .driver + .lock() + .await + .set_speed_rpm(rpm) + .await + .map_err(|_| fan::Error::Hardware)?; + self.inner.config.lock().await.auto_control = false; + Ok(()) } - /// Retrieve a Mutex wrapping the underlying controller - /// - /// Should only be used to update OEM specific state - pub fn controller(&self) -> &Mutex { - &self.controller + async fn set_duty_percent(&self, duty: u8) -> Result<(), fan::Error> { + self.inner + .driver + .lock() + .await + .set_speed_percent(duty) + .await + .map_err(|_| fan::Error::Hardware)?; + self.inner.config.lock().await.auto_control = false; + Ok(()) } - /// Wait for fan to receive a request - pub async fn wait_request(&self) -> ipc::Request<'_, GlobalRawMutex, Request, Response> { - self.device.ipc.receive().await + async fn stop(&self) -> Result<(), fan::Error> { + self.inner + .driver + .lock() + .await + .stop() + .await + .map_err(|_| fan::Error::Hardware)?; + self.inner.config.lock().await.auto_control = false; + Ok(()) } - /// Process fan request - pub async fn process_request(&self, request: Request) -> Response { - match request { - Request::GetRpm => { - let rpm = self.samples.lock().await.recent(); - Ok(ResponseData::Rpm(rpm)) - } - Request::GetAvgRpm => { - let rpm = self.samples.lock().await.average(); - Ok(ResponseData::Rpm(rpm)) - } - Request::SetRpm(rpm) => { - self.controller - .lock() - .await - .set_speed_rpm(rpm) - .await - .map_err(|_| Error::Hardware)?; - self.profile.lock().await.auto_control = false; - Ok(ResponseData::Success) - } - Request::SetDuty(percent) => { - self.controller - .lock() - .await - .set_speed_percent(percent) - .await - .map_err(|_| Error::Hardware)?; - self.profile.lock().await.auto_control = false; - Ok(ResponseData::Success) - } - Request::Stop => { - self.change_state(FanState::Off).await?; - self.profile.lock().await.auto_control = false; - Ok(ResponseData::Success) - } - Request::GetMinRpm => { - let min_rpm = self.controller.lock().await.min_rpm(); - Ok(ResponseData::Rpm(min_rpm)) - } - Request::GetMaxRpm => { - let max_rpm = self.controller.lock().await.max_rpm(); - Ok(ResponseData::Rpm(max_rpm)) - } - Request::SetSamplingPeriod(period) => { - self.profile.lock().await.sample_period = period; - Ok(ResponseData::Success) - } - Request::EnableAutoControl => { - // Make sure we actually transition to a known state - // Next iteration of handle auto control would then put it in actual correct state - self.change_state(FanState::Off).await?; - self.profile.lock().await.auto_control = true; - self.device.auto_control_enable.signal(()); - Ok(ResponseData::Success) - } - Request::SetSpeedUpdatePeriod(period) => { - self.profile.lock().await.update_period = period; - Ok(ResponseData::Success) - } - Request::GetOnTemp => { - let temp = self.profile.lock().await.on_temp; - Ok(ResponseData::Temp(temp)) - } - Request::GetRampTemp => { - let temp = self.profile.lock().await.ramp_temp; - Ok(ResponseData::Temp(temp)) - } - Request::GetMaxTemp => { - let temp = self.profile.lock().await.max_temp; - Ok(ResponseData::Temp(temp)) - } - Request::SetOnTemp(temp) => { - self.profile.lock().await.on_temp = temp; - Ok(ResponseData::Success) - } - Request::SetRampTemp(temp) => { - self.profile.lock().await.ramp_temp = temp; - Ok(ResponseData::Success) - } - Request::SetMaxTemp(temp) => { - self.profile.lock().await.max_temp = temp; - Ok(ResponseData::Success) - } - Request::SetHysteresis(temp) => { - self.profile.lock().await.hysteresis = temp; - Ok(ResponseData::Success) - } - Request::GetProfile => { - let profile = *self.profile.lock().await; - Ok(ResponseData::Profile(profile)) - } - Request::SetProfile(profile) => { - *self.profile.lock().await = profile; - Ok(ResponseData::Success) - } - Request::Custom(_, _) => self.controller.lock().await.handle_custom_request(request).await, - } + async fn set_rpm_sampling_period(&self, period: Duration) { + self.inner.config.lock().await.sample_period = period; } - /// Wait for fan to receive a request, process it, and send a response - pub async fn wait_and_process(&self) { - let request = self.wait_request().await; - let response = self.process_request(request.command).await; - request.respond(response); + async fn set_rpm_update_period(&self, period: Duration) { + self.inner.config.lock().await.update_period = period; } - /// Waits for a IPC request, then processes it - pub async fn handle_rx(&self) { - loop { - self.wait_and_process().await; + async fn state_temp(&self, on_state: fan::OnState) -> DegreesCelsius { + let config = self.inner.config.lock().await; + match on_state { + fan::OnState::Min => config.min_temp, + fan::OnState::Ramping => config.ramp_temp, + fan::OnState::Max => config.max_temp, } } - /// Periodically samples RPM from physical fan and caches it - pub async fn handle_sampling(&self) { - loop { - match self.controller.lock().await.rpm().await { - Ok(rpm) => self.samples.lock().await.push(rpm), - Err(e) => error!("Fan {} error sampling fan rpm: {:?}", self.device.id.0, e.kind()), - } - - let period = self.profile.lock().await.sample_period; - Timer::after_millis(period).await; + async fn set_state_temp(&self, on_state: fan::OnState, temp: DegreesCelsius) { + let mut config = self.inner.config.lock().await; + match on_state { + fan::OnState::Min => config.min_temp = temp, + fan::OnState::Ramping => config.ramp_temp = temp, + fan::OnState::Max => config.max_temp = temp, } } +} - pub async fn handle_auto_control<'hw>(&self, thermal_service: &crate::Service<'hw>) { - loop { - if self.profile.lock().await.auto_control { - let temp = match thermal_service - .execute_sensor_request(self.profile.lock().await.sensor_id, crate::sensor::Request::GetTemp) - .await - { - Ok(crate::sensor::ResponseData::Temp(temp)) => temp, - _ => { - error!( - "Fan {} failed to get temperature, disabling auto control and setting speed to max", - self.device.id.0 - ); - - self.profile.lock().await.auto_control = false; - if self.controller.lock().await.set_speed_max().await.is_err() { - error!("Fan {} failed to set speed to max!", self.device.id.0); - } - - thermal_service - .send_event(Event::FanFailure(self.device.id, Error::Hardware)) - .await; - continue; - } - }; +/// Parameters required to initialize a fan service. +pub struct InitParams<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender> { + /// The underlying fan driver this service will control. + pub driver: T, + /// Initial configuration for the fan service. + pub config: Config, + /// The sensor service this fan will use to get temperature readings. + pub sensor_service: S, + /// Event senders for fan events. + pub event_senders: &'hw mut [E], +} - if let Err(e) = self.handle_fan_state(temp).await { - thermal_service.send_event(Event::FanFailure(self.device.id, e)).await; - error!("Fan {} error handling fan state transition: {:?}", self.device.id.0, e); - } +/// The memory resources required by the fan. +pub struct Resources { + inner: Option>, +} - let sleep_duration = self.profile.lock().await.update_period; - Timer::after_millis(sleep_duration).await; +// Note: We can't derive Default unless we trait bound T by Default, +// but we don't want that restriction since the default is just the None case +impl Default for Resources { + fn default() -> Self { + Self { inner: None } + } +} - // Sleep until auto control is re-enabled - } else { - self.device.auto_control_enable.wait().await; - } +/// A task runner for a fan. Users must run this in an embassy task or similar async execution context. +pub struct Runner<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender, const SAMPLE_BUF_LEN: usize> { + service: &'hw ServiceInner, + sensor: S, + event_senders: &'hw mut [E], +} + +impl<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender, const SAMPLE_BUF_LEN: usize> + Runner<'hw, T, S, E, SAMPLE_BUF_LEN> +{ + async fn broadcast_event(&mut self, event: fan::Event) { + for sender in self.event_senders.iter_mut() { + sender.send(event).await; } } - async fn handle_fan_off_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - let profile = self.profile.lock().await; + async fn ramp_response(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if temp >= profile.on_temp { - self.change_state(FanState::On).await?; - } + let mut driver = self.service.driver.lock().await; + let min_rpm = driver.min_start_rpm(); + let max_rpm = driver.max_rpm(); - Ok(()) + // Provide a linear fan response between its min and max RPM relative to temperature between ramp start and max temp + let rpm = if temp <= config.ramp_temp { + min_rpm + } else if temp >= config.max_temp { + max_rpm + } else { + let ratio = (temp - config.ramp_temp) / (config.max_temp - config.ramp_temp); + let range = (max_rpm - min_rpm) as f32; + min_rpm + (ratio * range) as u16 + }; + + driver + .set_speed_rpm(rpm) + .await + .map(|_| ()) + .map_err(|_| fan::Error::Hardware) } - async fn handle_fan_on_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - let profile = self.profile.lock().await; + async fn handle_fan_off_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if temp < (profile.on_temp - profile.hysteresis) { - self.change_state(FanState::Off).await?; - } else if temp >= profile.ramp_temp { - self.change_state(FanState::Ramping).await?; + if temp >= config.min_temp { + self.service.change_state(fan::State::On(fan::OnState::Min)).await?; } Ok(()) } - async fn handle_fan_ramping_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - let profile = self.profile.lock().await; + async fn handle_fan_on_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if temp < (profile.ramp_temp - profile.hysteresis) { - self.change_state(FanState::On).await?; - } else if temp >= profile.max_temp { - self.change_state(FanState::Max).await?; - } else { - self.controller - .lock() - .await - .handle_ramp_response(&profile, temp) - .await - .map_err(|_| Error::Hardware)?; + if temp < (config.min_temp - config.hysteresis) { + self.service.change_state(fan::State::Off).await?; + } else if temp >= config.ramp_temp { + self.service.change_state(fan::State::On(fan::OnState::Ramping)).await?; } Ok(()) } - async fn handle_fan_max_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - let profile = self.profile.lock().await; + async fn handle_fan_ramping_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if temp < (profile.max_temp - profile.hysteresis) { - self.change_state(FanState::Ramping).await?; + if temp < (config.ramp_temp - config.hysteresis) { + self.service.change_state(fan::State::On(fan::OnState::Min)).await?; + } else if temp >= config.max_temp { + self.service.change_state(fan::State::On(fan::OnState::Max)).await?; + } else { + self.ramp_response(temp).await?; } Ok(()) } - async fn change_state(&self, to: FanState) -> Result<(), Error> { - let mut controller = self.controller.lock().await; - match to { - FanState::Off => { - controller.stop().await.map_err(|_| Error::Hardware)?; - } - FanState::On => { - controller.start().await.map_err(|_| Error::Hardware)?; - } - FanState::Ramping => { - // Ramp state will continuously update RPM according to its ramp response function - } - FanState::Max => { - let max_rpm = controller.max_rpm(); - let _ = controller.set_speed_rpm(max_rpm).await.map_err(|_| Error::Hardware)?; - } - } - drop(controller); + async fn handle_fan_max_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - let state = *self.state.lock().await; - trace!( - "Fan {} transitioned to {:?} state from {:?} state", - self.device.id.0, to, state - ); - *self.state.lock().await = to; + if temp < (config.max_temp - config.hysteresis) { + self.service.change_state(fan::State::On(fan::OnState::Ramping)).await?; + } Ok(()) } - async fn handle_fan_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - // Must copy state here, if attempt to dereference in match, mutex is still held in match arms - let state = *self.state.lock().await; + async fn handle_fan_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let state = *self.service.state.lock().await; match state { - FanState::Off => self.handle_fan_off_state(temp).await, - FanState::On => self.handle_fan_on_state(temp).await, - FanState::Ramping => self.handle_fan_ramping_state(temp).await, - FanState::Max => self.handle_fan_max_state(temp).await, + fan::State::Off => self.handle_fan_off_state(temp).await, + fan::State::On(fan::OnState::Min) => self.handle_fan_on_state(temp).await, + fan::State::On(fan::OnState::Ramping) => self.handle_fan_ramping_state(temp).await, + fan::State::On(fan::OnState::Max) => self.handle_fan_max_state(temp).await, } } -} - -/// The memory resources required by the fan. -pub struct Resources<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - inner: Option>, -} -// Note: We can't derive Default unless we trait bound T by Default, -// but we don't want that restriction since the default is just the None case -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> Default for Resources<'hw, T, SAMPLE_BUF_LEN> { - fn default() -> Self { - Self { inner: None } - } -} + async fn handle_auto_control(&mut self) { + loop { + if self.service.config.lock().await.auto_control { + let temp = self.sensor.temperature().await; + if let Err(e) = self.handle_fan_state(temp).await { + error!("Error handling fan state transition, disabling auto control: {:?}", e); + self.service.config.lock().await.auto_control = false; + self.broadcast_event(fan::Event::Failure(e)).await; + } -struct ServiceInner<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - fan: &'hw Fan, - thermal_service: &'hw crate::Service<'hw>, -} + let sleep_duration = self.service.config.lock().await.update_period; + Timer::after(sleep_duration).await; -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> ServiceInner<'hw, T, SAMPLE_BUF_LEN> { - fn new(init_params: InitParams<'hw, T, SAMPLE_BUF_LEN>) -> Self { - Self { - fan: init_params.fan, - thermal_service: init_params.thermal_service, + // Sleep until auto control is re-enabled + } else { + self.service.en_signal.wait().await; + } } } - - fn fan(&self) -> &Fan { - self.fan - } } -/// A task runner for a fan. Users must run this in an embassy task or similar async execution context. -pub struct Runner<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - service: &'hw ServiceInner<'hw, T, SAMPLE_BUF_LEN>, -} - -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> odp_service_common::runnable_service::ServiceRunner<'hw> - for Runner<'hw, T, SAMPLE_BUF_LEN> +impl<'hw, T: fan::Driver, S: sensor::SensorService + 'hw, E: Sender + 'hw, const SAMPLE_BUF_LEN: usize> + odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw, T, S, E, SAMPLE_BUF_LEN> { - async fn run(self) -> embedded_services::Never { + async fn run(mut self) -> embedded_services::Never { + let service = self.service; loop { - let _ = embassy_futures::join::join3( - self.service.fan.handle_rx(), - self.service.fan.handle_sampling(), - self.service.fan.handle_auto_control(self.service.thermal_service), - ) - .await; + let _ = embassy_futures::join::join(service.handle_sampling(), self.handle_auto_control()).await; } } } -/// Fan service control handle. -pub struct Service<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - inner: &'hw ServiceInner<'hw, T, SAMPLE_BUF_LEN>, -} - -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> Service<'hw, T, SAMPLE_BUF_LEN> { - /// Get a reference to the inner fan. - pub fn fan(&self) -> &Fan { - self.inner.fan() - } -} - -/// Parameters required to initialize a fan service. -pub struct InitParams<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - /// The underlying `Fan` wrapper this service will control. - pub fan: &'hw Fan, - /// The thermal service handle for this fan to communicate with a sensor. - pub thermal_service: &'hw crate::Service<'hw>, -} - -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> odp_service_common::runnable_service::Service<'hw> - for Service<'hw, T, SAMPLE_BUF_LEN> +impl<'hw, T: fan::Driver, S: sensor::SensorService + 'hw, E: Sender + 'hw, const SAMPLE_BUF_LEN: usize> + odp_service_common::runnable_service::Service<'hw> for Service<'hw, T, S, E, SAMPLE_BUF_LEN> { - type Runner = Runner<'hw, T, SAMPLE_BUF_LEN>; - type Resources = Resources<'hw, T, SAMPLE_BUF_LEN>; - type ErrorType = Error; - type InitParams = InitParams<'hw, T, SAMPLE_BUF_LEN>; + type Runner = Runner<'hw, T, S, E, SAMPLE_BUF_LEN>; + type Resources = Resources; + type ErrorType = fan::Error; + type InitParams = InitParams<'hw, T, S, E>; async fn new( service_storage: &'hw mut Self::Resources, init_params: Self::InitParams, ) -> Result<(Self, Self::Runner), Self::ErrorType> { - let service = service_storage.inner.insert(ServiceInner::new(init_params)); - Ok((Self { inner: service }, Runner { service })) + let service = service_storage + .inner + .insert(ServiceInner::new(init_params.driver, init_params.config)); + Ok(( + Self { + inner: service, + _phantom: PhantomData, + }, + Runner { + service, + sensor: init_params.sensor_service, + event_senders: init_params.event_senders, + }, + )) } } diff --git a/thermal-service/src/lib.rs b/thermal-service/src/lib.rs index 55d816f99..dbcc178a4 100644 --- a/thermal-service/src/lib.rs +++ b/thermal-service/src/lib.rs @@ -1,101 +1,74 @@ //! Thermal service #![no_std] -#![allow(clippy::todo)] -#![allow(clippy::unwrap_used)] -use embedded_sensors_hal_async::temperature::DegreesCelsius; -use thermal_service_messages::{ThermalRequest, ThermalResult}; +use thermal_service_interface::{fan::FanService, sensor::SensorService}; -mod context; pub mod fan; #[cfg(feature = "mock")] pub mod mock; -pub mod mptf; pub mod sensor; -pub mod utils; +mod utils; -/// Thermal error -#[derive(Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Error; - -/// Thermal event -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Event { - /// Sensor sampled temperature exceeding a threshold - ThresholdExceeded(sensor::DeviceId, sensor::ThresholdType, DegreesCelsius), - /// Sensor is no longer exceeding a threshold - ThresholdCleared(sensor::DeviceId, sensor::ThresholdType), - /// Sensor encountered hardware failure - SensorFailure(sensor::DeviceId, sensor::Error), - /// Fan encountered hardware failure - FanFailure(fan::DeviceId, fan::Error), +struct ServiceInner<'hw, S: SensorService, F: FanService> { + sensors: &'hw [S], + fans: &'hw [F], } -pub struct Service<'hw> { - context: context::Context<'hw>, +/// Thermal service handle. +/// +/// This maintains a list of registered temperature sensors and fans, which can be accessed by instance ID. +/// +/// To allow for a collection of sensors and fans of different underlying driver types, +/// type erasure will need to be handled by the user, likely via enum dispatch, +/// since async traits are not currently dyn compatible. +#[derive(Clone, Copy)] +pub struct Service<'hw, S: SensorService, F: FanService> { + inner: &'hw ServiceInner<'hw, S, F>, } -impl<'hw> Service<'hw> { - pub async fn init( - service_storage: &'hw embassy_sync::once_lock::OnceLock>, - sensors: &'hw [&'hw sensor::Device], - fans: &'hw [&'hw fan::Device], - ) -> &'hw Self { - service_storage.get_or_init(|| Self { - context: context::Context::new(sensors, fans), - }) - } - - /// Send a thermal event - pub async fn send_event(&self, event: Event) { - self.context.send_event(event).await - } - - /// Wait for a thermal event - pub async fn wait_event(&self) -> Event { - self.context.wait_event().await - } - - /// Provides access to the sensors list - pub fn sensors(&self) -> &[&sensor::Device] { - self.context.sensors() - } +/// Parameters required to initialize the thermal service. +pub struct InitParams<'hw, S: SensorService, F: FanService> { + /// Registered temperature sensors. + pub sensors: &'hw [S], + /// Registered fans. + pub fans: &'hw [F], +} - /// Find a sensor by its ID - pub fn get_sensor(&self, id: sensor::DeviceId) -> Option<&sensor::Device> { - self.context.get_sensor(id) - } +/// The memory resources required by the thermal service. +pub struct Resources<'hw, S: SensorService, F: FanService> { + inner: Option>, +} - /// Send a request to a sensor through the thermal service instead of directly. - pub async fn execute_sensor_request(&self, id: sensor::DeviceId, request: sensor::Request) -> sensor::Response { - self.context.execute_sensor_request(id, request).await +// Note: We can't derive Default because the compiler requires S: Default + F: Default bounds, +// but we don't need that since the default is just the None case +impl Default for Resources<'_, S, F> { + fn default() -> Self { + Self { inner: None } } +} - /// Provides access to the fans list - pub fn fans(&self) -> &[&fan::Device] { - self.context.fans() +impl<'hw, S: SensorService, F: FanService> Service<'hw, S, F> { + /// Initializes the thermal service with the provided sensors and fans. + pub fn init(resources: &'hw mut Resources<'hw, S, F>, init_params: InitParams<'hw, S, F>) -> Self { + let inner = resources.inner.insert(ServiceInner { + sensors: init_params.sensors, + fans: init_params.fans, + }); + Self { inner } } +} - /// Find a fan by its ID - pub fn get_fan(&self, id: fan::DeviceId) -> Option<&fan::Device> { - self.context.get_fan(id) - } +impl<'hw, S: SensorService + Copy, F: FanService + Copy> thermal_service_interface::ThermalService + for Service<'hw, S, F> +{ + type Sensor = S; + type Fan = F; - /// Send a request to a fan through the thermal service instead of directly. - pub async fn execute_fan_request(&self, id: fan::DeviceId, request: fan::Request) -> fan::Response { - self.context.execute_fan_request(id, request).await + fn sensor(&self, id: u8) -> Option { + self.inner.sensors.get(id as usize).copied() } -} - -impl<'hw> embedded_services::relay::mctp::RelayServiceHandlerTypes for Service<'hw> { - type RequestType = ThermalRequest; - type ResultType = ThermalResult; -} -impl<'hw> embedded_services::relay::mctp::RelayServiceHandler for Service<'hw> { - async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { - mptf::process_request(&request, self).await + fn fan(&self, id: u8) -> Option { + self.inner.fans.get(id as usize).copied() } } diff --git a/thermal-service/src/mock/fan.rs b/thermal-service/src/mock/fan.rs index c3fd5401e..c86256dd9 100644 --- a/thermal-service/src/mock/fan.rs +++ b/thermal-service/src/mock/fan.rs @@ -1,5 +1,6 @@ -use crate::fan; +use crate::fan::Config; use embedded_fans_async::{Error, ErrorKind, ErrorType, Fan, RpmSense}; +use thermal_service_interface::fan as fan_interface; /// `MockFan` error. #[derive(Clone, Copy, Debug)] @@ -21,6 +22,16 @@ impl MockFan { pub fn new() -> Self { Self::default() } + + /// Returns a suitable `Config` for a mock fan service. + pub fn config() -> Config { + Config { + min_temp: super::MIN_TEMP + super::TEMP_RANGE / 4.0, + ramp_temp: super::MIN_TEMP + super::TEMP_RANGE / 2.0, + max_temp: super::MAX_TEMP - super::TEMP_RANGE / 4.0, + ..Default::default() + } + } } impl ErrorType for MockFan { @@ -54,6 +65,4 @@ impl RpmSense for MockFan { } } -impl fan::CustomRequestHandler for MockFan {} -impl fan::RampResponseHandler for MockFan {} -impl fan::Controller for MockFan {} +impl fan_interface::Driver for MockFan {} diff --git a/thermal-service/src/mock/mod.rs b/thermal-service/src/mock/mod.rs index df78b3412..f93912d19 100644 --- a/thermal-service/src/mock/mod.rs +++ b/thermal-service/src/mock/mod.rs @@ -1,57 +1,7 @@ pub mod fan; pub mod sensor; -const SAMPLE_BUF_LEN: usize = 16; - // Represents the temperature ranges the mock thermal service will move through pub(crate) const MIN_TEMP: f32 = 20.0; pub(crate) const MAX_TEMP: f32 = 40.0; pub(crate) const TEMP_RANGE: f32 = MAX_TEMP - MIN_TEMP; - -/// Default mock sensor ID. -pub const MOCK_SENSOR_ID: crate::sensor::DeviceId = crate::sensor::DeviceId(0); - -/// Default mock fan ID. -pub const MOCK_FAN_ID: crate::fan::DeviceId = crate::fan::DeviceId(0); - -/// A thermal-service wrapped [`sensor::MockSensor`]. -pub type TsMockSensor = crate::sensor::Sensor; - -/// A thermal-service wrapped [`fan::MockFan`]. -pub type TsMockFan = crate::fan::Fan; - -/// Creates a new mock sensor ready for use with the thermal service. -/// -/// This is a convenience wrapper, but for finer control a [`sensor::MockSensor`] can still be -/// constructed manually. -/// -/// This still needs to be wrapped in a static and registered with the thermal service, -/// and then a respective task spawned. -pub fn new_sensor() -> TsMockSensor { - let sensor = sensor::MockSensor::new(); - crate::sensor::Sensor::new(MOCK_SENSOR_ID, sensor, crate::sensor::Profile::default()) -} - -/// Creates a new mock fan ready for use with the thermal service. -/// -/// This is a convenience wrapper, but for finer control a [`fan::MockFan`] can still be -/// constructed manually. -/// -/// This still needs to be wrapped in a static and registered with the thermal service, -/// and then a respective task spawned. -pub fn new_fan() -> TsMockFan { - let fan = fan::MockFan::new(); - - // Attaches the mock sensor to the mock fan and set the fan state temps - // so that they are in range with the mock sensor - let profile = crate::fan::Profile { - sensor_id: MOCK_SENSOR_ID, - auto_control: true, - on_temp: MIN_TEMP + TEMP_RANGE / 4.0, - ramp_temp: MIN_TEMP + TEMP_RANGE / 2.0, - max_temp: MAX_TEMP - TEMP_RANGE / 4.0, - ..Default::default() - }; - - crate::fan::Fan::new(MOCK_FAN_ID, fan, profile) -} diff --git a/thermal-service/src/mock/sensor.rs b/thermal-service/src/mock/sensor.rs index 1dd2028b3..6c587cfb1 100644 --- a/thermal-service/src/mock/sensor.rs +++ b/thermal-service/src/mock/sensor.rs @@ -1,6 +1,7 @@ -use crate::sensor; +use crate::sensor::Config; use embedded_sensors_hal_async::sensor as sensor_traits; use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor, TemperatureThresholdSet}; +use thermal_service_interface::sensor; /// `MockSensor` error. #[derive(Clone, Copy, Debug)] @@ -30,6 +31,16 @@ impl MockSensor { falling: false, } } + + /// Returns a suitable `Config` for a mock sensor service. + pub fn config() -> Config { + Config { + warn_high_threshold: super::MIN_TEMP + super::TEMP_RANGE / 4.0, + prochot_threshold: super::MIN_TEMP + super::TEMP_RANGE / 2.0, + critical_threshold: super::MAX_TEMP - super::TEMP_RANGE / 4.0, + ..Default::default() + } + } } impl TemperatureSensor for MockSensor { @@ -66,5 +77,4 @@ impl TemperatureThresholdSet for MockSensor { } } -impl sensor::CustomRequestHandler for MockSensor {} -impl sensor::Controller for MockSensor {} +impl sensor::Driver for MockSensor {} diff --git a/thermal-service/src/mptf.rs b/thermal-service/src/mptf.rs deleted file mode 100644 index 25e62c49f..000000000 --- a/thermal-service/src/mptf.rs +++ /dev/null @@ -1,311 +0,0 @@ -//! Definitions for standard MPTF messages the generic Thermal service can expect -//! -//! Transport services such as eSPI and SSH would need to ensure messages are sent to the Thermal service in this format. -//! -//! This interface is subject to change as the eSPI OOB service is developed -use crate::{self as ts, fan, sensor, utils}; -use thermal_service_messages::{DeciKelvin, Milliseconds}; - -use embedded_services::error; - -/// MPTF Standard UUIDs which the thermal service understands -pub mod uuid_standard { - pub const CRT_TEMP: uuid::Bytes = uuid::uuid!("218246e7-baf6-45f1-aa13-07e4845256b8").to_bytes_le(); - pub const PROC_HOT_TEMP: uuid::Bytes = uuid::uuid!("22dc52d2-fd0b-47ab-95b8-26552f9831a5").to_bytes_le(); - pub const PROFILE_TYPE: uuid::Bytes = uuid::uuid!("23b4a025-cdfd-4af9-a411-37a24c574615").to_bytes_le(); - pub const FAN_ON_TEMP: uuid::Bytes = uuid::uuid!("ba17b567-c368-48d5-bc6f-a312a41583c1").to_bytes_le(); - pub const FAN_RAMP_TEMP: uuid::Bytes = uuid::uuid!("3a62688c-d95b-4d2d-bacc-90d7a5816bcd").to_bytes_le(); - pub const FAN_MAX_TEMP: uuid::Bytes = uuid::uuid!("dcb758b1-f0fd-4ec7-b2c0-ef1e2a547b76").to_bytes_le(); - pub const FAN_MIN_RPM: uuid::Bytes = uuid::uuid!("db261c77-934b-45e2-9742-256c62badb7a").to_bytes_le(); - pub const FAN_MAX_RPM: uuid::Bytes = uuid::uuid!("5cf839df-8be7-42b9-9ac5-3403ca2c8a6a").to_bytes_le(); - pub const FAN_CURRENT_RPM: uuid::Bytes = uuid::uuid!("adf95492-0776-4ffc-84f3-b6c8b5269683").to_bytes_le(); -} - -/// Notifications to Host -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Notify { - /// Warn threshold was exceeded - Warn, - /// Prochot threshold was exceeded - ProcHot, - /// Critical threshold was exceeded - Critical, -} - -async fn sensor_get_tmp<'hw>( - instance_id: u8, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - if let Ok(ts::sensor::ResponseData::Temp(temp)) = thermal_service - .execute_sensor_request(sensor::DeviceId(instance_id), sensor::Request::GetTemp) - .await - { - Ok(thermal_service_messages::ThermalResponse::ThermalGetTmpResponse { - temperature: utils::c_to_dk(temp), - }) - } else { - Err(thermal_service_messages::ThermalError::InvalidParameter) - } -} - -async fn get_var_handler<'hw>( - instance_id: u8, - var_uuid: uuid::Bytes, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - match var_uuid { - uuid_standard::CRT_TEMP => sensor_get_thrs(instance_id, sensor::ThresholdType::Critical, thermal_service).await, - uuid_standard::PROC_HOT_TEMP => { - sensor_get_thrs(instance_id, sensor::ThresholdType::Prochot, thermal_service).await - } - // TODO: Add a SetProfileId request type? But for sensor or fan? - uuid_standard::PROFILE_TYPE => { - todo!() - } - uuid_standard::FAN_ON_TEMP => fan_get_temp(instance_id, fan::Request::GetOnTemp, thermal_service).await, - - uuid_standard::FAN_RAMP_TEMP => fan_get_temp(instance_id, fan::Request::GetRampTemp, thermal_service).await, - uuid_standard::FAN_MAX_TEMP => fan_get_temp(instance_id, fan::Request::GetMaxTemp, thermal_service).await, - uuid_standard::FAN_MIN_RPM => fan_get_rpm(instance_id, fan::Request::GetMinRpm, thermal_service).await, - uuid_standard::FAN_MAX_RPM => fan_get_rpm(instance_id, fan::Request::GetMaxRpm, thermal_service).await, - uuid_standard::FAN_CURRENT_RPM => fan_get_rpm(instance_id, fan::Request::GetRpm, thermal_service).await, - // TODO: Allow OEM to handle these? - uuid => { - error!("Received GetVar for unrecognized UUID: {:?}", uuid); - Err(thermal_service_messages::ThermalError::InvalidParameter) - } - } -} - -async fn set_var_handler<'hw>( - instance_id: u8, - var_uuid: uuid::Bytes, - set_var: u32, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - match var_uuid { - uuid_standard::CRT_TEMP => { - sensor_set_thrs(instance_id, sensor::ThresholdType::Critical, set_var, thermal_service).await - } - uuid_standard::PROC_HOT_TEMP => { - sensor_set_thrs(instance_id, sensor::ThresholdType::Prochot, set_var, thermal_service).await - } - // TODO: Add a SetProfileId request type? But for sensor or fan? - uuid_standard::PROFILE_TYPE => { - todo!() - } - uuid_standard::FAN_ON_TEMP => { - fan_set_var( - instance_id, - fan::Request::SetOnTemp(utils::dk_to_c(set_var)), - thermal_service, - ) - .await - } - uuid_standard::FAN_RAMP_TEMP => { - fan_set_var( - instance_id, - fan::Request::SetRampTemp(utils::dk_to_c(set_var)), - thermal_service, - ) - .await - } - uuid_standard::FAN_MAX_TEMP => { - fan_set_var( - instance_id, - fan::Request::SetMaxTemp(utils::dk_to_c(set_var)), - thermal_service, - ) - .await - } - // TODO: What does it mean to set the min/max RPM? Aren't these hardware defined? - uuid_standard::FAN_MIN_RPM => { - todo!() - } - // TODO: What does it mean to set the min/max RPM? Aren't these hardware defined? - uuid_standard::FAN_MAX_RPM => { - todo!() - } - uuid_standard::FAN_CURRENT_RPM => { - fan_set_var(instance_id, fan::Request::SetRpm(set_var as u16), thermal_service).await - } - // TODO: Allow OEM to handle these? - uuid => { - error!("Received SetVar for unrecognized UUID: {:?}", uuid); - Err(thermal_service_messages::ThermalError::InvalidParameter) - } - } -} - -async fn sensor_get_warn_thrs<'hw>( - instance_id: u8, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - let low = thermal_service - .execute_sensor_request( - sensor::DeviceId(instance_id), - sensor::Request::GetThreshold(sensor::ThresholdType::WarnLow), - ) - .await; - let high = thermal_service - .execute_sensor_request( - sensor::DeviceId(instance_id), - sensor::Request::GetThreshold(sensor::ThresholdType::WarnHigh), - ) - .await; - - match (low, high) { - (Ok(sensor::ResponseData::Threshold(low)), Ok(sensor::ResponseData::Threshold(high))) => { - Ok(thermal_service_messages::ThermalResponse::ThermalGetThrsResponse { - timeout: 0, - low: utils::c_to_dk(low), - high: utils::c_to_dk(high), - }) - } - _ => Err(thermal_service_messages::ThermalError::InvalidParameter), - } -} - -async fn sensor_set_warn_thrs<'hw>( - instance_id: u8, - _timeout: Milliseconds, - low: DeciKelvin, - high: DeciKelvin, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - let low_res = thermal_service - .execute_sensor_request( - sensor::DeviceId(instance_id), - sensor::Request::SetThreshold(sensor::ThresholdType::WarnLow, utils::dk_to_c(low)), - ) - .await; - let high_res = thermal_service - .execute_sensor_request( - sensor::DeviceId(instance_id), - sensor::Request::SetThreshold(sensor::ThresholdType::WarnHigh, utils::dk_to_c(high)), - ) - .await; - - if low_res.is_ok() && high_res.is_ok() { - Ok(thermal_service_messages::ThermalResponse::ThermalSetThrsResponse) - } else { - Err(thermal_service_messages::ThermalError::InvalidParameter) - } -} - -async fn sensor_get_thrs<'hw>( - instance: u8, - threshold_type: sensor::ThresholdType, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - match thermal_service - .execute_sensor_request( - sensor::DeviceId(instance), - sensor::Request::GetThreshold(threshold_type), - ) - .await - { - Ok(sensor::ResponseData::Temp(temp)) => Ok(thermal_service_messages::ThermalResponse::ThermalGetVarResponse { - val: utils::c_to_dk(temp), - }), - _ => Err(thermal_service_messages::ThermalError::HardwareError), - } -} - -async fn fan_get_temp<'hw>( - instance: u8, - fan_request: fan::Request, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - match thermal_service - .execute_fan_request(fan::DeviceId(instance), fan_request) - .await - { - Ok(fan::ResponseData::Temp(temp)) => Ok(thermal_service_messages::ThermalResponse::ThermalGetVarResponse { - val: utils::c_to_dk(temp), - }), - _ => Err(thermal_service_messages::ThermalError::HardwareError), - } -} - -async fn fan_get_rpm<'hw>( - instance: u8, - fan_request: fan::Request, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - match thermal_service - .execute_fan_request(fan::DeviceId(instance), fan_request) - .await - { - Ok(fan::ResponseData::Rpm(rpm)) => { - Ok(thermal_service_messages::ThermalResponse::ThermalGetVarResponse { val: rpm.into() }) - } - _ => Err(thermal_service_messages::ThermalError::HardwareError), - } -} - -async fn sensor_set_thrs<'hw>( - instance: u8, - threshold_type: sensor::ThresholdType, - threshold_dk: u32, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - match thermal_service - .execute_sensor_request( - sensor::DeviceId(instance), - sensor::Request::SetThreshold(threshold_type, utils::dk_to_c(threshold_dk)), - ) - .await - { - Ok(sensor::ResponseData::Success) => Ok(thermal_service_messages::ThermalResponse::ThermalSetVarResponse), - _ => Err(thermal_service_messages::ThermalError::HardwareError), - } -} - -async fn fan_set_var<'hw>( - instance: u8, - fan_request: fan::Request, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - match thermal_service - .execute_fan_request(fan::DeviceId(instance), fan_request) - .await - { - Ok(fan::ResponseData::Success) => Ok(thermal_service_messages::ThermalResponse::ThermalSetVarResponse), - _ => Err(thermal_service_messages::ThermalError::HardwareError), - } -} - -pub(crate) async fn process_request<'hw>( - request: &thermal_service_messages::ThermalRequest, - thermal_service: &crate::Service<'hw>, -) -> thermal_service_messages::ThermalResult { - match request { - thermal_service_messages::ThermalRequest::ThermalGetTmpRequest { instance_id } => { - sensor_get_tmp(*instance_id, thermal_service).await - } - thermal_service_messages::ThermalRequest::ThermalSetThrsRequest { - instance_id, - timeout, - low, - high, - } => sensor_set_warn_thrs(*instance_id, *timeout, *low, *high, thermal_service).await, - thermal_service_messages::ThermalRequest::ThermalGetThrsRequest { instance_id } => { - sensor_get_warn_thrs(*instance_id, thermal_service).await - } - // TODO: How do we handle this generically? - thermal_service_messages::ThermalRequest::ThermalSetScpRequest { .. } => todo!(), - thermal_service_messages::ThermalRequest::ThermalGetVarRequest { - instance_id, - len: _len, - var_uuid, - } => get_var_handler(*instance_id, *var_uuid, thermal_service).await, - thermal_service_messages::ThermalRequest::ThermalSetVarRequest { - instance_id, - len: _len, - var_uuid, - set_var, - } => set_var_handler(*instance_id, *var_uuid, *set_var, thermal_service).await, - } -} diff --git a/thermal-service/src/sensor.rs b/thermal-service/src/sensor.rs index 1771d14de..9c50e0717 100644 --- a/thermal-service/src/sensor.rs +++ b/thermal-service/src/sensor.rs @@ -1,31 +1,14 @@ -//! Sensor Device -use crate::Event; use crate::utils::SampleBuf; -use embassy_sync::mutex::Mutex; -use embassy_sync::signal::Signal; -use embassy_time::Timer; -use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor, TemperatureThresholdSet}; -use embedded_services::GlobalRawMutex; -use embedded_services::error; -use embedded_services::ipc::deferred as ipc; - -// Timeout period (in ms) for physical bus access -const BUS_TIMEOUT: u64 = 200; - -/// Convenience type for Sensor response result -pub type Response = Result; - -/// Allows OEM to implement custom requests -/// -/// The default response is to return an error on unrecognized requests -pub trait CustomRequestHandler { - fn handle_custom_request(&self, _request: Request) -> impl core::future::Future { - async { Err(Error::InvalidRequest) } - } -} +use core::marker::PhantomData; +use embassy_sync::{mutex::Mutex, signal::Signal}; +use embassy_time::{Duration, Timer, with_timeout}; +use embedded_sensors_hal_async::temperature::DegreesCelsius; +use embedded_services::event::Sender; +use embedded_services::{GlobalRawMutex, error}; +use thermal_service_interface::sensor; -/// Ensures all necessary traits are implemented for the controlling driver -pub trait Controller: TemperatureSensor + TemperatureThresholdSet + CustomRequestHandler {} +// Timeout period for physical bus access +const BUS_TIMEOUT: Duration = Duration::from_millis(200); /* Helper macro for calling a bus function with automatic retry after timeout or failure. * @@ -37,14 +20,14 @@ macro_rules! with_retry { $self:expr, $bus_method:expr ) => {{ - let mut retry_attempts = $self.profile.lock().await.retry_attempts; + let mut retry_attempts = $self.config.lock().await.retry_attempts; loop { if retry_attempts == 0 { - break Err(Error::Hardware); + break Err(sensor::Error::RetryExhausted); } - match embassy_time::with_timeout(embassy_time::Duration::from_millis(BUS_TIMEOUT), $bus_method).await { + match with_timeout(BUS_TIMEOUT, $bus_method).await { Ok(Ok(val)) => break Ok(val), _ => { retry_attempts -= 1; @@ -54,520 +37,303 @@ macro_rules! with_retry { }}; } -/// Sensor threshold type -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ThresholdType { - /// Threshold below which host is notified - WarnLow, - /// Threshold above which host is notified - WarnHigh, - /// Threshold above which PROCHOT is asserted - Prochot, - /// Threshold above which critical temperature is reached and system should be shutdown - /// Some systems may tie sensor alert pin directly to reset controller, in which case - /// SetHardAlert should be used. - Critical, -} - -/// Sensor error type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Sensor service configuration parameters. +#[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Error { - /// Invalid request - InvalidRequest, - /// Device encountered a hardware failure - Hardware, -} - -/// Sensor request -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Request { - /// Most recent cached temperature measurement - GetTemp, - /// Average temperature measurement (over BUFFER_SIZE * SAMPLING_PERIOD) - GetAvgTemp, - /// Instructs sensor to immediately sample temperature (not cached) - GetTmpNow, - /// Low threshold below which sensor will set the alert pin active (in degrees Celsius) - SetHardAlertLow(DegreesCelsius), - /// High threshold above which sensor will set the alert pin active (in degrees Celsius) - SetHardAlertHigh(DegreesCelsius), - /// Get a threshold - GetThreshold(ThresholdType), - /// Set a threshold - SetThreshold(ThresholdType, DegreesCelsius), - /// Threshold in which sensor begins fast sampling - SetFastSamplingThreshold(DegreesCelsius), - /// Set temperature sampling period (in ms) - SetSamplingPeriod(u64), - /// Set fast temperature sampling period (in ms) - SetFastSamplingPeriod(u64), - /// An offset that is applied to all physical temperature samples (in degrees Celsius) - SetOffset(DegreesCelsius), - /// Enable sensor sampling - EnableSampling, - /// Disable sensor sampling - DisableSampling, - /// Set the max number of times communication with physical sensor will be attempted until error is reported - SetRetryAttempts(u8), - /// Get the thermal profile associated with this sensor - GetProfile, - /// Set the thermal profile associated with this sensor - SetProfile(Profile), - /// Custom-implemented command - Custom(u8, &'static [u8]), -} - -/// Sensor response -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ResponseData { - /// Response for any request that is successful but does not require data - Success, - /// Temperature (in degrees Celsius) - Temp(DegreesCelsius), - /// Threshold (in degrees Celsius) - Threshold(DegreesCelsius), - /// Profile - Profile(Profile), - /// Custom-implemented response - Custom(&'static [u8]), -} - -/// Sensor device ID new type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DeviceId(pub u8); - -/// Sensor device struct -pub struct Device { - /// Device ID - id: DeviceId, - /// Channel for IPC requests and responses - ipc: ipc::Channel, - /// Signal for enable - enable: Signal, -} - -impl Device { - /// Create a new sensor device - pub fn new(id: DeviceId) -> Self { - Self { - id, - ipc: ipc::Channel::new(), - enable: Signal::new(), - } - } - - /// Get the device ID - pub fn id(&self) -> DeviceId { - self.id - } - - /// Execute request and wait for response - pub async fn execute_request(&self, request: Request) -> Response { - self.ipc.execute(request).await - } -} - -/// Sensor profile -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Profile { - /// Profile ID - pub id: usize, - /// Period (in ms) sensor will sample its temperature - pub sample_period: u64, - /// Period (in ms) sensor will sample its temperature when in fast sampling state - pub fast_sample_period: u64, - /// Whether or not automatic background sampling is enabled or not +pub struct Config { + /// Rate at which to sample the sensor when operating in normal conditions. + pub sample_period: Duration, + /// Rate at which to sample the sensor when operating in fast conditions. + pub fast_sample_period: Duration, + /// Whether periodic sampling is enabled. pub sampling_enabled: bool, - /// Hysteresis value (in degrees Celsius) preventing sensor from rapidly reporting threshold events + /// Hysteresis value to prevent rapid generation of threshold events when temperature is near a threshold. pub hysteresis: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will trigger a WARN LOW event + /// Temperature threshold below which a warning event will be generated. pub warn_low_threshold: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will trigger a WARN HIGH event + /// Temperature threshold above which a warning event will be generated. pub warn_high_threshold: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will trigger a PROCHOT event + /// Temperature threshold above which a prochot event will be generated. pub prochot_threshold: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will trigger a CRITICAL event - pub crt_threshold: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will enter the fast sampling state + /// Temperature threshold above which a critical event will be generated. + pub critical_threshold: DegreesCelsius, + /// Temperature threshold above which fast sampling is enabled. pub fast_sampling_threshold: DegreesCelsius, - /// Offset (in degrees Celsius) to be added to sampled temperature + /// Offset to be applied to the temperature readings. pub offset: DegreesCelsius, - /// Number of attempts sensor will make to communicate with the physical device over the bus + /// Number of retry attempts for bus operations. pub retry_attempts: u8, } -impl Default for Profile { +impl Default for Config { fn default() -> Self { Self { - id: 0, - sample_period: 1000, - fast_sample_period: 200, + sample_period: Duration::from_secs(1), + fast_sample_period: Duration::from_millis(200), sampling_enabled: true, + hysteresis: 2.0, warn_low_threshold: DegreesCelsius::MIN, warn_high_threshold: DegreesCelsius::MAX, prochot_threshold: DegreesCelsius::MAX, - crt_threshold: DegreesCelsius::MAX, + critical_threshold: DegreesCelsius::MAX, fast_sampling_threshold: DegreesCelsius::MAX, offset: 0.0, retry_attempts: 5, - hysteresis: 2.0, } } } -// Additional Sensor state -#[derive(Debug, Clone, Copy, PartialEq, Default)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -struct State { - is_warn_low: bool, - is_warn_high: bool, - is_prochot: bool, - is_critical: bool, -} - -/// Wrapper binding a communication device, hardware driver, and additional state. -pub struct Sensor { - /// Sensor communication device - device: Device, - /// Sensor controller - controller: Mutex, - /// Sensor profile - profile: Mutex, - /// Sensor state - state: Mutex, - /// Cached temperature samples +struct ServiceInner { + driver: Mutex, + en_signal: Signal, + config: Mutex, samples: Mutex>, } -impl Sensor { - /// New sensor - /// - /// Sample buffer length MUST be a power of two - pub fn new(id: DeviceId, controller: T, profile: Profile) -> Self { +impl ServiceInner { + fn new(driver: T, config: Config) -> Self { Self { - device: Device::new(id), - controller: Mutex::new(controller), - profile: Mutex::new(profile), - state: Mutex::new(State::default()), + driver: Mutex::new(driver), + en_signal: Signal::new(), + config: Mutex::new(config), samples: Mutex::new(SampleBuf::create()), } } +} - /// Retrieve a reference to underlying device for registration with services - pub fn device(&self) -> &Device { - &self.device +/// Sensor service control handle. +pub struct Service<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> { + inner: &'hw ServiceInner, + _phantom: PhantomData, +} + +// Note: We can't derive these traits because the compiler thinks our generics then need to be Copy + Clone, +// but we only hold a reference and don't actually need to be that strict +impl, const SAMPLE_BUF_LEN: usize> Clone + for Service<'_, T, E, SAMPLE_BUF_LEN> +{ + fn clone(&self) -> Self { + *self } +} + +impl, const SAMPLE_BUF_LEN: usize> Copy + for Service<'_, T, E, SAMPLE_BUF_LEN> +{ +} - /// Retrieve a Mutex wrapping the underlying controller - /// - /// Should only be used to update OEM specific state - pub fn controller(&self) -> &Mutex { - &self.controller +impl<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> sensor::SensorService + for Service<'hw, T, E, SAMPLE_BUF_LEN> +{ + async fn temperature(&self) -> DegreesCelsius { + self.inner.samples.lock().await.recent() } - /// Wait for sensor to receive a request - pub async fn wait_request(&self) -> ipc::Request<'_, GlobalRawMutex, Request, Response> { - self.device.ipc.receive().await + async fn temperature_average(&self) -> DegreesCelsius { + self.inner.samples.lock().await.average() } - /// Process sensor request - pub async fn process_request(&self, request: Request) -> Response { - match request { - Request::GetTemp => { - let temp = self.samples.lock().await.recent(); - Ok(ResponseData::Temp(temp)) - } - Request::GetAvgTemp => { - let temp = self.samples.lock().await.average(); - Ok(ResponseData::Temp(temp)) - } - Request::GetTmpNow => { - let temp = with_retry!(self, self.controller.lock().await.temperature())?; - Ok(ResponseData::Temp(temp)) - } - Request::SetHardAlertLow(low) => { - with_retry!(self, self.controller.lock().await.set_temperature_threshold_low(low))?; - Ok(ResponseData::Success) - } - Request::SetHardAlertHigh(high) => { - with_retry!(self, self.controller.lock().await.set_temperature_threshold_high(high))?; - Ok(ResponseData::Success) - } - Request::GetThreshold(ThresholdType::WarnLow) => { - let threshold = self.profile.lock().await.warn_low_threshold; - Ok(ResponseData::Threshold(threshold)) - } - Request::GetThreshold(ThresholdType::WarnHigh) => { - let threshold = self.profile.lock().await.warn_high_threshold; - Ok(ResponseData::Threshold(threshold)) - } - Request::GetThreshold(ThresholdType::Prochot) => { - let threshold = self.profile.lock().await.prochot_threshold; - Ok(ResponseData::Threshold(threshold)) - } - Request::GetThreshold(ThresholdType::Critical) => { - let threshold = self.profile.lock().await.crt_threshold; - Ok(ResponseData::Threshold(threshold)) - } - Request::SetThreshold(ThresholdType::WarnLow, threshold) => { - self.profile.lock().await.warn_low_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetThreshold(ThresholdType::WarnHigh, threshold) => { - self.profile.lock().await.warn_high_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetThreshold(ThresholdType::Prochot, threshold) => { - self.profile.lock().await.prochot_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetThreshold(ThresholdType::Critical, threshold) => { - self.profile.lock().await.crt_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetFastSamplingThreshold(threshold) => { - self.profile.lock().await.fast_sampling_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetSamplingPeriod(period) => { - self.profile.lock().await.sample_period = period; - Ok(ResponseData::Success) - } - Request::SetFastSamplingPeriod(period) => { - self.profile.lock().await.fast_sample_period = period; - Ok(ResponseData::Success) - } - Request::SetOffset(offset) => { - self.profile.lock().await.offset = offset; - Ok(ResponseData::Success) - } - Request::EnableSampling => { - self.profile.lock().await.sampling_enabled = true; - self.device.enable.signal(()); - Ok(ResponseData::Success) - } - Request::DisableSampling => { - self.profile.lock().await.sampling_enabled = false; - Ok(ResponseData::Success) - } - Request::SetRetryAttempts(limit) => { - self.profile.lock().await.retry_attempts = limit; - Ok(ResponseData::Success) - } - Request::GetProfile => { - let profile = *self.profile.lock().await; - Ok(ResponseData::Profile(profile)) - } - Request::SetProfile(profile) => { - *self.profile.lock().await = profile; - Ok(ResponseData::Success) - } - Request::Custom(_, _) => self.controller.lock().await.handle_custom_request(request).await, + async fn temperature_immediate(&self) -> Result { + with_retry!(self.inner, self.inner.driver.lock().await.temperature()) + } + + async fn set_threshold(&self, threshold: sensor::Threshold, value: DegreesCelsius) { + let mut config = self.inner.config.lock().await; + match threshold { + sensor::Threshold::WarnLow => config.warn_low_threshold = value, + sensor::Threshold::WarnHigh => config.warn_high_threshold = value, + sensor::Threshold::Prochot => config.prochot_threshold = value, + sensor::Threshold::Critical => config.critical_threshold = value, } } - // Wait for sensor to receive a request, process it, and send a response - pub async fn wait_and_process(&self) { - let request = self.wait_request().await; - let response = self.process_request(request.command).await; - request.respond(response); + async fn threshold(&self, threshold: sensor::Threshold) -> DegreesCelsius { + let config = self.inner.config.lock().await; + match threshold { + sensor::Threshold::WarnLow => config.warn_low_threshold, + sensor::Threshold::WarnHigh => config.warn_high_threshold, + sensor::Threshold::Prochot => config.prochot_threshold, + sensor::Threshold::Critical => config.critical_threshold, + } } - /// Waits for a request then processes it and sends a response - pub async fn handle_rx(&self) { - loop { - self.wait_and_process().await; + async fn set_sample_period(&self, period: Duration) { + self.inner.config.lock().await.sample_period = period; + } + + async fn enable_sampling(&self) { + self.inner.config.lock().await.sampling_enabled = true; + self.inner.en_signal.signal(()); + } + + async fn disable_sampling(&self) { + self.inner.config.lock().await.sampling_enabled = false; + } +} + +/// Parameters required to initialize a sensor service. +pub struct InitParams<'hw, T: sensor::Driver, E: Sender> { + /// The underlying sensor driver this service will control. + pub driver: T, + /// Initial configuration for the sensor service. + pub config: Config, + /// Event senders for sensor events. + pub event_senders: &'hw mut [E], +} + +/// The memory resources required by the sensor. +pub struct Resources { + inner: Option>, +} + +// Note: We can't derive Default unless we trait bound T by Default, +// but we don't want that restriction since the default is just the None case +impl Default for Resources { + fn default() -> Self { + Self { inner: None } + } +} + +// Additional sensor runner state +#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct State { + is_warn_low: bool, + is_warn_high: bool, + is_prochot: bool, + is_critical: bool, +} + +/// A task runner for a sensor. Users must run this in an embassy task or similar async execution context. +pub struct Runner<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> { + service: &'hw ServiceInner, + event_senders: &'hw mut [E], + state: State, +} + +impl<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> Runner<'hw, T, E, SAMPLE_BUF_LEN> { + async fn broadcast_event(&mut self, event: sensor::Event) { + for sender in self.event_senders.iter_mut() { + sender.send(event).await; } } - async fn check_thresholds<'hw>(&self, temp: DegreesCelsius, thermal_service: &crate::Service<'hw>) { - let profile = self.profile.lock().await; - let mut state = self.state.lock().await; + async fn check_thresholds(&mut self, temp: DegreesCelsius) { + let config = *self.service.config.lock().await; - if temp >= profile.warn_high_threshold && !state.is_warn_high { - thermal_service - .send_event(Event::ThresholdExceeded(self.device.id, ThresholdType::WarnHigh, temp)) + if temp >= config.warn_high_threshold && !self.state.is_warn_high { + self.state.is_warn_high = true; + self.broadcast_event(sensor::Event::ThresholdExceeded(sensor::Threshold::WarnHigh)) .await; - state.is_warn_high = true; - } else if temp < (profile.warn_high_threshold - profile.hysteresis) && state.is_warn_high { - thermal_service - .send_event(Event::ThresholdCleared(self.device.id, ThresholdType::WarnHigh)) + } else if temp < (config.warn_high_threshold - config.hysteresis) && self.state.is_warn_high { + self.state.is_warn_high = false; + self.broadcast_event(sensor::Event::ThresholdCleared(sensor::Threshold::WarnHigh)) .await; - state.is_warn_high = false; } - if temp <= profile.warn_low_threshold && !state.is_warn_low { - thermal_service - .send_event(Event::ThresholdExceeded(self.device.id, ThresholdType::WarnLow, temp)) + if temp <= config.warn_low_threshold && !self.state.is_warn_low { + self.state.is_warn_low = true; + self.broadcast_event(sensor::Event::ThresholdExceeded(sensor::Threshold::WarnLow)) .await; - state.is_warn_low = true; - } else if temp > (profile.warn_low_threshold + profile.hysteresis) && state.is_warn_low { - thermal_service - .send_event(Event::ThresholdCleared(self.device.id, ThresholdType::WarnLow)) + } else if temp > (config.warn_low_threshold + config.hysteresis) && self.state.is_warn_low { + self.state.is_warn_low = false; + self.broadcast_event(sensor::Event::ThresholdCleared(sensor::Threshold::WarnLow)) .await; - state.is_warn_low = false; } - if temp >= profile.prochot_threshold && !state.is_prochot { - thermal_service - .send_event(Event::ThresholdExceeded(self.device.id, ThresholdType::Prochot, temp)) + if temp >= config.prochot_threshold && !self.state.is_prochot { + self.state.is_prochot = true; + self.broadcast_event(sensor::Event::ThresholdExceeded(sensor::Threshold::Prochot)) .await; - state.is_prochot = true; - } else if temp < (profile.prochot_threshold - profile.hysteresis) && state.is_prochot { - thermal_service - .send_event(Event::ThresholdCleared(self.device.id, ThresholdType::Prochot)) + } else if temp < (config.prochot_threshold - config.hysteresis) && self.state.is_prochot { + self.state.is_prochot = false; + self.broadcast_event(sensor::Event::ThresholdCleared(sensor::Threshold::Prochot)) .await; - state.is_prochot = false; } - if temp >= profile.crt_threshold && !state.is_critical { - thermal_service - .send_event(Event::ThresholdExceeded(self.device.id, ThresholdType::Critical, temp)) + if temp >= config.critical_threshold && !self.state.is_critical { + self.state.is_critical = true; + self.broadcast_event(sensor::Event::ThresholdExceeded(sensor::Threshold::Critical)) .await; - state.is_critical = true; - } else if temp < (profile.crt_threshold - profile.hysteresis) && state.is_critical { - thermal_service - .send_event(Event::ThresholdCleared(self.device.id, ThresholdType::Critical)) + } else if temp < (config.critical_threshold - config.hysteresis) && self.state.is_critical { + self.state.is_critical = false; + self.broadcast_event(sensor::Event::ThresholdCleared(sensor::Threshold::Critical)) .await; - state.is_critical = false; } } +} - /// Periodically samples temperature from physical sensor and caches it - pub async fn handle_sampling<'hw>(&self, thermal_service: &crate::Service<'hw>) { +impl<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> + odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw, T, E, SAMPLE_BUF_LEN> +{ + async fn run(mut self) -> embedded_services::Never { loop { + let config = *self.service.config.lock().await; + // Only sample temperature if enabled - if self.profile.lock().await.sampling_enabled { - let temp = match with_retry!(self, self.controller.lock().await.temperature()) { + if config.sampling_enabled { + let temp = match with_retry!(self.service, self.service.driver.lock().await.temperature()) { Ok(temp) => temp, - _ => { - self.profile.lock().await.sampling_enabled = false; - thermal_service - .send_event(Event::SensorFailure(self.device.id, Error::Hardware)) - .await; - error!("Error sampling sensor {}, disabling sampling", self.device.id.0); + Err(e) => { + self.service.config.lock().await.sampling_enabled = false; + self.broadcast_event(sensor::Event::Failure(e)).await; + error!("Error sampling sensor, disabling sampling"); continue; } }; // Add offset to measured temperature - let temp = temp + self.profile.lock().await.offset; + let temp = temp + config.offset; // Cache in buffer for quick retrieval from other services - self.samples.lock().await.push(temp); + self.service.samples.lock().await.push(temp); // Check thresholds - self.check_thresholds(temp, thermal_service).await; + self.check_thresholds(temp).await; // Adjust sampling rate based on how hot we are getting - let profile = self.profile.lock().await; - let sleep_duration = if temp >= profile.fast_sampling_threshold { - profile.fast_sample_period + let sleep_duration = if temp >= config.fast_sampling_threshold { + config.fast_sample_period } else { - profile.sample_period + config.sample_period }; - drop(profile); // Sleep in-between sampling periods - Timer::after_millis(sleep_duration).await; + Timer::after(sleep_duration).await; // Otherwise sleep and wait to be re-enabled } else { - self.device.enable.wait().await; + self.service.en_signal.wait().await; } } } } -/// The memory resources required by the sensor. -pub struct Resources<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - inner: Option>, -} - -// Note: We can't derive Default unless we trait bound T by Default, -// but we don't want that restriction since the default is just the None case -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> Default for Resources<'hw, T, SAMPLE_BUF_LEN> { - fn default() -> Self { - Self { inner: None } - } -} - -struct ServiceInner<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - sensor: &'hw Sensor, - thermal_service: &'hw crate::Service<'hw>, -} - -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> ServiceInner<'hw, T, SAMPLE_BUF_LEN> { - fn new(init_params: InitParams<'hw, T, SAMPLE_BUF_LEN>) -> Self { - Self { - sensor: init_params.sensor, - thermal_service: init_params.thermal_service, - } - } - - fn sensor(&self) -> &Sensor { - self.sensor - } -} - -/// A task runner for a sensor. Users must run this in an embassy task or similar async execution context. -pub struct Runner<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - service: &'hw ServiceInner<'hw, T, SAMPLE_BUF_LEN>, -} - -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> odp_service_common::runnable_service::ServiceRunner<'hw> - for Runner<'hw, T, SAMPLE_BUF_LEN> -{ - async fn run(self) -> embedded_services::Never { - loop { - let _ = embassy_futures::join::join( - self.service.sensor.handle_rx(), - self.service.sensor.handle_sampling(self.service.thermal_service), - ) - .await; - } - } -} - -/// Sensor service control handle. -pub struct Service<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - inner: &'hw ServiceInner<'hw, T, SAMPLE_BUF_LEN>, -} - -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> Service<'hw, T, SAMPLE_BUF_LEN> { - /// Get a reference to the inner sensor. - pub fn sensor(&self) -> &Sensor { - self.inner.sensor() - } -} - -/// Parameters required to initialize a sensor service. -pub struct InitParams<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> { - /// The underlying `Sensor` wrapper this service will control. - pub sensor: &'hw Sensor, - /// The thermal service handle for this sensor to communicate events to. - pub thermal_service: &'hw crate::Service<'hw>, -} - -impl<'hw, T: Controller, const SAMPLE_BUF_LEN: usize> odp_service_common::runnable_service::Service<'hw> - for Service<'hw, T, SAMPLE_BUF_LEN> +impl<'hw, T: sensor::Driver, E: Sender + 'hw, const SAMPLE_BUF_LEN: usize> + odp_service_common::runnable_service::Service<'hw> for Service<'hw, T, E, SAMPLE_BUF_LEN> { - type Runner = Runner<'hw, T, SAMPLE_BUF_LEN>; - type Resources = Resources<'hw, T, SAMPLE_BUF_LEN>; - type ErrorType = Error; - type InitParams = InitParams<'hw, T, SAMPLE_BUF_LEN>; + type Runner = Runner<'hw, T, E, SAMPLE_BUF_LEN>; + type Resources = Resources; + type ErrorType = sensor::Error; + type InitParams = InitParams<'hw, T, E>; async fn new( service_storage: &'hw mut Self::Resources, init_params: Self::InitParams, ) -> Result<(Self, Self::Runner), Self::ErrorType> { - let service = service_storage.inner.insert(ServiceInner::new(init_params)); - Ok((Self { inner: service }, Runner { service })) + let service = service_storage + .inner + .insert(ServiceInner::new(init_params.driver, init_params.config)); + Ok(( + Self { + inner: service, + _phantom: PhantomData, + }, + Runner { + service, + event_senders: init_params.event_senders, + state: State::default(), + }, + )) } } diff --git a/thermal-service/src/utils.rs b/thermal-service/src/utils.rs index 58c70c127..f8598c130 100644 --- a/thermal-service/src/utils.rs +++ b/thermal-service/src/utils.rs @@ -1,4 +1,4 @@ -//! Helpful utilities for the thermal service +//! Helpful utilities for the thermal service. use heapless::Deque; /// Buffer for storing samples @@ -29,23 +29,20 @@ impl SampleBuf { } impl SampleBuf { + /// Returns the average of the samples in the buffer, or 0.0 if the buffer is empty. pub fn average(&self) -> f32 { - self.deque.iter().copied().sum::() / (self.deque.len() as f32) + let len = self.deque.len(); + if len == 0 { + return 0.0; + } + self.deque.iter().copied().sum::() / len as f32 } } impl SampleBuf { + /// Returns the average of the samples in the buffer, or 0 if the buffer is empty. pub fn average(&self) -> u16 { - self.deque.iter().copied().sum::() / (self.deque.len() as u16) + let sum: u32 = self.deque.iter().copied().map(u32::from).sum(); + sum.checked_div(self.deque.len() as u32).unwrap_or(0) as u16 } } - -/// Convert deciKelvin to degrees Celsius -pub const fn dk_to_c(dk: thermal_service_messages::DeciKelvin) -> f32 { - (dk as f32 / 10.0) - 273.15 -} - -/// Convert degrees Celsius to deciKelvin -pub const fn c_to_dk(c: f32) -> thermal_service_messages::DeciKelvin { - ((c + 273.15) * 10.0) as thermal_service_messages::DeciKelvin -}