From 0b9324ca1ffdf6884f031922f57dcd46991bd08a Mon Sep 17 00:00:00 2001 From: Kurtis Dinelle Date: Mon, 9 Feb 2026 13:46:12 -0800 Subject: [PATCH] thermal-service: Add mocks --- examples/std/Cargo.toml | 2 +- examples/std/src/bin/thermal.rs | 436 +++-------------------------- thermal-service/Cargo.toml | 3 +- thermal-service/src/lib.rs | 2 + thermal-service/src/mock/fan.rs | 59 ++++ thermal-service/src/mock/mod.rs | 57 ++++ thermal-service/src/mock/sensor.rs | 70 +++++ 7 files changed, 235 insertions(+), 394 deletions(-) create mode 100644 thermal-service/src/mock/fan.rs create mode 100644 thermal-service/src/mock/mod.rs create mode 100644 thermal-service/src/mock/sensor.rs diff --git a/examples/std/Cargo.toml b/examples/std/Cargo.toml index bd245060..5f537535 100644 --- a/examples/std/Cargo.toml +++ b/examples/std/Cargo.toml @@ -38,7 +38,7 @@ type-c-service = { path = "../../type-c-service", features = ["log"] } embedded-sensors-hal-async = "0.3.0" embedded-fans-async = "0.2.0" -thermal-service = { path = "../../thermal-service", features = ["log"] } +thermal-service = { path = "../../thermal-service", features = ["log", "mock"] } thermal-service-messages = { path = "../../thermal-service-messages" } env_logger = "0.9.0" diff --git a/examples/std/src/bin/thermal.rs b/examples/std/src/bin/thermal.rs index 0f5a7b9d..357ef97f 100644 --- a/examples/std/src/bin/thermal.rs +++ b/examples/std/src/bin/thermal.rs @@ -1,399 +1,29 @@ use embassy_executor::{Executor, Spawner}; -use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; -use embassy_sync::mutex::Mutex; use embassy_sync::once_lock::OnceLock; use embassy_time::Timer; -use embedded_fans_async as fan; -use embedded_sensors_hal_async::sensor; -use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor, TemperatureThresholdSet}; -use embedded_services::comms; -use log::{info, warn}; +use embedded_services::{error, info}; use static_cell::StaticCell; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; use thermal_service as ts; -use thermal_service_messages::ThermalRequest; -use ts::mptf; -const SAMPLE_BUF_LEN: usize = 16; - -// Mock host service -mod host { - use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; - use embedded_services::comms::{self, Endpoint, EndpointID, External, MailboxDelegate}; - use log::{info, warn}; - use thermal_service as ts; - use thermal_service_messages::{ThermalResponse, ThermalResult}; - use ts::mptf; - - pub struct Host { - pub tp: Endpoint, - pub alert: Signal, - } - - impl Host { - pub fn new() -> Self { - Self { - tp: Endpoint::uninit(EndpointID::External(External::Host)), - alert: Signal::new(), - } - } - - fn handle_response(&self, response: ThermalResponse) { - match response { - ThermalResponse::ThermalGetTmpResponse { temperature } => { - info!("Host received temperature: {} °C", ts::utils::dk_to_c(temperature)) - } - ThermalResponse::ThermalGetVarResponse { val } => { - info!("Host received fan RPM: {val}") - } - _ => info!("Received MPTF response: {response:?}"), - } - } - } - - impl MailboxDelegate for Host { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - if let Some(&result) = message.data.get::() { - self.handle_response(result.map_err(|_| comms::MailboxDelegateError::Other)?); - Ok(()) - } else if let Some(¬ification) = message.data.get::() { - warn!("Received notification: {notification:?}"); - self.alert.signal(()); - Ok(()) - } else { - Err(comms::MailboxDelegateError::MessageNotFound) - } - } - } -} - -// A mock struct shared by MockSensor and MockAlertPin to sync on raw samples and thresholds -struct MockBus { - samples: [f32; 35], - idx: AtomicUsize, - threshold_low: Mutex, - threshold_high: Mutex, -} - -impl MockBus { - fn new() -> Self { - Self { - samples: [ - 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0, 65.0, 70.0, 75.0, 80.0, 85.0, 90.0, 95.0, 100.0, - 105.0, 100.0, 95.0, 90.0, 85.0, 80.0, 75.0, 70.0, 65.0, 60.0, 55.0, 50.0, 45.0, 40.0, 35.0, 30.0, 25.0, - 20.0, - ], - idx: AtomicUsize::new(0), - threshold_low: Mutex::new(0.0), - threshold_high: Mutex::new(0.0), - } - } - - // Return the current sample and move to next sample (wrapping around at end) - fn sample_and_next(&self) -> f32 { - self.samples[self - .idx - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |idx| { - Some((idx + 1) % self.samples.len()) - }) - .unwrap()] - } - - async fn set_threshold_low(&self, threshold: f32) { - *self.threshold_low.lock().await = threshold - } - - async fn set_threshold_high(&self, threshold: f32) { - *self.threshold_high.lock().await = threshold - } -} - -#[derive(Copy, Clone, Debug)] -struct MockSensorError; -impl sensor::Error for MockSensorError { - fn kind(&self) -> sensor::ErrorKind { - sensor::ErrorKind::Other - } -} - -// A mock temperature sensor -struct MockSensor { - bus: &'static MockBus, -} - -impl MockSensor { - fn new(bus: &'static MockBus) -> Self { - Self { bus } - } -} - -impl sensor::ErrorType for MockSensor { - type Error = MockSensorError; -} - -impl TemperatureSensor for MockSensor { - async fn temperature(&mut self) -> Result { - Ok(self.bus.sample_and_next()) - } -} - -impl TemperatureThresholdSet for MockSensor { - async fn set_temperature_threshold_low(&mut self, threshold: DegreesCelsius) -> Result<(), Self::Error> { - self.bus.set_threshold_low(threshold).await; - Ok(()) - } - - async fn set_temperature_threshold_high(&mut self, threshold: DegreesCelsius) -> Result<(), Self::Error> { - self.bus.set_threshold_high(threshold).await; - Ok(()) - } -} - -impl ts::sensor::CustomRequestHandler for MockSensor {} -impl ts::sensor::Controller for MockSensor {} - -#[derive(Copy, Clone, Debug)] -struct MockFanError; -impl fan::Error for MockFanError { - fn kind(&self) -> embedded_fans_async::ErrorKind { - fan::ErrorKind::Other - } -} - -// A mock fan -struct MockFan { - rpm: u16, -} - -impl MockFan { - fn new() -> Self { - Self { rpm: 0 } - } -} - -impl fan::ErrorType for MockFan { - type Error = MockFanError; -} - -impl fan::Fan for MockFan { - fn min_rpm(&self) -> u16 { - 1000 - } - - fn max_rpm(&self) -> u16 { - 5000 - } - - fn min_start_rpm(&self) -> u16 { - 1000 - } - - async fn set_speed_rpm(&mut self, rpm: u16) -> Result { - self.rpm = rpm; - Ok(rpm) - } -} - -impl fan::RpmSense for MockFan { - async fn rpm(&mut self) -> Result { - Ok(self.rpm) - } -} - -impl ts::fan::CustomRequestHandler for MockFan {} -impl ts::fan::RampResponseHandler for MockFan {} -impl ts::fan::Controller for MockFan {} - -// Simulates host receiving requests from OSPM and forwarding to thermal service #[embassy_executor::task] -async fn host() { - info!("Spawning host task"); - - static HOST: OnceLock = OnceLock::new(); - let host = HOST.get_or_init(host::Host::new); - info!("Registering host endpoint"); - comms::register_endpoint(host, &host.tp).await.unwrap(); - - let thermal_id = comms::EndpointID::Internal(comms::Internal::Thermal); - - // Set thresholds to 40 °C (3131 deciKelvin) - host.tp - .send( - thermal_id, - &ThermalRequest::ThermalSetThrsRequest { - instance_id: 0, - timeout: 0, - low: 0, - high: 3131, - }, - ) - .await - .unwrap(); - Timer::after_millis(100).await; - - // Set Fan ON temp to 40 °C (3131 deciKelvin) - host.tp - .send( - thermal_id, - &ThermalRequest::ThermalSetVarRequest { - instance_id: 0, - len: 4, - var_uuid: mptf::uuid_standard::FAN_ON_TEMP, - set_var: 3131, - }, - ) - .await - .unwrap(); - Timer::after_millis(100).await; - - // Set Fan RAMP temp to 50 °C (3231 deciKelvin) - host.tp - .send( - thermal_id, - &ThermalRequest::ThermalSetVarRequest { - instance_id: 0, - len: 4, - var_uuid: mptf::uuid_standard::FAN_RAMP_TEMP, - set_var: 3231, - }, - ) - .await - .unwrap(); - Timer::after_millis(100).await; - - // Set Fan MAX temp to 80 °C (3531 deciKelvin) - host.tp - .send( - thermal_id, - &ThermalRequest::ThermalSetVarRequest { - instance_id: 0, - len: 4, - var_uuid: mptf::uuid_standard::FAN_MAX_TEMP, - set_var: 3531, - }, - ) - .await - .unwrap(); - Timer::after_millis(100).await; - - // Wait to receive MPTF notification that threshold exceeded, then request temperature and RPM - loop { - host.alert.wait().await; - - info!("Host requesting temperature in response to threshold alert"); - host.tp - .send(thermal_id, &ThermalRequest::ThermalGetTmpRequest { instance_id: 0 }) - .await - .unwrap(); - - // Need to wait briefly before send is fixed to propagate errors and we can handle retries - Timer::after_millis(100).await; - - info!("Host requesting fan RPM in response to threshold alert"); - host.tp - .send( - thermal_id, - &ThermalRequest::ThermalGetVarRequest { - instance_id: 0, - len: 4, - var_uuid: mptf::uuid_standard::FAN_CURRENT_RPM, - }, - ) - .await - .unwrap(); - } -} - -async fn create_sensor() -> &'static ts::sensor::Sensor { - info!("Initializing mock bus"); - static BUS: OnceLock = OnceLock::new(); - let bus = BUS.get_or_init(MockBus::new); - - info!("Initializing mock sensor"); - let mock_sensor = MockSensor::new(bus); - static SENSOR: OnceLock> = OnceLock::new(); - - let profile = ts::sensor::Profile { - warn_high_threshold: 40.0, - prochot_threshold: 50.0, - crt_threshold: 80.0, - ..Default::default() - }; - SENSOR.get_or_init(|| ts::sensor::Sensor::new(ts::sensor::DeviceId(0), mock_sensor, profile)) -} +async fn run(spawner: Spawner) { + embedded_services::init().await; -async fn create_fan() -> &'static ts::fan::Fan { - info!("Initializing mock fan"); - let mock_fan = MockFan::new(); - static FAN: OnceLock> = OnceLock::new(); - FAN.get_or_init(|| ts::fan::Fan::new(ts::fan::DeviceId(0), mock_fan, ts::fan::Profile::default())) -} + static SENSOR: StaticCell = StaticCell::new(); + let sensor = SENSOR.init(ts::mock::new_sensor()); -async fn init_thermal(spawner: Spawner) { - info!("Initializing thermal service"); + static FAN: StaticCell = StaticCell::new(); + let fan = FAN.init(ts::mock::new_fan()); static SERVICE: OnceLock = OnceLock::new(); - let sensor = create_sensor().await; - let fan = create_fan().await; - - if let Ok(service) = ts::Service::new(&SERVICE, &[sensor.device()], &[fan.device()]).await { - spawner.must_spawn(mock_sensor_task(sensor, service)); - spawner.must_spawn(mock_fan_task(fan, service)); - spawner.must_spawn(handle_requests(service)); - spawner.must_spawn(handle_alerts(service)); - } else { - panic!("Failed to initialize thermal service!") - } -} - -#[embassy_executor::task] -async fn handle_alerts(service: &'static ts::Service) { - loop { - match service.wait_event().await { - ts::Event::ThresholdExceeded(ts::sensor::DeviceId(sensor_id), ts::sensor::ThresholdType::WarnHigh, _) => { - warn!("Sensor {sensor_id} exceeded WARN threshold"); - service - .send_service_msg(comms::EndpointID::External(comms::External::Host), &mptf::Notify::Warn) - .await - .unwrap() - } - ts::Event::ThresholdExceeded(ts::sensor::DeviceId(sensor_id), ts::sensor::ThresholdType::Prochot, _) => { - warn!("Sensor {sensor_id} exceeded PROCHOT threshold"); - service - .send_service_msg( - comms::EndpointID::External(comms::External::Host), - &mptf::Notify::ProcHot, - ) - .await - .unwrap() - } - ts::Event::ThresholdExceeded(ts::sensor::DeviceId(sensor_id), ts::sensor::ThresholdType::Critical, _) => { - warn!("Sensor {sensor_id} exceeded CRITICAL threshold"); - service - .send_service_msg( - comms::EndpointID::External(comms::External::Host), - &mptf::Notify::Critical, - ) - .await - .unwrap() - } - event => warn!("Event: {event:?}"), - } - } -} + let service = ts::Service::new(&SERVICE, &[sensor.device()], &[fan.device()]) + .await + .expect("Failed to initialize thermal service"); -#[embassy_executor::task] -async fn handle_requests(service: &'static ts::Service) -> ! { - ts::task::handle_requests(service).await; - unreachable!() -} - -#[embassy_executor::task] -async fn run(spawner: Spawner) { - embedded_services::init().await; - init_thermal(spawner).await; - spawner.must_spawn(host()); + spawner.must_spawn(sensor_task(service, sensor)); + spawner.must_spawn(fan_task(service, fan)); + spawner.must_spawn(handle_requests_task(service)); + spawner.must_spawn(monitor(service)); } fn main() { @@ -407,16 +37,38 @@ fn main() { } #[embassy_executor::task] -async fn mock_sensor_task( - sensor: &'static ts::sensor::Sensor, - service: &'static ts::Service, -) -> ! { - ts::task::sensor_task(sensor, service).await; - unreachable!() +async fn sensor_task(service: &'static ts::Service, sensor: &'static ts::mock::TsMockSensor) { + ts::task::sensor_task(sensor, service).await } #[embassy_executor::task] -async fn mock_fan_task(fan: &'static ts::fan::Fan, service: &'static ts::Service) -> ! { +async fn fan_task(service: &'static ts::Service, fan: &'static ts::mock::TsMockFan) { ts::task::fan_task(fan, service).await; - unreachable!() +} + +#[embassy_executor::task] +async fn handle_requests_task(service: &'static ts::Service) { + ts::task::handle_requests(service).await; +} + +#[embassy_executor::task] +async fn monitor(service: &'static ts::Service) { + 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"), + } + 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"), + } + + Timer::after_secs(1).await; + } } diff --git a/thermal-service/Cargo.toml b/thermal-service/Cargo.toml index b5414768..97eacd2f 100644 --- a/thermal-service/Cargo.toml +++ b/thermal-service/Cargo.toml @@ -41,6 +41,7 @@ log = [ "embassy-time/log", "embassy-sync/log", ] +mock = [] [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/thermal-service/src/lib.rs b/thermal-service/src/lib.rs index 4057d401..b49ac43c 100644 --- a/thermal-service/src/lib.rs +++ b/thermal-service/src/lib.rs @@ -8,6 +8,8 @@ use embedded_services::{comms, error, info, intrusive_list}; mod context; pub mod fan; +#[cfg(feature = "mock")] +pub mod mock; pub mod mptf; pub mod sensor; pub mod task; diff --git a/thermal-service/src/mock/fan.rs b/thermal-service/src/mock/fan.rs new file mode 100644 index 00000000..c3fd5401 --- /dev/null +++ b/thermal-service/src/mock/fan.rs @@ -0,0 +1,59 @@ +use crate::fan; +use embedded_fans_async::{Error, ErrorKind, ErrorType, Fan, RpmSense}; + +/// `MockFan` error. +#[derive(Clone, Copy, Debug)] +pub struct MockFanError; +impl Error for MockFanError { + fn kind(&self) -> ErrorKind { + ErrorKind::Other + } +} + +/// Mock fan. +#[derive(Clone, Copy, Debug, Default)] +pub struct MockFan { + rpm: u16, +} + +impl MockFan { + /// Create a new `MockFan`. + pub fn new() -> Self { + Self::default() + } +} + +impl ErrorType for MockFan { + type Error = MockFanError; +} + +impl Fan for MockFan { + fn min_rpm(&self) -> u16 { + 0 + } + + fn max_rpm(&self) -> u16 { + 6000 + } + + fn min_start_rpm(&self) -> u16 { + 1000 + } + + async fn set_speed_rpm(&mut self, rpm: u16) -> Result { + self.rpm = rpm; + Ok(rpm) + } +} + +impl RpmSense for MockFan { + async fn rpm(&mut self) -> Result { + // The mock fan is simple, it just remembers the last RPM it was set to and reports that + // as its current RPM. + Ok(self.rpm) + } +} + +impl fan::CustomRequestHandler for MockFan {} +impl fan::RampResponseHandler for MockFan {} +impl fan::Controller for MockFan {} diff --git a/thermal-service/src/mock/mod.rs b/thermal-service/src/mock/mod.rs new file mode 100644 index 00000000..df78b341 --- /dev/null +++ b/thermal-service/src/mock/mod.rs @@ -0,0 +1,57 @@ +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 new file mode 100644 index 00000000..1dd2028b --- /dev/null +++ b/thermal-service/src/mock/sensor.rs @@ -0,0 +1,70 @@ +use crate::sensor; +use embedded_sensors_hal_async::sensor as sensor_traits; +use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor, TemperatureThresholdSet}; + +/// `MockSensor` error. +#[derive(Clone, Copy, Debug)] +pub struct MockSensorError; +impl sensor_traits::Error for MockSensorError { + fn kind(&self) -> sensor_traits::ErrorKind { + sensor_traits::ErrorKind::Other + } +} + +impl sensor_traits::ErrorType for MockSensor { + type Error = MockSensorError; +} + +/// Mock sensor. +#[derive(Clone, Copy, Debug, Default)] +pub struct MockSensor { + temp: DegreesCelsius, + falling: bool, +} + +impl MockSensor { + /// Create a new `MockSensor`. + pub fn new() -> Self { + Self { + temp: super::MIN_TEMP, + falling: false, + } + } +} + +impl TemperatureSensor for MockSensor { + async fn temperature(&mut self) -> Result { + let t = self.temp; + + // Creates a sawtooth pattern + if self.falling { + self.temp -= 1.0; + if self.temp <= super::MIN_TEMP { + self.temp = super::MIN_TEMP; + self.falling = false; + } + } else { + self.temp += 1.0; + if self.temp >= super::MAX_TEMP { + self.temp = super::MAX_TEMP; + self.falling = true; + } + } + + Ok(t) + } +} + +// Setting a threshold for `MockSensor` doesn't make sense so immediately return error +impl TemperatureThresholdSet for MockSensor { + async fn set_temperature_threshold_low(&mut self, _threshold: DegreesCelsius) -> Result<(), Self::Error> { + Err(MockSensorError) + } + + async fn set_temperature_threshold_high(&mut self, _threshold: DegreesCelsius) -> Result<(), Self::Error> { + Err(MockSensorError) + } +} + +impl sensor::CustomRequestHandler for MockSensor {} +impl sensor::Controller for MockSensor {}