From 3123a58e6765f6e5a9b17e700a42b765b52c87e9 Mon Sep 17 00:00:00 2001 From: James Munns Date: Wed, 22 Jul 2026 19:35:26 +0200 Subject: [PATCH 01/33] Try to push down some state into the BSP itself --- task/thermal/src/bsp/cosmo_ab.rs | 89 ++++++++++++++++++---- task/thermal/src/control.rs | 127 ++++++------------------------- 2 files changed, 97 insertions(+), 119 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 060f785955..95c9cfdb03 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -5,17 +5,21 @@ //! BSP for the Cosmo rev A hardware use crate::{ + Fan, control::{ - ChannelType, ControllerInitError, Device, FanControl, Fans, - InputChannel, Max31790State, PidConfig, TemperatureSensor, + ChannelType, ControllerInitError, Device, FanControl, InputChannel, + Max31790State, PidConfig, TemperatureSensor, }, i2c_config::{devices, sensors}, }; pub use drv_cpu_seq_api::SeqError; use drv_cpu_seq_api::{PowerState, Sequencer, StateChangeReason}; use task_sensor_api::SensorId; -use task_thermal_api::ThermalProperties; -use userlib::{TaskId, UnwrapLite, task_slot, units::Celsius}; +use task_thermal_api::{SensorReadError, ThermalError, ThermalProperties}; +use userlib::{ + TaskId, UnwrapLite, task_slot, + units::{Celsius, PWMDuty}, +}; task_slot!(SEQ, cosmo_seq); @@ -108,21 +112,78 @@ impl Bsp { } } - // We assume Cosmo fan presence cannot change - pub fn get_fan_presence(&self) -> Result, SeqError> { - // Awkwardly build the fan array, because there's not a great way to - // build a fixed-size array from a function - let mut fans = Fans::new(); - for i in 0..NUM_FANS { - fans[i] = Some(sensors::MAX31790_SPEED_SENSORS[i]); + pub fn update_fan_presence( + &mut self, + _on_added: F, + _on_remove: G, + ) -> Result<(), SeqError> + where + F: Fn(&Fan), + G: Fn(&Fan), + { + // Our fans are always here, never added or removed! + Ok(()) + } + + pub fn read_fan_rpms( + &mut self, + mut on_success: F, + mut on_error: G, + _on_missing: H, + ) where + F: FnMut(&SensorId, f32), + G: FnMut(&SensorId, SensorReadError), + H: FnMut(&SensorId), + { + for (idx, sensor) in sensors::MAX31790_SPEED_SENSORS.iter().enumerate() + { + // TODO: Why does this use idx? + let fctrl_res = self.fan_control(Fan::from(idx)); + let fctrl = match fctrl_res { + Ok(f) => f, + Err(e) => { + on_error(sensor, SensorReadError::from(e)); + continue; + } + }; + + // TODO(AJM): Keep last fan RPM? + match fctrl.fan_rpm() { + Ok(reading) => on_success(sensor, reading.0.into()), + Err(e) => on_error(sensor, SensorReadError::I2cError(e)), + } } - Ok(fans) } - pub fn fan_sensor_id(&self, i: usize) -> SensorId { - sensors::MAX31790_SPEED_SENSORS[i] + // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, + // even if some fail. return the LAST error if any. + pub fn set_all_fan_rpms( + &mut self, + duty: PWMDuty, + ) -> Result<(), ThermalError> { + let mut last_err = Ok(()); + for idx in 0..NUM_FANS { + let fctrl_res = self.fan_control(Fan::from(idx)); + let fctrl = match fctrl_res { + Ok(f) => f, + Err(e) => { + last_err = Err(ThermalError::from(e)); + continue; + } + }; + + if fctrl.set_pwm(duty).is_err() { + last_err = Err(ThermalError::DeviceError); + } + } + + last_err } + // pub fn fan_sensor_id(&self, i: usize) -> SensorId { + // sensors::MAX31790_SPEED_SENSORS[i] + // } + pub fn new(i2c_task: TaskId) -> Self { // Initializes and build a handle to the fan controller IC let fctrl = Max31790State::new(&devices::max31790(i2c_task)[0]); diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 46c4f251b2..40dc784257 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -5,7 +5,7 @@ use core::cell::Cell; use crate::{ - Fan, ThermalError, Trace, + ThermalError, Trace, bsp::{self, Bsp, PowerBitmask}, }; use drv_i2c_api::{I2cDevice, ResponseCode}; @@ -84,46 +84,6 @@ impl TemperatureSensor { } } -/// Represents the indvidual fans in the system -/// -/// Depending on the system we have diferent numbers of fans structured in -/// different ways. Not all fans are guaranteed to be there at all times so -/// their corresponding sensor is an `Option`. We should not read the RPM of -/// fans which are not present and their PWM should only be driven low. -#[derive(Copy, Clone)] -pub struct Fans([Option; N]); - -impl core::ops::Index for Fans<{ bsp::NUM_FANS }> { - type Output = Option; - - fn index(&self, index: usize) -> &Option { - &self.0[index] - } -} - -impl core::ops::IndexMut for Fans<{ bsp::NUM_FANS }> { - fn index_mut(&mut self, index: usize) -> &mut Option { - &mut self.0[index] - } -} - -impl Fans<{ bsp::NUM_FANS }> { - pub fn new() -> Self { - Self([None; bsp::NUM_FANS]) - } - pub fn is_present(&self, index: crate::Fan) -> bool { - self.0[index.0 as usize].is_some() - } - pub fn enumerate( - &self, - ) -> impl Iterator)> { - self.0.iter().enumerate() - } - pub fn as_fans(&self) -> impl Iterator + '_ { - self.enumerate().map(|(f, _s)| Fan::from(f)) - } -} - //////////////////////////////////////////////////////////////////////////////// /// Enum representing any of our fan controller types, bound to one of their @@ -136,7 +96,7 @@ pub enum FanControl<'a> { } impl<'a> FanControl<'a> { - fn set_pwm(&self, pwm: PWMDuty) -> Result<(), ResponseCode> { + pub fn set_pwm(&self, pwm: PWMDuty) -> Result<(), ResponseCode> { match self { Self::Max31790(m, fan) => m.set_pwm(*fan, pwm), Self::Emc2305(m, fan) => m.set_pwm(*fan, pwm), @@ -474,9 +434,6 @@ pub(crate) struct ThermalControl<'a> { /// Previous value of `err_blackbox`, copied over at power-down prev_err_blackbox: &'static mut ThermalSensorErrors, - /// Fans for the system - fans: Fans<{ bsp::NUM_FANS }>, - /// Last group PWM control value last_pwm: PWMDuty, @@ -1028,7 +985,6 @@ impl<'a> ThermalControl<'a> { dynamic_inputs: [None; bsp::NUM_DYNAMIC_TEMPERATURE_INPUTS], - fans: Fans::new(), last_pwm: PWMDuty(0), err_blackbox, @@ -1122,19 +1078,13 @@ impl<'a> ThermalControl<'a> { } } - match self.bsp.get_fan_presence() { - Ok(next) => { - for fan in next.as_fans() { - if !self.fans.is_present(fan) && next.is_present(fan) { - ringbuf_entry!(Trace::FanAdded(fan)); - } else if self.fans.is_present(fan) && !next.is_present(fan) - { - ringbuf_entry!(Trace::FanRemoved(fan)); - } - } - self.fans = next; - } - Err(e) => ringbuf_entry!(Trace::FanPresenceUpdateFailed(e)), + let result = self.bsp.update_fan_presence( + |fan| ringbuf_entry!(Trace::FanAdded(*fan)), + |fan| ringbuf_entry!(Trace::FanRemoved(*fan)), + ); + + if let Err(e) = result { + ringbuf_entry!(Trace::FanPresenceUpdateFailed(e)); } } @@ -1146,33 +1096,18 @@ impl<'a> ThermalControl<'a> { /// read in `self.err_blackbox` for later investigation. pub fn read_sensors(&mut self) { // Read fan data and log it to the sensors task - for (index, sensor_id) in self.fans.enumerate() { - if let Some(sensor_id) = sensor_id { - match self - .bsp - .fan_control(Fan::from(index)) - .map_err(SensorReadError::from) - .and_then(|ctrl| { - ctrl.fan_rpm().map_err(SensorReadError::I2cError) - }) { - Ok(reading) => { - self.sensor_api.post_now(*sensor_id, reading.0.into()) - } - Err(e) => { - ringbuf_entry!(Trace::FanReadFailed(*sensor_id, e)); - self.err_blackbox.push(*sensor_id, e); - self.sensor_api.nodata_now(*sensor_id, e.into()) - } - } - } else { - // Invalidate fan speed readings in the sensors task - let sensor_id = self.bsp.fan_sensor_id(index); - self.sensor_api.nodata_now( - sensor_id, - task_sensor_api::NoData::DeviceNotPresent, - ); - } - } + self.bsp.read_fan_rpms( + |id, value| self.sensor_api.post_now(*id, value), + |id, error| { + ringbuf_entry!(Trace::FanReadFailed(*id, error)); + self.err_blackbox.push(*id, error); + self.sensor_api.nodata_now(*id, error.into()) + }, + |id| { + self.sensor_api + .nodata_now(*id, task_sensor_api::NoData::DeviceNotPresent); + }, + ); // Read miscellaneous temperature data and log it to the sensors task for s in self.bsp.misc_sensors.iter() { @@ -1678,25 +1613,7 @@ impl<'a> ThermalControl<'a> { } }; self.last_pwm = pwm; - let mut last_err = Ok(()); - for (index, sensor_id) in self.fans.enumerate() { - // If a fan is missing, keep its PWM signal low - let pwm = match sensor_id { - Some(_) => pwm, - None => PWMDuty(0), - }; - if let Err(e) = self - .bsp - .fan_control(Fan::from(index)) - .map_err(ThermalError::from) - .and_then(|fan| { - fan.set_pwm(pwm).map_err(|_| ThermalError::DeviceError) - }) - { - last_err = Err(e); - } - } - last_err + self.bsp.set_all_fan_rpms(pwm) } /// Attempts to set the PWM of every fan to whatever the previous value was. From ded5468f3c87bd989ab33baa4a4394fbed27189a Mon Sep 17 00:00:00 2001 From: James Munns Date: Wed, 22 Jul 2026 20:01:45 +0200 Subject: [PATCH 02/33] Also misc_sensors --- task/thermal/src/bsp/cosmo_ab.rs | 28 +++++++++++++++++++--------- task/thermal/src/control.rs | 26 ++++++++++++++------------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 95c9cfdb03..e396948186 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -125,16 +125,12 @@ impl Bsp { Ok(()) } - pub fn read_fan_rpms( + pub fn read_fan_rpms( &mut self, - mut on_success: F, - mut on_error: G, - _on_missing: H, - ) where - F: FnMut(&SensorId, f32), - G: FnMut(&SensorId, SensorReadError), - H: FnMut(&SensorId), - { + mut on_success: impl FnMut(&SensorId, f32), + mut on_error: impl FnMut(&SensorId, SensorReadError), + _on_missing: impl FnMut(&SensorId), + ) { for (idx, sensor) in sensors::MAX31790_SPEED_SENSORS.iter().enumerate() { // TODO: Why does this use idx? @@ -155,6 +151,20 @@ impl Bsp { } } + pub fn read_misc_sensors( + &mut self, + i2c_task: TaskId, + mut on_success: impl FnMut(&SensorId, f32), + mut on_error: impl FnMut(&SensorId, SensorReadError), + ) { + for s in self.misc_sensors.iter() { + match s.read_temp(i2c_task) { + Ok(v) => on_success(&s.sensor_id, v.0), + Err(e) => on_error(&s.sensor_id, e), + } + } + } + // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, // even if some fail. return the LAST error if any. pub fn set_all_fan_rpms( diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 40dc784257..4e44c5967e 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -54,7 +54,7 @@ pub enum Device { pub struct TemperatureSensor { device: Device, builder: fn(TaskId) -> drv_i2c_api::I2cDevice, - sensor_id: SensorId, + pub sensor_id: SensorId, } impl TemperatureSensor { @@ -70,7 +70,10 @@ impl TemperatureSensor { sensor_id, } } - fn read_temp(&self, i2c_task: TaskId) -> Result { + pub fn read_temp( + &self, + i2c_task: TaskId, + ) -> Result { let dev = (self.builder)(i2c_task); let t = match &self.device { Device::Tmp117 => Tmp117::new(&dev).read_temperature()?, @@ -1110,16 +1113,15 @@ impl<'a> ThermalControl<'a> { ); // Read miscellaneous temperature data and log it to the sensors task - for s in self.bsp.misc_sensors.iter() { - match s.read_temp(self.i2c_task) { - Ok(v) => self.sensor_api.post_now(s.sensor_id, v.0), - Err(e) => { - ringbuf_entry!(Trace::MiscReadFailed(s.sensor_id, e)); - self.err_blackbox.push(s.sensor_id, e); - self.sensor_api.nodata_now(s.sensor_id, e.into()) - } - } - } + self.bsp.read_misc_sensors( + self.i2c_task, + |id, value| self.sensor_api.post_now(*id, value), + |id, error| { + ringbuf_entry!(Trace::MiscReadFailed(*id, error)); + self.err_blackbox.push(*id, error); + self.sensor_api.nodata_now(*id, error.into()) + }, + ); // We read the power mode right before reading sensors, to avoid // potential TOCTOU issues; some sensors cannot be read if they are not From c1919e63d2d4f8cefbb7e98c6d518200892c8b5d Mon Sep 17 00:00:00 2001 From: James Munns Date: Thu, 23 Jul 2026 11:54:06 +0200 Subject: [PATCH 03/33] WIP: start working on input --- task/thermal/src/bsp/cosmo_ab.rs | 25 +++++++++- task/thermal/src/control.rs | 82 ++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 37 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index e396948186..2a00965025 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -47,7 +47,7 @@ pub const USE_CONTROLLER: bool = true; pub(crate) struct Bsp { /// Controlled sensors - pub inputs: &'static [InputChannel; NUM_TEMPERATURE_INPUTS], + pub priv_inputs: &'static [InputChannel; NUM_TEMPERATURE_INPUTS], pub dynamic_inputs: &'static [SensorId; NUM_DYNAMIC_TEMPERATURE_INPUTS], /// Monitored sensors @@ -165,6 +165,27 @@ impl Bsp { } } + pub fn read_inputs( + &mut self, + mode: PowerBitmask, + i2c_task: TaskId, + mut on_success: impl FnMut(&SensorId, f32), + mut on_error: impl FnMut(&InputChannel, SensorReadError), + mut on_unpowered: impl FnMut(&SensorId), + ) { + for s in self.priv_inputs.iter() { + if !mode.intersects(s.power_mode_mask) { + on_unpowered(&s.sensor.sensor_id); + continue; + } + + match s.sensor.read_temp(i2c_task) { + Ok(v) => on_success(&s.sensor.sensor_id, v.0), + Err(e) => on_error(s, e), + } + } + } + // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, // even if some fail. return the LAST error if any. pub fn set_all_fan_rpms( @@ -215,7 +236,7 @@ impl Bsp { max_output: 100.0, }, - inputs: &INPUTS, + priv_inputs: &INPUTS, dynamic_inputs: &[], // We monitor and log all of the air temperatures diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 4e44c5967e..76d1ca6fab 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -131,16 +131,16 @@ impl<'a> FanControl<'a> { /// particular component in the system. pub(crate) struct InputChannel { /// Temperature sensor - sensor: TemperatureSensor, + pub sensor: TemperatureSensor, /// Thermal properties of the associated component - model: ThermalProperties, + pub model: ThermalProperties, /// Mask with bits set based on the Bsp's `power_mode` bits - power_mode_mask: PowerBitmask, + pub power_mode_mask: PowerBitmask, /// Channel type - ty: ChannelType, + pub ty: ChannelType, } #[derive(Copy, Clone, Eq, PartialEq)] @@ -1126,41 +1126,41 @@ impl<'a> ThermalControl<'a> { // We read the power mode right before reading sensors, to avoid // potential TOCTOU issues; some sensors cannot be read if they are not // powered. + let unexpected_failure = |s: &InputChannel, e: SensorReadError| { + let removable = matches!( + s.ty, + ChannelType::Removable | ChannelType::RemovableAndErrorProne + ); + let removed = + e == SensorReadError::I2cError(ResponseCode::NoDevice); + !(removable && removed) + }; + let power_mode = self.bsp.power_mode(); - for s in self.bsp.inputs.iter() { - if power_mode.intersects(s.power_mode_mask) { - match s.sensor.read_temp(self.i2c_task) { - Ok(v) => self.sensor_api.post_now(s.sensor.sensor_id, v.0), - Err(e) => { - // Record an error errors if the sensor is not removable - // or we get a unexpected error from a removable sensor - if !(matches!( - s.ty, - ChannelType::Removable - | ChannelType::RemovableAndErrorProne - ) && e - == SensorReadError::I2cError( - ResponseCode::NoDevice, - )) - { - ringbuf_entry!(Trace::SensorReadFailed( - s.sensor.sensor_id, - e - )); - self.err_blackbox.push(s.sensor.sensor_id, e); - } - self.sensor_api.nodata_now(s.sensor.sensor_id, e.into()) - } + + self.bsp.read_inputs( + power_mode, + self.i2c_task, + |id, value| self.sensor_api.post_now(*id, value), + |s, error| { + // Record an error errors if the sensor is not removable + // or we get a unexpected error from a removable sensor + if unexpected_failure(s, error) { + ringbuf_entry!(Trace::SensorReadFailed( + s.sensor.sensor_id, + error + )); + self.err_blackbox.push(s.sensor.sensor_id, error); } - } else { + self.sensor_api.nodata_now(s.sensor.sensor_id, error.into()) + }, + |id| { // If the device isn't supposed to be on in the current power // state, then record it as Off in the sensors task. - self.sensor_api.nodata_now( - s.sensor.sensor_id, - task_sensor_api::NoData::DeviceOff, - ) - } - } + self.sensor_api + .nodata_now(*id, task_sensor_api::NoData::DeviceOff); + }, + ); // Note that this function does not send data about dynamic temperature // inputs to the `sensors` task! This is because we don't know what @@ -1191,6 +1191,18 @@ impl<'a> ThermalControl<'a> { // in `self.state`. When we're in the `Boot` state, this will leave the // value as `None`; when we're `Running`, it will maintain the previous // state, estimating a new temperature with the thermal model. + self.bsp.read_inputs( + self.power_mode, + self.i2c_task, + |id, value| { + self.state.write_temperature( + todo!("idx, not id!"), + todo!("reading, not f32!"), + ) + }, + todo!(), + todo!(), + ); for (i, s) in self.bsp.inputs.iter().enumerate() { if self.power_mode.intersects(s.power_mode_mask) { let sensor_id = s.sensor.sensor_id; From 45e7f1e5aa6a83ce70adeb1a9a3ae50c1c2baa56 Mon Sep 17 00:00:00 2001 From: James Munns Date: Thu, 23 Jul 2026 14:13:59 +0200 Subject: [PATCH 04/33] WIP: Extract state to BSP --- task/sensor-api/src/lib.rs | 12 +- task/thermal/src/bsp/cosmo_ab.rs | 131 ++++++++-- task/thermal/src/control.rs | 407 +++++++++++++------------------ 3 files changed, 295 insertions(+), 255 deletions(-) diff --git a/task/sensor-api/src/lib.rs b/task/sensor-api/src/lib.rs index 6801a212e2..6d2ce98e59 100644 --- a/task/sensor-api/src/lib.rs +++ b/task/sensor-api/src/lib.rs @@ -221,14 +221,18 @@ impl From for SensorError { impl Sensor { /// Post the given data with a timestamp of now #[inline] - pub fn post_now(&self, id: SensorId, value: f32) { - self.post(id, value, sys_get_timer().now) + pub fn post_now(&self, id: SensorId, value: f32) -> u64 { + let now = sys_get_timer().now; + self.post(id, value, now); + now } /// Post the given `NoData` error with a timestamp of now #[inline] - pub fn nodata_now(&self, id: SensorId, nodata: NoData) { - self.nodata(id, nodata, sys_get_timer().now) + pub fn nodata_now(&self, id: SensorId, nodata: NoData) -> u64 { + let now = sys_get_timer().now; + self.nodata(id, nodata, now); + now } } diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 2a00965025..327ca33956 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -7,14 +7,15 @@ use crate::{ Fan, control::{ - ChannelType, ControllerInitError, Device, FanControl, InputChannel, - Max31790State, PidConfig, TemperatureSensor, + ChannelType, ControllerInitError, Device, DynamicInputChannel, + FanControl, InputChannel, Max31790State, PidConfig, TemperatureReading, + TemperatureSensor, TimestampedSensorError, }, i2c_config::{devices, sensors}, }; pub use drv_cpu_seq_api::SeqError; use drv_cpu_seq_api::{PowerState, Sequencer, StateChangeReason}; -use task_sensor_api::SensorId; +use task_sensor_api::{Sensor, SensorId}; use task_thermal_api::{SensorReadError, ThermalError, ThermalProperties}; use userlib::{ TaskId, UnwrapLite, task_slot, @@ -47,8 +48,9 @@ pub const USE_CONTROLLER: bool = true; pub(crate) struct Bsp { /// Controlled sensors - pub priv_inputs: &'static [InputChannel; NUM_TEMPERATURE_INPUTS], - pub dynamic_inputs: &'static [SensorId; NUM_DYNAMIC_TEMPERATURE_INPUTS], + pub priv_inputs: &'static mut [InputChannel; NUM_TEMPERATURE_INPUTS], + pub dynamic_inputs: + &'static mut [DynamicInputChannel; NUM_DYNAMIC_TEMPERATURE_INPUTS], /// Monitored sensors pub misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], @@ -127,9 +129,9 @@ impl Bsp { pub fn read_fan_rpms( &mut self, - mut on_success: impl FnMut(&SensorId, f32), - mut on_error: impl FnMut(&SensorId, SensorReadError), - _on_missing: impl FnMut(&SensorId), + mut on_success: impl FnMut(&SensorId, f32) -> u64, + mut on_error: impl FnMut(&SensorId, SensorReadError) -> u64, + _on_missing: impl FnMut(&SensorId) -> u64, ) { for (idx, sensor) in sensors::MAX31790_SPEED_SENSORS.iter().enumerate() { @@ -147,21 +149,21 @@ impl Bsp { match fctrl.fan_rpm() { Ok(reading) => on_success(sensor, reading.0.into()), Err(e) => on_error(sensor, SensorReadError::I2cError(e)), - } + }; } } pub fn read_misc_sensors( &mut self, i2c_task: TaskId, - mut on_success: impl FnMut(&SensorId, f32), - mut on_error: impl FnMut(&SensorId, SensorReadError), + mut on_success: impl FnMut(&SensorId, f32) -> u64, + mut on_error: impl FnMut(&SensorId, SensorReadError) -> u64, ) { for s in self.misc_sensors.iter() { match s.read_temp(i2c_task) { Ok(v) => on_success(&s.sensor_id, v.0), Err(e) => on_error(&s.sensor_id, e), - } + }; } } @@ -169,10 +171,16 @@ impl Bsp { &mut self, mode: PowerBitmask, i2c_task: TaskId, - mut on_success: impl FnMut(&SensorId, f32), - mut on_error: impl FnMut(&InputChannel, SensorReadError), - mut on_unpowered: impl FnMut(&SensorId), + mut on_success: impl FnMut(&SensorId, f32) -> u64, + mut on_error: impl FnMut(&InputChannel, SensorReadError) -> u64, + mut on_unpowered: impl FnMut(&SensorId) -> u64, ) { + // TODO(AJM): For some reason, we make two passes. The first hydrates + // data so that we can send it to the sensor API. This comes from the + // call to `read_sensors()`. We don't record the state here, as we only + // do that when we call `run_control()`. This has the interesting side + // effect that in ThermalMode::Manual and ThermalMode::Off, we don't + // remember the temperatures we've seen. for s in self.priv_inputs.iter() { if !mode.intersects(s.power_mode_mask) { on_unpowered(&s.sensor.sensor_id); @@ -182,10 +190,94 @@ impl Bsp { match s.sensor.read_temp(i2c_task) { Ok(v) => on_success(&s.sensor.sensor_id, v.0), Err(e) => on_error(s, e), - } + }; } } + // TODO: This might go away + pub fn read_inputs_back_from_sensor_api( + &mut self, + _mode: PowerBitmask, + _sensor_api: &Sensor, + _on_success: impl FnMut(&SensorId, f32), + _on_error: impl FnMut(&InputChannel, SensorReadError), + _on_unpowered: impl FnMut(&SensorId), + ) { + let x = _sensor_api.get_reading(todo!()).unwrap(); + todo!() + } + + // TODO: This probably needs to exist, but for cosmo we have no dynamic + // inputs to read back + pub fn read_dynamic_inputs_back_from_sensor_api( + &mut self, + _sensor_api: Sensor, + ) { + // // The dynamic inputs don't depend on power mode; instead, they are + // // assumed to be present when a model exists in `self.dynamic_inputs`; + // // this model is set by external callers using + // // `update_dynamic_input` and `remove_dynamic_input`. + // for (i, sensor_id) in self.bsp.dynamic_inputs.iter().enumerate() { + // // + // // TODO(AJM): This is important to note! The index is inputs + i. + // // This *might* not matter when `state` isn't the one handling this. + // // + // // let index = i + self.bsp.inputs.len(); + // let index = todo!(); + // match self.dynamic_inputs[i] { + // Some(..) => { + // if let Ok(r) = self.sensor_api.get_reading(*sensor_id) { + // self.state.write_temperature(index, r); + // } + // } + // None => self.state.write_temperature_inactive(index), + // } + // } + } + + // returns Ok(true) if this was a new input + pub fn update_dynamic_input( + &mut self, + index: usize, + model: ThermalProperties, + ) -> Result { + // No dynamic inputs here, todo: static assert this + Err(ThermalError::InvalidIndex) + } + + // sets last_reading to Some(Missing), returns sensor id + pub fn remove_dynamic_input( + &mut self, + index: usize, + ) -> Result { + // No dynamic inputs here, todo: static assert this + Err(ThermalError::InvalidIndex) + } + + pub fn all_inputs_present(&self) -> bool { + self.priv_inputs.iter().all(|i| i.last_reading.is_some()) + } + + // Visit all temperature sensors, first the inputs, then the dynamic_inputs. + // Inputs and Dynamic Inputs that are missing will be skipped. + pub fn for_each_temp_allow_missing_inputs( + &self, + f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), + ) { + todo!() + } + + // Visit all temperature sensors, first the inputs, then the dynamic_inputs. + // All inputs MUST have a previous reading or this will panic, though the + // readings may be allowed to be Missing if the model allows it. Dynamic + // inputs that are not present will be skipped. + pub fn for_each_temp( + &self, + f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), + ) { + todo!() + } + // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, // even if some fail. return the LAST error if any. pub fn set_all_fan_rpms( @@ -221,6 +313,9 @@ impl Bsp { // Handle for the sequencer task, which we check for power state let seq = Sequencer::from(SEQ.get_task_id()); + static INPUTS_ONCE: static_cell::ClaimOnceCell< + [InputChannel; NUM_TEMPERATURE_INPUTS], + > = static_cell::ClaimOnceCell::new(INPUTS); Self { seq, @@ -236,8 +331,8 @@ impl Bsp { max_output: 100.0, }, - priv_inputs: &INPUTS, - dynamic_inputs: &[], + priv_inputs: INPUTS_ONCE.claim(), + dynamic_inputs: &mut [], // We monitor and log all of the air temperatures misc_sensors: &MISC_SENSORS, diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 76d1ca6fab..29fcaad5cf 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -141,6 +141,8 @@ pub(crate) struct InputChannel { /// Channel type pub ty: ChannelType, + + pub last_reading: Option, } #[derive(Copy, Clone, Eq, PartialEq)] @@ -196,6 +198,7 @@ impl InputChannel { model, power_mode_mask, ty, + last_reading: None, } } } @@ -214,7 +217,9 @@ impl InputChannel { /// depending on what's plugged in. #[derive(Clone, Copy)] pub(crate) struct DynamicInputChannel { - model: ThermalProperties, + pub sensor_id: SensorId, + pub model: ThermalProperties, + pub last_reading: Option, } //////////////////////////////////////////////////////////////////////////////// @@ -421,12 +426,6 @@ pub(crate) struct ThermalControl<'a> { /// PID parameters, pulled from the BSP by default but user-modifiable pid_config: PidConfig, - /// Dynamic inputs are fixed in number but configured at runtime. - /// - /// `None` values in this list are ignored. - dynamic_inputs: - [Option; bsp::NUM_DYNAMIC_TEMPERATURE_INPUTS], - /// Records details on the first sensor read failures since the thermal loop /// entered the `Uncontrollable` state and the system was powered off. /// @@ -451,7 +450,7 @@ pub(crate) struct ThermalControl<'a> { /// Represents the state of a temperature sensor, which either has a valid /// reading or is marked as inactive (due to power state or being missing) #[derive(Copy, Clone, Debug)] -enum TemperatureReading { +pub enum TemperatureReading { /// Normal reading, timestamped using monotonic system time Valid(TimestampedTemperatureReading), @@ -693,13 +692,10 @@ enum ThermalControlState { /// /// (dynamic sensors must report in *if* they are present, i.e. not `None` /// in the `dynamic_inputs` array) - Boot { values: OptionalTemperatureArray }, + Boot, /// Normal happy control loop - Running { - values: TemperatureArray, - pid: OneSidedPidState, - }, + Running { pid: OneSidedPidState }, // // === Overheated control regime states === @@ -708,7 +704,6 @@ enum ThermalControlState { /// critical temperature ranges. We turn on fans at high power and record /// the time at which we entered this state. Critical { - values: TemperatureArray, /// The time at which we transitioned to the `Critical` state *this* /// time, either from `Running` or from FAN PARTY!!!. start_time: u64, @@ -724,28 +719,26 @@ enum ThermalControlState { /// This gives us an opportunity to recover from overheating by running the /// fans aggressively without also deciding to give up and kill ourselves /// while things are improving but not fast enough. - FanParty { values: TemperatureArray }, + FanParty, /// The system cannot control the temperature; power down and wait for /// intervention from higher up the stack. - Uncontrollable( - // we keep the values array around to avoid losing ownership - OptionalTemperatureArray, - ), + Uncontrollable, } impl ThermalControlState { /// Sets all temperature readings to `None` and returns the array fn reset_values(&mut self) -> OptionalTemperatureArray { - match self { - ThermalControlState::Boot { values } - | ThermalControlState::Uncontrollable(values) => { - values.reset_values() - } - ThermalControlState::Running { values, .. } - | ThermalControlState::Critical { values, .. } - | ThermalControlState::FanParty { values } => values.reset_values(), - } + todo!() + // match self { + // ThermalControlState::Boot { values } + // | ThermalControlState::Uncontrollable(values) => { + // values.reset_values() + // } + // ThermalControlState::Running { values, .. } + // | ThermalControlState::Critical { values, .. } + // | ThermalControlState::FanParty { values } => values.reset_values(), + // } } } @@ -809,17 +802,6 @@ mod temperature_array { } } - /// Temperature state iterator with `Option` values - pub fn zip_temperatures<'a>( - &'a self, - bsp: &'a Bsp, - dynamic_channels: &'a DynamicChannelsArray, - ) -> impl Iterator< - Item = (SensorId, Option, ThermalProperties), - > + use<'a> { - zip_temperatures(bsp, self.0, dynamic_channels, |v| v) - } - /// Sets the temperature at index `i` to a value /// /// # Panics @@ -851,50 +833,51 @@ mod temperature_array { self.0[i].set(Some(value)) } - /// Temperature state iterator with `TemperatureReading` values - pub fn zip_temperatures<'a>( - &'a self, - bsp: &'a Bsp, - dynamic_channels: &'a DynamicChannelsArray, - ) -> impl Iterator< - Item = (SensorId, TemperatureReading, ThermalProperties), - > + use<'a> { - zip_temperatures(bsp, self.0, dynamic_channels, |v| v.unwrap_lite()) - } + // /// Temperature state iterator with `TemperatureReading` values + // pub fn zip_temperatures<'a>( + // &'a self, + // bsp: &'a Bsp, + // dynamic_channels: &'a DynamicChannelsArray, + // ) -> impl Iterator< + // Item = (SensorId, TemperatureReading, ThermalProperties), + // > + use<'a> { + // zip_temperatures(bsp, self.0, dynamic_channels, |v| v.unwrap_lite()) + // } } - /// Returns an iterator over tuples of `(sensor_id, value, thermal model)` - /// - /// The `values` array contains static and dynamic values (in order); - /// this function will panic if sizes are mismatched. - /// - /// Every dynamic input is represented by an `Option`. - /// If the input is not present right now, it will be `None`, but will - /// continue to take up space to preserve ordering. - /// - /// In cases where dynamic inputs are not present (i.e. they are `None` in - /// the array), the iterator will skip that entire tuple. - fn zip_temperatures<'a, U: 'static>( - bsp: &'a Bsp, - values: &'a RawTemperatureArray, - dynamic_channels: &'a DynamicChannelsArray, - f: fn(Option) -> U, - ) -> impl Iterator + use<'a, U> - { - assert_eq!(values.len(), bsp.inputs.len() + bsp.dynamic_inputs.len()); - assert_eq!(bsp.dynamic_inputs.len(), dynamic_channels.len()); - bsp.inputs - .iter() - .map(|i| Some((i.sensor.sensor_id, i.model))) - .chain( - dynamic_channels - .iter() - .zip(bsp.dynamic_inputs.iter().cloned()) - .map(|(i, s)| i.map(|i| (s, i.model))), - ) - .zip(values.iter().map(move |v| f(v.get()))) - .filter_map(|(model, v)| model.map(|(id, t)| (id, v, t))) - } + // /// Returns an iterator over tuples of `(sensor_id, value, thermal model)` + // /// + // /// The `values` array contains static and dynamic values (in order); + // /// this function will panic if sizes are mismatched. + // /// + // /// Every dynamic input is represented by an `Option`. + // /// If the input is not present right now, it will be `None`, but will + // /// continue to take up space to preserve ordering. + // /// + // /// In cases where dynamic inputs are not present (i.e. they are `None` in + // /// the array), the iterator will skip that entire tuple. + // fn zip_temperatures<'a, U: 'static>( + // bsp: &'a Bsp, + // values: &'a RawTemperatureArray, + // dynamic_channels: &'a DynamicChannelsArray, + // f: fn(Option) -> U, + // ) -> impl Iterator + use<'a, U> + // { + // // assert_eq!(values.len(), bsp.inputs.len() + bsp.dynamic_inputs.len()); + // assert_eq!(bsp.dynamic_inputs.len(), dynamic_channels.len()); + // // bsp.inputs + // // .iter() + // // .map(|i| Some((i.sensor.sensor_id, i.model))) + // // .chain( + // // dynamic_channels + // // .iter() + // // .zip(bsp.dynamic_inputs.iter().cloned()) + // // .map(|(i, s)| i.map(|i| (s, i.model))), + // // ) + // // .zip(values.iter().map(move |v| f(v.get()))) + // // .filter_map(|(model, v)| model.map(|(id, t)| (id, v, t))) + // todo!() + // } } use temperature_array::{ OptionalTemperatureArray, RawTemperatureArray, TemperatureArray, @@ -912,35 +895,37 @@ struct OverheatTimer { impl ThermalControlState { fn write_temperature(&mut self, index: usize, reading: Reading) { - let r = TemperatureReading::Valid(TimestampedTemperatureReading { - time_ms: reading.timestamp, - value: Celsius(reading.value), - }); - match self { - ThermalControlState::Boot { values } => { - values.set(index, r); - } - ThermalControlState::Running { values, .. } - | ThermalControlState::Critical { values, .. } - | ThermalControlState::FanParty { values, .. } => { - values.set(index, r); - } - ThermalControlState::Uncontrollable(..) => (), - } + todo!() + // let r = TemperatureReading::Valid(TimestampedTemperatureReading { + // time_ms: reading.timestamp, + // value: Celsius(reading.value), + // }); + // match self { + // ThermalControlState::Boot { values } => { + // values.set(index, r); + // } + // ThermalControlState::Running { values, .. } + // | ThermalControlState::Critical { values, .. } + // | ThermalControlState::FanParty { values, .. } => { + // values.set(index, r); + // } + // ThermalControlState::Uncontrollable(..) => (), + // } } fn write_temperature_inactive(&mut self, index: usize) { - match self { - ThermalControlState::Boot { values } => { - values.set(index, TemperatureReading::Inactive); - } - ThermalControlState::Running { values, .. } - | ThermalControlState::Critical { values, .. } - | ThermalControlState::FanParty { values, .. } => { - values.set(index, TemperatureReading::Inactive); - } - ThermalControlState::Uncontrollable(..) => (), - } + todo!() + // match self { + // ThermalControlState::Boot { values } => { + // values.set(index, TemperatureReading::Inactive); + // } + // ThermalControlState::Running { values, .. } + // | ThermalControlState::Critical { values, .. } + // | ThermalControlState::FanParty { values, .. } => { + // values.set(index, TemperatureReading::Inactive); + // } + // ThermalControlState::Uncontrollable(..) => (), + // } } } @@ -979,15 +964,11 @@ impl<'a> ThermalControl<'a> { i2c_task, sensor_api, target_margin: Celsius(0.0f32), - state: ThermalControlState::Boot { - values: OptionalTemperatureArray::new(data), - }, + state: ThermalControlState::Boot, pid_config, power_mode: PowerBitmask::empty(), // no sensors active - dynamic_inputs: [None; bsp::NUM_DYNAMIC_TEMPERATURE_INPUTS], - last_pwm: PWMDuty(0), err_blackbox, @@ -1057,7 +1038,7 @@ impl<'a> ThermalControl<'a> { /// Resets the control state fn reset_state(&mut self) { let values = self.state.reset_values(); - self.state = ThermalControlState::Boot { values }; + self.state = ThermalControlState::Boot; ringbuf_entry!(Trace::AutoState(self.get_state())); } @@ -1108,7 +1089,7 @@ impl<'a> ThermalControl<'a> { }, |id| { self.sensor_api - .nodata_now(*id, task_sensor_api::NoData::DeviceNotPresent); + .nodata_now(*id, task_sensor_api::NoData::DeviceNotPresent) }, ); @@ -1158,7 +1139,7 @@ impl<'a> ThermalControl<'a> { // If the device isn't supposed to be on in the current power // state, then record it as Off in the sensors task. self.sensor_api - .nodata_now(*id, task_sensor_api::NoData::DeviceOff); + .nodata_now(*id, task_sensor_api::NoData::DeviceOff) }, ); @@ -1191,69 +1172,60 @@ impl<'a> ThermalControl<'a> { // in `self.state`. When we're in the `Boot` state, this will leave the // value as `None`; when we're `Running`, it will maintain the previous // state, estimating a new temperature with the thermal model. - self.bsp.read_inputs( + self.bsp.read_inputs_back_from_sensor_api( self.power_mode, - self.i2c_task, + &self.sensor_api, |id, value| { self.state.write_temperature( todo!("idx, not id!"), todo!("reading, not f32!"), ) }, - todo!(), - todo!(), + |_, _| todo!(), + |_| todo!(), ); - for (i, s) in self.bsp.inputs.iter().enumerate() { - if self.power_mode.intersects(s.power_mode_mask) { - let sensor_id = s.sensor.sensor_id; - let r = self.sensor_api.get_reading(sensor_id); - match r { - Ok(r) => { - self.state.write_temperature(i, r); - } - Err(SensorError::NotPresent) - if s.ty == ChannelType::Removable => - { - // Ignore errors if the sensor is removable and the - // error indicates that it's not present. - self.state.write_temperature_inactive(i); - } - Err(_) if s.ty == ChannelType::RemovableAndErrorProne => { - // Ignore all errors if this device is error-prone - self.state.write_temperature_inactive(i); - } - Err(_) => (), - } - } else { - self.state.write_temperature_inactive(i); - } - } + todo!(); + // for (i, s) in self.bsp.inputs.iter().enumerate() { + // if self.power_mode.intersects(s.power_mode_mask) { + // let sensor_id = s.sensor.sensor_id; + // let r = self.sensor_api.get_reading(sensor_id); + // match r { + // Ok(r) => { + // self.state.write_temperature(i, r); + // } + // Err(SensorError::NotPresent) + // if s.ty == ChannelType::Removable => + // { + // // Ignore errors if the sensor is removable and the + // // error indicates that it's not present. + // self.state.write_temperature_inactive(i); + // } + // Err(_) if s.ty == ChannelType::RemovableAndErrorProne => { + // // Ignore all errors if this device is error-prone + // self.state.write_temperature_inactive(i); + // } + // Err(_) => (), + // } + // } else { + // self.state.write_temperature_inactive(i); + // } + // } // The dynamic inputs don't depend on power mode; instead, they are // assumed to be present when a model exists in `self.dynamic_inputs`; // this model is set by external callers using // `update_dynamic_input` and `remove_dynamic_input`. - for (i, sensor_id) in self.bsp.dynamic_inputs.iter().enumerate() { - let index = i + self.bsp.inputs.len(); - match self.dynamic_inputs[i] { - Some(..) => { - if let Ok(r) = self.sensor_api.get_reading(*sensor_id) { - self.state.write_temperature(index, r); - } - } - None => self.state.write_temperature_inactive(index), - } - } + self.bsp + .read_dynamic_inputs_back_from_sensor_api(self.sensor_api); let control_result = match &mut self.state { - ThermalControlState::Boot { values } => { + ThermalControlState::Boot => { let mut any_power_down = None; let mut worst_margin = f32::MAX; - for (sensor_id, v, model) in - values.zip_temperatures(self.bsp, &self.dynamic_inputs) - { + + let mut f = |sensor_id, v, model| { match v { - Some(TemperatureReading::Valid(v)) => { + TemperatureReading::Valid(v) => { let worst_case = v.worst_case(now_ms, &model); let temperature = worst_case.worst_case_temp; if model.should_power_down(temperature) { @@ -1262,26 +1234,25 @@ impl<'a> ThermalControl<'a> { worst_margin = worst_margin.min(model.margin(temperature).0); } - Some(TemperatureReading::Inactive) => { + TemperatureReading::Inactive => { // Inactive sensors are ignored, but do not gate us // from transitioning to `Running` } - - None => (), } - } + }; + self.bsp.for_each_temp_allow_missing_inputs(f); if let Some(due_to) = any_power_down { self.transition_to_uncontrollable_due_to(due_to, now_ms) - } else if let Some(values) = values.as_temperature_array() { - self.transition_to_running(worst_margin, now_ms, values) + } else if self.bsp.all_inputs_present() { + self.transition_to_running(worst_margin, now_ms) } else { ControlResult::Pwm(PWMDuty( self.pid_config.max_output as u8, )) } } - ThermalControlState::Running { values, pid } => { + ThermalControlState::Running { pid } => { let mut any_power_down = None; let mut any_critical = None; let mut worst_margin = f32::MAX; @@ -1290,9 +1261,7 @@ impl<'a> ThermalControl<'a> { // below their max temperature; negative means someone is // overheating. We want to pick the _smallest_ margin, since // that's the part which is most overheated. - for (sensor_id, v, model) in - values.zip_temperatures(self.bsp, &self.dynamic_inputs) - { + let mut f = |sensor_id, v, model| { if let TemperatureReading::Valid(v) = v { let worst_case = v.worst_case(now_ms, &model); let temperature = worst_case.worst_case_temp; @@ -1306,13 +1275,13 @@ impl<'a> ThermalControl<'a> { worst_margin = worst_margin.min(model.margin(temperature).0); } - } + }; + self.bsp.for_each_temp(f); if let Some(due_to) = any_power_down { self.transition_to_uncontrollable_due_to(due_to, now_ms) } else if let Some(due_to) = any_critical { - let values = *values; - self.transition_to_critical(due_to, now_ms, values) + self.transition_to_critical(due_to, now_ms) } else { // We adjust the worst component margin by our target // margin, which must be > 0. This effectively tells the @@ -1330,15 +1299,12 @@ impl<'a> ThermalControl<'a> { ControlResult::Pwm(PWMDuty(pwm as u8)) } } - ThermalControlState::Critical { values, .. } => { + ThermalControlState::Critical { .. } => { let mut all_nominal = true; let mut any_still_critical = false; let mut any_power_down = None; let mut worst_margin = f32::MAX; - - for (sensor_id, v, model) in - values.zip_temperatures(self.bsp, &self.dynamic_inputs) - { + let mut f = |sensor_id, v, model| { if let TemperatureReading::Valid(v) = v { let worst_case = v.worst_case(now_ms, &model); let temperature = worst_case.worst_case_temp; @@ -1350,35 +1316,30 @@ impl<'a> ThermalControl<'a> { worst_margin = worst_margin.min(model.margin(temperature).0); } - } + }; if let Some(due_to) = any_power_down { self.transition_to_uncontrollable_due_to(due_to, now_ms) } else if all_nominal { - let values = *values; - self.transition_to_running(worst_margin, now_ms, values) + self.transition_to_running(worst_margin, now_ms) } else if !any_still_critical { // If all temperatures have gone below critical, but are // still above nominal, stop the overheat timeout but // continue running at 100% PWM until things go below // nominal. - let values = *values; - self.transition_to_fan_party(now_ms, values) + self.transition_to_fan_party(now_ms) } else { ControlResult::Pwm(PWMDuty( self.pid_config.max_output as u8, )) } } - ThermalControlState::FanParty { values } => { + ThermalControlState::FanParty => { let mut all_nominal = true; let mut any_power_down = None; let mut any_critical = None; let mut worst_margin = f32::MAX; - - for (sensor_id, v, model) in - values.zip_temperatures(self.bsp, &self.dynamic_inputs) - { + let mut f = |sensor_id, v, model| { if let TemperatureReading::Valid(v) = v { let worst_case = v.worst_case(now_ms, &model); let temperature = worst_case.worst_case_temp; @@ -1392,25 +1353,23 @@ impl<'a> ThermalControl<'a> { worst_margin = worst_margin.min(model.margin(temperature).0); } - } + }; if let Some(due_to) = any_power_down { self.transition_to_uncontrollable_due_to(due_to, now_ms) } else if let Some(due_to) = any_critical { // If anything's gone over critical, transition back to the // `Critical` state. - let values = *values; - self.transition_to_critical(due_to, now_ms, values) + self.transition_to_critical(due_to, now_ms) } else if all_nominal { - let values = *values; - self.transition_to_running(worst_margin, now_ms, values) + self.transition_to_running(worst_margin, now_ms) } else { ControlResult::Pwm(PWMDuty( self.pid_config.max_output as u8, )) } } - ThermalControlState::Uncontrollable(..) => ControlResult::PowerDown, + ThermalControlState::Uncontrollable => ControlResult::PowerDown, }; match control_result { @@ -1441,7 +1400,6 @@ impl<'a> ThermalControl<'a> { &mut self, worst_margin: f32, now_ms: u64, - values: TemperatureArray, ) -> ControlResult { self.record_leaving_critical(now_ms); self.record_leaving_overheat(now_ms); @@ -1451,7 +1409,7 @@ impl<'a> ThermalControl<'a> { let mut pid = OneSidedPidState::default(); let pwm = pid.run(&self.pid_config, self.target_margin.0 - worst_margin); - self.state = ThermalControlState::Running { values, pid }; + self.state = ThermalControlState::Running { pid }; ringbuf_entry!(Trace::AutoState(self.get_state())); ControlResult::Pwm(PWMDuty(pwm as u8)) @@ -1463,7 +1421,6 @@ impl<'a> ThermalControl<'a> { &mut self, (sensor_id, worst_case): (SensorId, WorstCaseTemperature), now_ms: u64, - values: TemperatureArray, ) -> ControlResult { let WorstCaseTemperature { worst_case_temp, @@ -1479,10 +1436,7 @@ impl<'a> ThermalControl<'a> { temperature: last_reading, age_s, }); - self.state = ThermalControlState::Critical { - values, - start_time: now_ms, - }; + self.state = ThermalControlState::Critical { start_time: now_ms }; ringbuf_entry!(Trace::AutoState(self.get_state())); if self.overheat_timer.is_none() { self.overheat_timer = Some(OverheatTimer { @@ -1497,13 +1451,9 @@ impl<'a> ThermalControl<'a> { /// Transition the control state to `FanParty` (from `Critical`), in /// response to all component temperatures dropping below their critical /// thresholds. - fn transition_to_fan_party( - &mut self, - now_ms: u64, - values: TemperatureArray, - ) -> ControlResult { + fn transition_to_fan_party(&mut self, now_ms: u64) -> ControlResult { self.record_leaving_critical(now_ms); - self.state = ThermalControlState::FanParty { values }; + self.state = ThermalControlState::FanParty; ringbuf_entry!(Trace::AutoState(self.get_state())); // It's PARTY TIME!!!! @@ -1547,8 +1497,8 @@ impl<'a> ThermalControl<'a> { self.record_leaving_critical(now_ms); self.record_leaving_overheat(now_ms); - let values = self.state.reset_values(); - self.state = ThermalControlState::Uncontrollable(values); + let _values = self.state.reset_values(); + self.state = ThermalControlState::Uncontrollable; ringbuf_entry!(Trace::AutoState(self.get_state())); ControlResult::PowerDown @@ -1655,13 +1605,13 @@ impl<'a> ThermalControl<'a> { pub fn get_state(&self) -> ThermalAutoState { match self.state { - ThermalControlState::Boot { .. } => ThermalAutoState::Boot, + ThermalControlState::Boot => ThermalAutoState::Boot, ThermalControlState::Running { .. } => ThermalAutoState::Running, ThermalControlState::Critical { .. } => ThermalAutoState::Critical, - ThermalControlState::Uncontrollable(..) => { + ThermalControlState::Uncontrollable => { ThermalAutoState::Uncontrollable } - ThermalControlState::FanParty { .. } => ThermalAutoState::FanParty, + ThermalControlState::FanParty => ThermalAutoState::FanParty, } } @@ -1670,16 +1620,15 @@ impl<'a> ThermalControl<'a> { index: usize, model: ThermalProperties, ) -> Result<(), ThermalError> { - #[allow(clippy::absurd_extreme_comparisons)] - if index >= bsp::NUM_DYNAMIC_TEMPERATURE_INPUTS { - return Err(ThermalError::InvalidIndex); - } // If we're adding a new dynamic input, then reset the state to `Boot`, // ensuring that we'll wait for that channel to provide us with at least // one valid reading before resuming the PID loop. - if self.dynamic_inputs[index].is_none() { + // + // TODO(AJM): We just ignore it if there was already a dynamic input + // there already? + let is_new = self.bsp.update_dynamic_input(index, model)?; + if is_new { ringbuf_entry!(Trace::AddedDynamicInput(index)); - self.dynamic_inputs[index] = Some(DynamicInputChannel { model }); self.reset_state(); } Ok(()) @@ -1689,20 +1638,12 @@ impl<'a> ThermalControl<'a> { &mut self, index: usize, ) -> Result<(), ThermalError> { - #[allow(clippy::absurd_extreme_comparisons)] - if index >= bsp::NUM_DYNAMIC_TEMPERATURE_INPUTS { - Err(ThermalError::InvalidIndex) - } else { - ringbuf_entry!(Trace::RemovedDynamicInput(index)); - self.dynamic_inputs[index] = None; - - // Post this reading to the sensors task as well - let sensor_id = self.bsp.dynamic_inputs[index]; - self.sensor_api.nodata_now( - sensor_id, - task_sensor_api::NoData::DeviceNotPresent, - ); - Ok(()) - } + let sensor_id = self.bsp.remove_dynamic_input(index)?; + ringbuf_entry!(Trace::RemovedDynamicInput(index)); + + // Post this reading to the sensors task as well + self.sensor_api + .nodata_now(sensor_id, task_sensor_api::NoData::DeviceNotPresent); + Ok(()) } } From e3fb65d6db00bca1c1574b02e138731bea66d065 Mon Sep 17 00:00:00 2001 From: James Munns Date: Thu, 23 Jul 2026 15:17:39 +0200 Subject: [PATCH 05/33] Bring us back down to even --- task/thermal/src/bsp/cosmo_ab.rs | 167 +++++++++------- task/thermal/src/control.rs | 322 ++++--------------------------- 2 files changed, 136 insertions(+), 353 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 327ca33956..f0e60d5f3e 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -7,15 +7,15 @@ use crate::{ Fan, control::{ - ChannelType, ControllerInitError, Device, DynamicInputChannel, - FanControl, InputChannel, Max31790State, PidConfig, TemperatureReading, - TemperatureSensor, TimestampedSensorError, + ChannelType, ControllerInitError, Device, FanControl, InputChannel, + Max31790State, PidConfig, TemperatureReading, TemperatureSensor, + TimestampedTemperatureReading, unexpected_failure, }, i2c_config::{devices, sensors}, }; pub use drv_cpu_seq_api::SeqError; use drv_cpu_seq_api::{PowerState, Sequencer, StateChangeReason}; -use task_sensor_api::{Sensor, SensorId}; +use task_sensor_api::{NoData, Sensor, SensorError, SensorId}; use task_thermal_api::{SensorReadError, ThermalError, ThermalProperties}; use userlib::{ TaskId, UnwrapLite, task_slot, @@ -33,27 +33,22 @@ const NUM_NVME_BMC_TEMPERATURE_SENSORS: usize = // The control loop is driven by CPU, NIC, and BMC temperatures // XXX we should also monitor DIMM temperatures here -pub const NUM_TEMPERATURE_INPUTS: usize = sensors::NUM_SBTSI_TEMPERATURE_SENSORS +const NUM_TEMPERATURE_INPUTS: usize = sensors::NUM_SBTSI_TEMPERATURE_SENSORS + sensors::NUM_TMP451_TEMPERATURE_SENSORS + NUM_NVME_BMC_TEMPERATURE_SENSORS; -// Every temperature sensor on Cosmo is owned by this task -pub const NUM_DYNAMIC_TEMPERATURE_INPUTS: usize = 0; - // We've got 6 fans, driven from a single MAX31790 IC -pub const NUM_FANS: usize = drv_i2c_devices::max31790::MAX_FANS as usize; +const NUM_FANS: usize = drv_i2c_devices::max31790::MAX_FANS as usize; /// This controller is tuned and ready to go pub const USE_CONTROLLER: bool = true; pub(crate) struct Bsp { /// Controlled sensors - pub priv_inputs: &'static mut [InputChannel; NUM_TEMPERATURE_INPUTS], - pub dynamic_inputs: - &'static mut [DynamicInputChannel; NUM_DYNAMIC_TEMPERATURE_INPUTS], + inputs: &'static mut [InputChannel; NUM_TEMPERATURE_INPUTS], /// Monitored sensors - pub misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], + misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], /// Fan control IC fctrl: Max31790State, @@ -172,74 +167,93 @@ impl Bsp { mode: PowerBitmask, i2c_task: TaskId, mut on_success: impl FnMut(&SensorId, f32) -> u64, + mut on_unexp_error: impl FnMut(&InputChannel, SensorReadError) -> u64, mut on_error: impl FnMut(&InputChannel, SensorReadError) -> u64, mut on_unpowered: impl FnMut(&SensorId) -> u64, ) { - // TODO(AJM): For some reason, we make two passes. The first hydrates - // data so that we can send it to the sensor API. This comes from the - // call to `read_sensors()`. We don't record the state here, as we only - // do that when we call `run_control()`. This has the interesting side - // effect that in ThermalMode::Manual and ThermalMode::Off, we don't - // remember the temperatures we've seen. - for s in self.priv_inputs.iter() { + // NOTE(AJM): This combines what used to be two passes before with + // `read_sensors` and `run_control`. Previously, the former would read + // all of the sensors and post them to the sensor task, and the latter + // would read them back and store them as state. Now, I do that all in + // just the first pass during `read_sensors`. This has *some* side + // effect, as before we wouldn't retain state unless we were running + // control, but now we always do. HOWEVER, this is not observable state, + // as Manual mode does nothing with this persistence, and whenever we + // enter the Auto state (which does control), we purge all the old + // values anyway! + for s in self.inputs.iter_mut() { if !mode.intersects(s.power_mode_mask) { - on_unpowered(&s.sensor.sensor_id); + let _now = on_unpowered(&s.sensor.sensor_id); + s.last_reading = Some(TemperatureReading::Inactive); continue; } match s.sensor.read_temp(i2c_task) { - Ok(v) => on_success(&s.sensor.sensor_id, v.0), - Err(e) => on_error(s, e), + Ok(v) => { + let now = on_success(&s.sensor.sensor_id, v.0); + s.last_reading = Some(TemperatureReading::Valid( + TimestampedTemperatureReading { + time_ms: now, + value: v, + }, + )); + } + Err(e) => { + // The current `unexpected_failure` comes from + // `read_sensors`, which is only deciding whether it's worth + // logging about. In either case, it will push NoData to the + // sensor api. + let e1 = e.clone(); + if unexpected_failure(s, e) { + let _now = on_unexp_error(s, e); + } else { + let _now = on_error(s, e); + } + + // However, when we later would have stored the state value + // for persistence in `run_control`, that used slightly + // different logic, ONLY clearing the persisted value if: + // + // - The sensor is not present AND removable + // - The sensor is error prone + // + // Replicate that logic here, doing some type shenanigans + // because we aren't round-tripping through the Sensor API + // anymore. + let e2 = NoData::from(e1); + let e3 = SensorError::from(e2); + match (s.ty, e3) { + (ChannelType::Removable, SensorError::NotPresent) => { + s.last_reading = None; + } + (ChannelType::RemovableAndErrorProne, _) => { + s.last_reading = None; + } + _ => { + // In all other cases, just leave whatever the last + // present value was so that the state estimation + // can continue estimating state. + } + } + } }; } } - // TODO: This might go away - pub fn read_inputs_back_from_sensor_api( - &mut self, - _mode: PowerBitmask, - _sensor_api: &Sensor, - _on_success: impl FnMut(&SensorId, f32), - _on_error: impl FnMut(&InputChannel, SensorReadError), - _on_unpowered: impl FnMut(&SensorId), - ) { - let x = _sensor_api.get_reading(todo!()).unwrap(); - todo!() - } - // TODO: This probably needs to exist, but for cosmo we have no dynamic - // inputs to read back + // inputs to read back. This should read from the api and store the state pub fn read_dynamic_inputs_back_from_sensor_api( &mut self, - _sensor_api: Sensor, + _sensor_api: &Sensor, ) { - // // The dynamic inputs don't depend on power mode; instead, they are - // // assumed to be present when a model exists in `self.dynamic_inputs`; - // // this model is set by external callers using - // // `update_dynamic_input` and `remove_dynamic_input`. - // for (i, sensor_id) in self.bsp.dynamic_inputs.iter().enumerate() { - // // - // // TODO(AJM): This is important to note! The index is inputs + i. - // // This *might* not matter when `state` isn't the one handling this. - // // - // // let index = i + self.bsp.inputs.len(); - // let index = todo!(); - // match self.dynamic_inputs[i] { - // Some(..) => { - // if let Ok(r) = self.sensor_api.get_reading(*sensor_id) { - // self.state.write_temperature(index, r); - // } - // } - // None => self.state.write_temperature_inactive(index), - // } - // } + // No dynamic inputs here } // returns Ok(true) if this was a new input pub fn update_dynamic_input( &mut self, - index: usize, - model: ThermalProperties, + _index: usize, + _model: ThermalProperties, ) -> Result { // No dynamic inputs here, todo: static assert this Err(ThermalError::InvalidIndex) @@ -248,23 +262,31 @@ impl Bsp { // sets last_reading to Some(Missing), returns sensor id pub fn remove_dynamic_input( &mut self, - index: usize, + _index: usize, ) -> Result { // No dynamic inputs here, todo: static assert this Err(ThermalError::InvalidIndex) } pub fn all_inputs_present(&self) -> bool { - self.priv_inputs.iter().all(|i| i.last_reading.is_some()) + self.inputs.iter().all(|i| i.last_reading.is_some()) } // Visit all temperature sensors, first the inputs, then the dynamic_inputs. // Inputs and Dynamic Inputs that are missing will be skipped. pub fn for_each_temp_allow_missing_inputs( &self, - f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), + mut f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), ) { - todo!() + let iter = self.inputs.iter().filter_map(|input| { + let last = input.last_reading?; + Some((input.sensor.sensor_id, last, input.model)) + }); + for (sensor_id, reading, model) in iter { + f(sensor_id, reading, model); + } + + // for _dinput in self.dynamic_inputs... } // Visit all temperature sensors, first the inputs, then the dynamic_inputs. @@ -273,9 +295,19 @@ impl Bsp { // inputs that are not present will be skipped. pub fn for_each_temp( &self, - f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), + mut f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), ) { - todo!() + for input in self.inputs.iter() { + let reading = input.last_reading.unwrap_lite(); + f(input.sensor.sensor_id, reading, input.model); + } + + // for _dinput in self.dynamic_inputs... + } + + pub fn reset_all_values(&mut self) { + self.inputs.iter_mut().for_each(|i| i.last_reading = None); + // self.dynamic_inputs... } // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, @@ -331,8 +363,7 @@ impl Bsp { max_output: 100.0, }, - priv_inputs: INPUTS_ONCE.claim(), - dynamic_inputs: &mut [], + inputs: INPUTS_ONCE.claim(), // We monitor and log all of the air temperatures misc_sensors: &MISC_SENSORS, diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 29fcaad5cf..5c8122830b 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -2,11 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use core::cell::Cell; - use crate::{ ThermalError, Trace, - bsp::{self, Bsp, PowerBitmask}, + bsp::{Bsp, PowerBitmask}, }; use drv_i2c_api::{I2cDevice, ResponseCode}; use drv_i2c_devices::{ @@ -22,10 +20,10 @@ use drv_i2c_devices::{ }; use ringbuf::ringbuf_entry_root as ringbuf_entry; -use task_sensor_api::{Reading, Sensor as SensorApi, SensorError, SensorId}; +use task_sensor_api::{Sensor as SensorApi, SensorId}; use task_thermal_api::{SensorReadError, ThermalAutoState, ThermalProperties}; use userlib::{ - TaskId, UnwrapLite, sys_get_timer, + TaskId, sys_get_timer, units::{Celsius, PWMDuty, Rpm}, }; @@ -216,6 +214,7 @@ impl InputChannel { /// many of them could be present, but their thermal properties could vary /// depending on what's plugged in. #[derive(Clone, Copy)] +#[allow(dead_code)] // Not all bsps have dynamic inputs pub(crate) struct DynamicInputChannel { pub sensor_id: SensorId, pub model: ThermalProperties, @@ -460,9 +459,9 @@ pub enum TemperatureReading { /// Represents a temperature reading at the time at which it was taken #[derive(Copy, Clone, Debug)] -struct TimestampedTemperatureReading { - time_ms: u64, - value: Celsius, +pub struct TimestampedTemperatureReading { + pub time_ms: u64, + pub value: Celsius, } /// Represents a worst-case temperature reading from the thermal model, @@ -584,12 +583,6 @@ impl Default for OneSidedPidState { } } -const TEMPERATURE_ARRAY_SIZE: usize = - bsp::NUM_TEMPERATURE_INPUTS + bsp::NUM_DYNAMIC_TEMPERATURE_INPUTS; - -type DynamicChannelsArray = - [Option; bsp::NUM_DYNAMIC_TEMPERATURE_INPUTS]; - /// This corresponds to states shown in RFD 276 /// /// All of our temperature arrays contain, in order @@ -726,163 +719,6 @@ enum ThermalControlState { Uncontrollable, } -impl ThermalControlState { - /// Sets all temperature readings to `None` and returns the array - fn reset_values(&mut self) -> OptionalTemperatureArray { - todo!() - // match self { - // ThermalControlState::Boot { values } - // | ThermalControlState::Uncontrollable(values) => { - // values.reset_values() - // } - // ThermalControlState::Running { values, .. } - // | ThermalControlState::Critical { values, .. } - // | ThermalControlState::FanParty { values } => values.reset_values(), - // } - } -} - -/// Abstractions over temperature array data -/// -/// We have three main types: -/// - A `RawTemperatureArray` is allocated in static memory using -/// `mutable_statics!` and provides the underlying storage for other array -/// types. It stores optional temperature readings in `Cell`s, so that it can -/// be passed around by shared reference. -/// - A [`OptionalTemperatureArray`] is a thin wrapper around a -/// `&'static RawTemperatureArray`, providing getters / setters / iterators. -/// - A [`TemperatureArray`] is a thin wrapper around a -/// `&'static RawTemperatureArray` which guarantees that all of the -/// temperatures in the array are `Some(..)`. -/// -/// This module exists to preserve the `TemperatureArray` invariants (or at -/// least make it harder to mess with them; someone could get at the inner -/// `&'static RawTemperatureArray and poke values directly, but let's just not -/// do that). -mod temperature_array { - use super::{ - Bsp, Cell, DynamicChannelsArray, SensorId, TEMPERATURE_ARRAY_SIZE, - TemperatureReading, ThermalProperties, UnwrapLite, - }; - - /// Array of optional temperature readings - /// - /// The array contains `Cell` objects so that it can be passed by shared - /// reference; otherwise, it becomes hard to transition between states in - /// the state machine because we can't move out a `&mut RawTemperatureArray` - pub type RawTemperatureArray = - [Cell>; TEMPERATURE_ARRAY_SIZE]; - - /// Type representing an array of optional temperature readings - #[derive(Copy, Clone)] - pub(crate) struct OptionalTemperatureArray(&'static RawTemperatureArray); - - impl OptionalTemperatureArray { - /// Builds a new optional temperature array - /// - /// Values are left unchanged (e.g. they are *not* set to `None`) - pub fn new(data: &'static RawTemperatureArray) -> Self { - Self(data) - } - - /// Resets all values to `None`, returning a copy of the array - pub fn reset_values(&self) -> Self { - for i in self.0 { - i.set(None); - } - *self - } - - /// Returns a [`TemperatureArray`] if all values are `Some(..)` - pub fn as_temperature_array(&self) -> Option { - if self.0.iter().all(|c| c.get().is_some()) { - Some(TemperatureArray(self.0)) - } else { - None - } - } - - /// Sets the temperature at index `i` to a value - /// - /// # Panics - /// If `i` is out of range for the array - pub fn set(&self, i: usize, value: TemperatureReading) { - self.0[i].set(Some(value)) - } - } - - /// Array of temperature values - /// - /// This stores an array of `Option`, but values are - /// guaranteed to be `Some(..)` by construction. - #[derive(Copy, Clone)] - pub struct TemperatureArray(&'static RawTemperatureArray); - - impl TemperatureArray { - /// Returns an `OptionalTemperatureArray` with all values set to `None` - pub fn reset_values(&self) -> OptionalTemperatureArray { - let opt = OptionalTemperatureArray::new(self.0); - opt.reset_values() - } - - /// Sets the temperature at index `i` to a value - /// - /// # Panics - /// If `i` is out of range for the array - pub fn set(&self, i: usize, value: TemperatureReading) { - self.0[i].set(Some(value)) - } - - // /// Temperature state iterator with `TemperatureReading` values - // pub fn zip_temperatures<'a>( - // &'a self, - // bsp: &'a Bsp, - // dynamic_channels: &'a DynamicChannelsArray, - // ) -> impl Iterator< - // Item = (SensorId, TemperatureReading, ThermalProperties), - // > + use<'a> { - // zip_temperatures(bsp, self.0, dynamic_channels, |v| v.unwrap_lite()) - // } - } - - // /// Returns an iterator over tuples of `(sensor_id, value, thermal model)` - // /// - // /// The `values` array contains static and dynamic values (in order); - // /// this function will panic if sizes are mismatched. - // /// - // /// Every dynamic input is represented by an `Option`. - // /// If the input is not present right now, it will be `None`, but will - // /// continue to take up space to preserve ordering. - // /// - // /// In cases where dynamic inputs are not present (i.e. they are `None` in - // /// the array), the iterator will skip that entire tuple. - // fn zip_temperatures<'a, U: 'static>( - // bsp: &'a Bsp, - // values: &'a RawTemperatureArray, - // dynamic_channels: &'a DynamicChannelsArray, - // f: fn(Option) -> U, - // ) -> impl Iterator + use<'a, U> - // { - // // assert_eq!(values.len(), bsp.inputs.len() + bsp.dynamic_inputs.len()); - // assert_eq!(bsp.dynamic_inputs.len(), dynamic_channels.len()); - // // bsp.inputs - // // .iter() - // // .map(|i| Some((i.sensor.sensor_id, i.model))) - // // .chain( - // // dynamic_channels - // // .iter() - // // .zip(bsp.dynamic_inputs.iter().cloned()) - // // .map(|(i, s)| i.map(|i| (s, i.model))), - // // ) - // // .zip(values.iter().map(move |v| f(v.get()))) - // // .filter_map(|(model, v)| model.map(|(id, t)| (id, v, t))) - // todo!() - // } -} -use temperature_array::{ - OptionalTemperatureArray, RawTemperatureArray, TemperatureArray, -}; - enum ControlResult { Pwm(PWMDuty), PowerDown, @@ -893,50 +729,6 @@ struct OverheatTimer { critical_ms: u64, } -impl ThermalControlState { - fn write_temperature(&mut self, index: usize, reading: Reading) { - todo!() - // let r = TemperatureReading::Valid(TimestampedTemperatureReading { - // time_ms: reading.timestamp, - // value: Celsius(reading.value), - // }); - // match self { - // ThermalControlState::Boot { values } => { - // values.set(index, r); - // } - // ThermalControlState::Running { values, .. } - // | ThermalControlState::Critical { values, .. } - // | ThermalControlState::FanParty { values, .. } => { - // values.set(index, r); - // } - // ThermalControlState::Uncontrollable(..) => (), - // } - } - - fn write_temperature_inactive(&mut self, index: usize) { - todo!() - // match self { - // ThermalControlState::Boot { values } => { - // values.set(index, TemperatureReading::Inactive); - // } - // ThermalControlState::Running { values, .. } - // | ThermalControlState::Critical { values, .. } - // | ThermalControlState::FanParty { values, .. } => { - // values.set(index, TemperatureReading::Inactive); - // } - // ThermalControlState::Uncontrollable(..) => (), - // } - } -} - -fn claim_static_resources() -> &'static RawTemperatureArray { - mutable_statics::mutable_statics! { - static mut TEMPERATURE_ARRAY: - [Cell>; TEMPERATURE_ARRAY_SIZE] - = [|| Cell::new(None); _]; - } -} - impl<'a> ThermalControl<'a> { /// Constructs a new `ThermalControl` based on a `struct Bsp`. This /// requires that every BSP has the same internal structure, @@ -958,7 +750,6 @@ impl<'a> ThermalControl<'a> { }; let pid_config = bsp.pid_config; - let data = claim_static_resources(); Self { bsp, i2c_task, @@ -1037,7 +828,7 @@ impl<'a> ThermalControl<'a> { /// Resets the control state fn reset_state(&mut self) { - let values = self.state.reset_values(); + self.bsp.reset_all_values(); self.state = ThermalControlState::Boot; ringbuf_entry!(Trace::AutoState(self.get_state())); } @@ -1107,18 +898,7 @@ impl<'a> ThermalControl<'a> { // We read the power mode right before reading sensors, to avoid // potential TOCTOU issues; some sensors cannot be read if they are not // powered. - let unexpected_failure = |s: &InputChannel, e: SensorReadError| { - let removable = matches!( - s.ty, - ChannelType::Removable | ChannelType::RemovableAndErrorProne - ); - let removed = - e == SensorReadError::I2cError(ResponseCode::NoDevice); - !(removable && removed) - }; - let power_mode = self.bsp.power_mode(); - self.bsp.read_inputs( power_mode, self.i2c_task, @@ -1126,13 +906,14 @@ impl<'a> ThermalControl<'a> { |s, error| { // Record an error errors if the sensor is not removable // or we get a unexpected error from a removable sensor - if unexpected_failure(s, error) { - ringbuf_entry!(Trace::SensorReadFailed( - s.sensor.sensor_id, - error - )); - self.err_blackbox.push(s.sensor.sensor_id, error); - } + ringbuf_entry!(Trace::SensorReadFailed( + s.sensor.sensor_id, + error + )); + self.err_blackbox.push(s.sensor.sensor_id, error); + self.sensor_api.nodata_now(s.sensor.sensor_id, error.into()) + }, + |s, error| { self.sensor_api.nodata_now(s.sensor.sensor_id, error.into()) }, |id| { @@ -1166,64 +947,24 @@ impl<'a> ThermalControl<'a> { self.reset_state(); } - // Load sensor readings from the `sensors` API. - // - // If the most recent reading is an error, then leave the previous value - // in `self.state`. When we're in the `Boot` state, this will leave the - // value as `None`; when we're `Running`, it will maintain the previous - // state, estimating a new temperature with the thermal model. - self.bsp.read_inputs_back_from_sensor_api( - self.power_mode, - &self.sensor_api, - |id, value| { - self.state.write_temperature( - todo!("idx, not id!"), - todo!("reading, not f32!"), - ) - }, - |_, _| todo!(), - |_| todo!(), - ); - todo!(); - // for (i, s) in self.bsp.inputs.iter().enumerate() { - // if self.power_mode.intersects(s.power_mode_mask) { - // let sensor_id = s.sensor.sensor_id; - // let r = self.sensor_api.get_reading(sensor_id); - // match r { - // Ok(r) => { - // self.state.write_temperature(i, r); - // } - // Err(SensorError::NotPresent) - // if s.ty == ChannelType::Removable => - // { - // // Ignore errors if the sensor is removable and the - // // error indicates that it's not present. - // self.state.write_temperature_inactive(i); - // } - // Err(_) if s.ty == ChannelType::RemovableAndErrorProne => { - // // Ignore all errors if this device is error-prone - // self.state.write_temperature_inactive(i); - // } - // Err(_) => (), - // } - // } else { - // self.state.write_temperature_inactive(i); - // } - // } + // `input` sensors have all been read during `read_sensors`. // The dynamic inputs don't depend on power mode; instead, they are // assumed to be present when a model exists in `self.dynamic_inputs`; // this model is set by external callers using // `update_dynamic_input` and `remove_dynamic_input`. + // + // TODO(AJM): Should we be doing this in `read_sensors` instead of + // `run_control`? self.bsp - .read_dynamic_inputs_back_from_sensor_api(self.sensor_api); + .read_dynamic_inputs_back_from_sensor_api(&self.sensor_api); let control_result = match &mut self.state { ThermalControlState::Boot => { let mut any_power_down = None; let mut worst_margin = f32::MAX; - let mut f = |sensor_id, v, model| { + let f = |sensor_id, v, model| { match v { TemperatureReading::Valid(v) => { let worst_case = v.worst_case(now_ms, &model); @@ -1261,7 +1002,7 @@ impl<'a> ThermalControl<'a> { // below their max temperature; negative means someone is // overheating. We want to pick the _smallest_ margin, since // that's the part which is most overheated. - let mut f = |sensor_id, v, model| { + let f = |sensor_id, v, model| { if let TemperatureReading::Valid(v) = v { let worst_case = v.worst_case(now_ms, &model); let temperature = worst_case.worst_case_temp; @@ -1304,7 +1045,7 @@ impl<'a> ThermalControl<'a> { let mut any_still_critical = false; let mut any_power_down = None; let mut worst_margin = f32::MAX; - let mut f = |sensor_id, v, model| { + let f = |sensor_id, v, model| { if let TemperatureReading::Valid(v) = v { let worst_case = v.worst_case(now_ms, &model); let temperature = worst_case.worst_case_temp; @@ -1317,6 +1058,7 @@ impl<'a> ThermalControl<'a> { worst_margin.min(model.margin(temperature).0); } }; + self.bsp.for_each_temp(f); if let Some(due_to) = any_power_down { self.transition_to_uncontrollable_due_to(due_to, now_ms) @@ -1339,7 +1081,7 @@ impl<'a> ThermalControl<'a> { let mut any_power_down = None; let mut any_critical = None; let mut worst_margin = f32::MAX; - let mut f = |sensor_id, v, model| { + let f = |sensor_id, v, model| { if let TemperatureReading::Valid(v) = v { let worst_case = v.worst_case(now_ms, &model); let temperature = worst_case.worst_case_temp; @@ -1354,6 +1096,7 @@ impl<'a> ThermalControl<'a> { worst_margin.min(model.margin(temperature).0); } }; + self.bsp.for_each_temp(f); if let Some(due_to) = any_power_down { self.transition_to_uncontrollable_due_to(due_to, now_ms) @@ -1497,7 +1240,7 @@ impl<'a> ThermalControl<'a> { self.record_leaving_critical(now_ms); self.record_leaving_overheat(now_ms); - let _values = self.state.reset_values(); + self.bsp.reset_all_values(); self.state = ThermalControlState::Uncontrollable; ringbuf_entry!(Trace::AutoState(self.get_state())); @@ -1647,3 +1390,12 @@ impl<'a> ThermalControl<'a> { Ok(()) } } + +pub fn unexpected_failure(s: &InputChannel, e: SensorReadError) -> bool { + let removable = matches!( + s.ty, + ChannelType::Removable | ChannelType::RemovableAndErrorProne + ); + let removed = e == SensorReadError::I2cError(ResponseCode::NoDevice); + !(removable && removed) +} From a87ed16ea98e22d7a00f0a32dfe44d9558f0477a Mon Sep 17 00:00:00 2001 From: James Munns Date: Thu, 23 Jul 2026 16:22:08 +0200 Subject: [PATCH 06/33] Start decomposing run_control --- task/thermal/src/bsp/cosmo_ab.rs | 1 + task/thermal/src/control.rs | 195 ++++++++++++++----------------- 2 files changed, 88 insertions(+), 108 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index f0e60d5f3e..2d65329a3c 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -270,6 +270,7 @@ impl Bsp { pub fn all_inputs_present(&self) -> bool { self.inputs.iter().all(|i| i.last_reading.is_some()) + // && self.dynamic_inputs... } // Visit all temperature sensors, first the inputs, then the dynamic_inputs. diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 5c8122830b..02f381fa74 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -934,9 +934,10 @@ impl<'a> ThermalControl<'a> { /// Returns an error if the control loop failed to read critical sensors; /// the caller should set us to some kind of fail-safe mode if this /// occurs. - pub fn run_control(&mut self) -> Result<(), ThermalError> { - let now_ms = sys_get_timer().now; - + fn run_control_inner( + &mut self, + now_ms: u64, + ) -> Result { // When the power mode changes, we may require a new set of sensors to // be online. Reset the control state, waiting for all newly-required // sensors to come online before re-entering the control loop. @@ -959,33 +960,74 @@ impl<'a> ThermalControl<'a> { self.bsp .read_dynamic_inputs_back_from_sensor_api(&self.sensor_api); - let control_result = match &mut self.state { + // Run a common analysis pass first, regardless of state. Don't take any + // side effectful actions yet though. + let mut all_nominal = true; + let mut any_power_down = None; + let mut any_critical = None; + let mut worst_margin = f32::MAX; + + // Remember, positive margin means that all parts are happily + // below their max temperature; negative means someone is + // overheating. We want to pick the _smallest_ margin, since + // that's the part which is most overheated. + let f = |sensor_id, v, model| { + if let TemperatureReading::Valid(v) = v { + let worst_case = v.worst_case(now_ms, &model); + let temperature = worst_case.worst_case_temp; + all_nominal &= model.is_nominal(temperature); + if model.should_power_down(temperature) { + any_power_down = Some((sensor_id, worst_case)); + } + if model.is_critical(temperature) { + any_critical = Some((sensor_id, worst_case)); + } + worst_margin = worst_margin.min(model.margin(temperature).0); + } + + // Hidden assumption here! ALL `TemperatureReading`s here will be + // valid, UNLESS: + // + // 1. We are in Boot state and we allow missing inputs, which will + // skip anything we haven't heard yet (but will report `false` + // for `bsp.all_inputs_present()`) + // 2. For input sensors: the given input is not active in this + // power state + // 3. For dynamic sensors: The given dynamic input has not been + // given to us to activate + // + // We don't necessarily *check* that is the case though, and we + // might want to in the for_each_temp(_allow_missing_inputs) + // functions! + }; + match self.state { + ThermalControlState::Boot => { + self.bsp.for_each_temp_allow_missing_inputs(f) + } + ThermalControlState::Running { .. } => self.bsp.for_each_temp(f), + ThermalControlState::Critical { .. } => self.bsp.for_each_temp(f), + ThermalControlState::FanParty => self.bsp.for_each_temp(f), + ThermalControlState::Uncontrollable => { + return Ok(ControlResult::PowerDown); + } + } + + // In any state, if we've reached the "any_power_down" threshold, then + // it's time to go. + if let Some(due_to) = any_power_down { + return Ok(self.transition_to_uncontrollable_due_to(due_to, now_ms)); + } + + let all_nominal = all_nominal; + let any_critical = any_critical; + let worst_margin = worst_margin; + + // TODO(AJM): I think we could dedupe some of the code below, basically + // working backwards and checking if we "qualify" for each state, though + // that's a bit more invasive of a change + Ok(match &mut self.state { ThermalControlState::Boot => { - let mut any_power_down = None; - let mut worst_margin = f32::MAX; - - let f = |sensor_id, v, model| { - match v { - TemperatureReading::Valid(v) => { - let worst_case = v.worst_case(now_ms, &model); - let temperature = worst_case.worst_case_temp; - if model.should_power_down(temperature) { - any_power_down = Some((sensor_id, worst_case)); - } - worst_margin = - worst_margin.min(model.margin(temperature).0); - } - TemperatureReading::Inactive => { - // Inactive sensors are ignored, but do not gate us - // from transitioning to `Running` - } - } - }; - self.bsp.for_each_temp_allow_missing_inputs(f); - - if let Some(due_to) = any_power_down { - self.transition_to_uncontrollable_due_to(due_to, now_ms) - } else if self.bsp.all_inputs_present() { + if self.bsp.all_inputs_present() { self.transition_to_running(worst_margin, now_ms) } else { ControlResult::Pwm(PWMDuty( @@ -994,34 +1036,7 @@ impl<'a> ThermalControl<'a> { } } ThermalControlState::Running { pid } => { - let mut any_power_down = None; - let mut any_critical = None; - let mut worst_margin = f32::MAX; - - // Remember, positive margin means that all parts are happily - // below their max temperature; negative means someone is - // overheating. We want to pick the _smallest_ margin, since - // that's the part which is most overheated. - let f = |sensor_id, v, model| { - if let TemperatureReading::Valid(v) = v { - let worst_case = v.worst_case(now_ms, &model); - let temperature = worst_case.worst_case_temp; - if model.should_power_down(temperature) { - any_power_down = Some((sensor_id, worst_case)); - } - if model.is_critical(temperature) { - any_critical = Some((sensor_id, worst_case)); - } - - worst_margin = - worst_margin.min(model.margin(temperature).0); - } - }; - self.bsp.for_each_temp(f); - - if let Some(due_to) = any_power_down { - self.transition_to_uncontrollable_due_to(due_to, now_ms) - } else if let Some(due_to) = any_critical { + if let Some(due_to) = any_critical { self.transition_to_critical(due_to, now_ms) } else { // We adjust the worst component margin by our target @@ -1041,30 +1056,9 @@ impl<'a> ThermalControl<'a> { } } ThermalControlState::Critical { .. } => { - let mut all_nominal = true; - let mut any_still_critical = false; - let mut any_power_down = None; - let mut worst_margin = f32::MAX; - let f = |sensor_id, v, model| { - if let TemperatureReading::Valid(v) = v { - let worst_case = v.worst_case(now_ms, &model); - let temperature = worst_case.worst_case_temp; - all_nominal &= model.is_nominal(temperature); - any_still_critical |= model.is_critical(temperature); - if model.should_power_down(temperature) { - any_power_down = Some((sensor_id, worst_case)); - } - worst_margin = - worst_margin.min(model.margin(temperature).0); - } - }; - self.bsp.for_each_temp(f); - - if let Some(due_to) = any_power_down { - self.transition_to_uncontrollable_due_to(due_to, now_ms) - } else if all_nominal { + if all_nominal { self.transition_to_running(worst_margin, now_ms) - } else if !any_still_critical { + } else if !any_critical.is_some() { // If all temperatures have gone below critical, but are // still above nominal, stop the overheat timeout but // continue running at 100% PWM until things go below @@ -1077,30 +1071,7 @@ impl<'a> ThermalControl<'a> { } } ThermalControlState::FanParty => { - let mut all_nominal = true; - let mut any_power_down = None; - let mut any_critical = None; - let mut worst_margin = f32::MAX; - let f = |sensor_id, v, model| { - if let TemperatureReading::Valid(v) = v { - let worst_case = v.worst_case(now_ms, &model); - let temperature = worst_case.worst_case_temp; - all_nominal &= model.is_nominal(temperature); - if model.should_power_down(temperature) { - any_power_down = Some((sensor_id, worst_case)); - } - if model.is_critical(temperature) { - any_critical = Some((sensor_id, worst_case)); - } - worst_margin = - worst_margin.min(model.margin(temperature).0); - } - }; - self.bsp.for_each_temp(f); - - if let Some(due_to) = any_power_down { - self.transition_to_uncontrollable_due_to(due_to, now_ms) - } else if let Some(due_to) = any_critical { + if let Some(due_to) = any_critical { // If anything's gone over critical, transition back to the // `Critical` state. self.transition_to_critical(due_to, now_ms) @@ -1113,13 +1084,22 @@ impl<'a> ThermalControl<'a> { } } ThermalControlState::Uncontrollable => ControlResult::PowerDown, - }; + }) + } + /// An extremely simple thermal control loop. + /// + /// Returns an error if the control loop failed to read critical sensors; + /// the caller should set us to some kind of fail-safe mode if this + /// occurs. + pub fn run_control(&mut self) -> Result<(), ThermalError> { + let now_ms = sys_get_timer().now; + let control_result = self.run_control_inner(now_ms)?; match control_result { ControlResult::Pwm(target_pwm) => { // Send the new RPM to all of our fans ringbuf_entry!(Trace::ControlPwm(target_pwm.0)); - self.set_pwm(Ok(target_pwm), now_ms)?; + self.set_pwm(Ok(target_pwm), now_ms) } ControlResult::PowerDown => { ringbuf_entry!(Trace::PowerDownAt(sys_get_timer().now)); @@ -1129,10 +1109,9 @@ impl<'a> ThermalControl<'a> { ringbuf_entry!(Trace::PowerDownFailed(e)); } self.set_pwm(Err(task_sensor_api::NoData::DeviceOff), now_ms)?; + Ok(()) } } - - Ok(()) } /// Transition the control state to the normal control regime. From 823a7dc060f2bfbaddaf0d2677bb9dcf1b38ec98 Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 24 Jul 2026 00:57:55 +0200 Subject: [PATCH 07/33] Push logic into InputChannel --- task/thermal/src/bsp/cosmo_ab.rs | 123 ++++++++------------------ task/thermal/src/control.rs | 145 ++++++++++++++++++++++++++----- 2 files changed, 161 insertions(+), 107 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 2d65329a3c..d3bdec4ac1 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -8,17 +8,17 @@ use crate::{ Fan, control::{ ChannelType, ControllerInitError, Device, FanControl, InputChannel, - Max31790State, PidConfig, TemperatureReading, TemperatureSensor, - TimestampedTemperatureReading, unexpected_failure, + Max31790State, PidConfig, ReadingOutcome, TemperatureReading, + TemperatureSensor, }, i2c_config::{devices, sensors}, }; pub use drv_cpu_seq_api::SeqError; use drv_cpu_seq_api::{PowerState, Sequencer, StateChangeReason}; -use task_sensor_api::{NoData, Sensor, SensorError, SensorId}; +use task_sensor_api::{Sensor, SensorId}; use task_thermal_api::{SensorReadError, ThermalError, ThermalProperties}; use userlib::{ - TaskId, UnwrapLite, task_slot, + TaskId, UnwrapLite, sys_get_timer, task_slot, units::{Celsius, PWMDuty}, }; @@ -75,7 +75,7 @@ bitflags::bitflags! { } impl Bsp { - pub fn fan_control( + fn fan_control( &mut self, fan: crate::Fan, ) -> Result, ControllerInitError> { @@ -124,9 +124,9 @@ impl Bsp { pub fn read_fan_rpms( &mut self, - mut on_success: impl FnMut(&SensorId, f32) -> u64, - mut on_error: impl FnMut(&SensorId, SensorReadError) -> u64, - _on_missing: impl FnMut(&SensorId) -> u64, + mut on_success: impl FnMut(&SensorId, f32, u64), + mut on_error: impl FnMut(&SensorId, SensorReadError), + _on_missing: impl FnMut(&SensorId), ) { for (idx, sensor) in sensors::MAX31790_SPEED_SENSORS.iter().enumerate() { @@ -140,9 +140,11 @@ impl Bsp { } }; + // TODO: Record state! + let now = sys_get_timer().now; // TODO(AJM): Keep last fan RPM? match fctrl.fan_rpm() { - Ok(reading) => on_success(sensor, reading.0.into()), + Ok(reading) => on_success(sensor, reading.0.into(), now), Err(e) => on_error(sensor, SensorReadError::I2cError(e)), }; } @@ -151,12 +153,14 @@ impl Bsp { pub fn read_misc_sensors( &mut self, i2c_task: TaskId, - mut on_success: impl FnMut(&SensorId, f32) -> u64, - mut on_error: impl FnMut(&SensorId, SensorReadError) -> u64, + mut on_success: impl FnMut(&SensorId, f32, u64), + mut on_error: impl FnMut(&SensorId, SensorReadError), ) { for s in self.misc_sensors.iter() { + // TODO: Record state! + let now = sys_get_timer().now; match s.read_temp(i2c_task) { - Ok(v) => on_success(&s.sensor_id, v.0), + Ok(v) => on_success(&s.sensor_id, v.0, now), Err(e) => on_error(&s.sensor_id, e), }; } @@ -166,75 +170,24 @@ impl Bsp { &mut self, mode: PowerBitmask, i2c_task: TaskId, - mut on_success: impl FnMut(&SensorId, f32) -> u64, - mut on_unexp_error: impl FnMut(&InputChannel, SensorReadError) -> u64, - mut on_error: impl FnMut(&InputChannel, SensorReadError) -> u64, - mut on_unpowered: impl FnMut(&SensorId) -> u64, + mut on_success: impl FnMut(&SensorId, f32, u64), + mut on_unexp_error: impl FnMut(&SensorId, SensorReadError), + mut on_error: impl FnMut(&SensorId, SensorReadError), + mut on_unpowered: impl FnMut(&SensorId), ) { - // NOTE(AJM): This combines what used to be two passes before with - // `read_sensors` and `run_control`. Previously, the former would read - // all of the sensors and post them to the sensor task, and the latter - // would read them back and store them as state. Now, I do that all in - // just the first pass during `read_sensors`. This has *some* side - // effect, as before we wouldn't retain state unless we were running - // control, but now we always do. HOWEVER, this is not observable state, - // as Manual mode does nothing with this persistence, and whenever we - // enter the Auto state (which does control), we purge all the old - // values anyway! + // TODO: Just make this an iterator and hoist the match up to the caller + // instead of mapping closures for s in self.inputs.iter_mut() { - if !mode.intersects(s.power_mode_mask) { - let _now = on_unpowered(&s.sensor.sensor_id); - s.last_reading = Some(TemperatureReading::Inactive); - continue; - } - - match s.sensor.read_temp(i2c_task) { - Ok(v) => { - let now = on_success(&s.sensor.sensor_id, v.0); - s.last_reading = Some(TemperatureReading::Valid( - TimestampedTemperatureReading { - time_ms: now, - value: v, - }, - )); + match s.do_reading(mode, &i2c_task) { + ReadingOutcome::Unpowered { id } => on_unpowered(&id), + ReadingOutcome::AcceptableMissing { id, err } => { + on_error(&id, err) } - Err(e) => { - // The current `unexpected_failure` comes from - // `read_sensors`, which is only deciding whether it's worth - // logging about. In either case, it will push NoData to the - // sensor api. - let e1 = e.clone(); - if unexpected_failure(s, e) { - let _now = on_unexp_error(s, e); - } else { - let _now = on_error(s, e); - } - - // However, when we later would have stored the state value - // for persistence in `run_control`, that used slightly - // different logic, ONLY clearing the persisted value if: - // - // - The sensor is not present AND removable - // - The sensor is error prone - // - // Replicate that logic here, doing some type shenanigans - // because we aren't round-tripping through the Sensor API - // anymore. - let e2 = NoData::from(e1); - let e3 = SensorError::from(e2); - match (s.ty, e3) { - (ChannelType::Removable, SensorError::NotPresent) => { - s.last_reading = None; - } - (ChannelType::RemovableAndErrorProne, _) => { - s.last_reading = None; - } - _ => { - // In all other cases, just leave whatever the last - // present value was so that the state estimation - // can continue estimating state. - } - } + ReadingOutcome::UnacceptableMissing { id, err } => { + on_unexp_error(&id, err) + } + ReadingOutcome::Success { id, now, value } => { + on_success(&id, value.0, now) } }; } @@ -269,7 +222,7 @@ impl Bsp { } pub fn all_inputs_present(&self) -> bool { - self.inputs.iter().all(|i| i.last_reading.is_some()) + self.inputs.iter().all(|i| i.last_reading().is_some()) // && self.dynamic_inputs... } @@ -280,11 +233,11 @@ impl Bsp { mut f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), ) { let iter = self.inputs.iter().filter_map(|input| { - let last = input.last_reading?; - Some((input.sensor.sensor_id, last, input.model)) + let last = input.last_reading()?; + Some((input.sensor_id(), last, input.model())) }); for (sensor_id, reading, model) in iter { - f(sensor_id, reading, model); + f(sensor_id, *reading, model); } // for _dinput in self.dynamic_inputs... @@ -299,15 +252,15 @@ impl Bsp { mut f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), ) { for input in self.inputs.iter() { - let reading = input.last_reading.unwrap_lite(); - f(input.sensor.sensor_id, reading, input.model); + let reading = input.last_reading().unwrap_lite(); + f(input.sensor_id(), *reading, input.model()); } // for _dinput in self.dynamic_inputs... } pub fn reset_all_values(&mut self) { - self.inputs.iter_mut().for_each(|i| i.last_reading = None); + self.inputs.iter_mut().for_each(|i| i.reset_value()); // self.dynamic_inputs... } diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 02f381fa74..15e1e6596c 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -20,7 +20,7 @@ use drv_i2c_devices::{ }; use ringbuf::ringbuf_entry_root as ringbuf_entry; -use task_sensor_api::{Sensor as SensorApi, SensorId}; +use task_sensor_api::{NoData, Sensor as SensorApi, SensorError, SensorId}; use task_thermal_api::{SensorReadError, ThermalAutoState, ThermalProperties}; use userlib::{ TaskId, sys_get_timer, @@ -129,18 +129,18 @@ impl<'a> FanControl<'a> { /// particular component in the system. pub(crate) struct InputChannel { /// Temperature sensor - pub sensor: TemperatureSensor, + sensor: TemperatureSensor, /// Thermal properties of the associated component - pub model: ThermalProperties, + model: ThermalProperties, /// Mask with bits set based on the Bsp's `power_mode` bits - pub power_mode_mask: PowerBitmask, + power_mode_mask: PowerBitmask, /// Channel type - pub ty: ChannelType, + ty: ChannelType, - pub last_reading: Option, + last_reading: Option, } #[derive(Copy, Clone, Eq, PartialEq)] @@ -183,6 +183,25 @@ pub(crate) enum ChannelType { RemovableAndErrorProne, } +pub enum ReadingOutcome { + Unpowered { + id: SensorId, + }, + AcceptableMissing { + id: SensorId, + err: SensorReadError, + }, + UnacceptableMissing { + id: SensorId, + err: SensorReadError, + }, + Success { + id: SensorId, + now: u64, + value: Celsius, + }, +} + impl InputChannel { #[allow(dead_code)] // not all BSPS pub const fn new( @@ -199,6 +218,90 @@ impl InputChannel { last_reading: None, } } + + pub fn last_reading(&self) -> Option<&TemperatureReading> { + self.last_reading.as_ref() + } + + pub fn reset_value(&mut self) { + self.last_reading = None; + } + + pub fn sensor_id(&self) -> SensorId { + self.sensor.sensor_id + } + + pub fn model(&self) -> ThermalProperties { + self.model + } + + pub fn do_reading( + &mut self, + mode: PowerBitmask, + i2c_task: &TaskId, + ) -> ReadingOutcome { + let id = self.sensor.sensor_id; + if !mode.intersects(self.power_mode_mask) { + self.last_reading = Some(TemperatureReading::Inactive); + return ReadingOutcome::Unpowered { id }; + } + + match self.sensor.read_temp(*i2c_task) { + Ok(value) => { + // TODO: Slightly different time than will be reported! We can + // fix this by providing the time instead of the helper + // functions that get the now time for us. + // let now = on_success(&s.sensor.sensor_id, v.0); + let now = sys_get_timer().now; + + self.last_reading = Some(TemperatureReading::Valid( + TimestampedTemperatureReading { + time_ms: now, + value, + }, + )); + ReadingOutcome::Success { id, now, value } + } + Err(e) => { + // However, when we later would have stored the state value + // for persistence in `run_control`, that used slightly + // different logic, ONLY clearing the persisted value if: + // + // - The sensor is not present AND removable + // - The sensor is error prone + // + // Replicate that logic here, doing some type shenanigans + // because we aren't round-tripping through the Sensor API + // anymore. + let e1 = e.clone(); + let e2 = NoData::from(e1); + let e3 = SensorError::from(e2); + match (self.ty, e3) { + (ChannelType::Removable, SensorError::NotPresent) => { + self.last_reading = None; + } + (ChannelType::RemovableAndErrorProne, _) => { + self.last_reading = None; + } + _ => { + // In all other cases, just leave whatever the last + // present value was so that the state estimation + // can continue estimating state. + } + } + + // The current `unexpected_failure` comes from + // `read_sensors`, which is only deciding whether it's worth + // logging about. In either case, it will push NoData to the + // sensor api. + if unexpected_failure(self, e) { + ReadingOutcome::UnacceptableMissing { id, err: e } + } else { + ReadingOutcome::AcceptableMissing { id, err: e } + } + } + } + } } //////////////////////////////////////////////////////////////////////////////// @@ -872,26 +975,26 @@ impl<'a> ThermalControl<'a> { pub fn read_sensors(&mut self) { // Read fan data and log it to the sensors task self.bsp.read_fan_rpms( - |id, value| self.sensor_api.post_now(*id, value), + |id, value, now| self.sensor_api.post(*id, value, now), |id, error| { ringbuf_entry!(Trace::FanReadFailed(*id, error)); self.err_blackbox.push(*id, error); - self.sensor_api.nodata_now(*id, error.into()) + self.sensor_api.nodata_now(*id, error.into()); }, |id| { self.sensor_api - .nodata_now(*id, task_sensor_api::NoData::DeviceNotPresent) + .nodata_now(*id, task_sensor_api::NoData::DeviceNotPresent); }, ); // Read miscellaneous temperature data and log it to the sensors task self.bsp.read_misc_sensors( self.i2c_task, - |id, value| self.sensor_api.post_now(*id, value), + |id, value, now| self.sensor_api.post(*id, value, now), |id, error| { ringbuf_entry!(Trace::MiscReadFailed(*id, error)); self.err_blackbox.push(*id, error); - self.sensor_api.nodata_now(*id, error.into()) + self.sensor_api.nodata_now(*id, error.into()); }, ); @@ -902,25 +1005,23 @@ impl<'a> ThermalControl<'a> { self.bsp.read_inputs( power_mode, self.i2c_task, - |id, value| self.sensor_api.post_now(*id, value), - |s, error| { + |id, value, now| self.sensor_api.post(*id, value, now), + |id, error| { + let id = *id; // Record an error errors if the sensor is not removable // or we get a unexpected error from a removable sensor - ringbuf_entry!(Trace::SensorReadFailed( - s.sensor.sensor_id, - error - )); - self.err_blackbox.push(s.sensor.sensor_id, error); - self.sensor_api.nodata_now(s.sensor.sensor_id, error.into()) + ringbuf_entry!(Trace::SensorReadFailed(id, error)); + self.err_blackbox.push(id, error); + self.sensor_api.nodata_now(id, error.into()); }, - |s, error| { - self.sensor_api.nodata_now(s.sensor.sensor_id, error.into()) + |id, error| { + self.sensor_api.nodata_now(*id, error.into()); }, |id| { // If the device isn't supposed to be on in the current power // state, then record it as Off in the sensors task. self.sensor_api - .nodata_now(*id, task_sensor_api::NoData::DeviceOff) + .nodata_now(*id, task_sensor_api::NoData::DeviceOff); }, ); From 4d2fdbd66661b2e67c4b2897d539b1890997cbed Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 24 Jul 2026 12:55:09 +0200 Subject: [PATCH 08/33] Hoist logic back into control --- task/thermal/src/bsp/cosmo_ab.rs | 30 +++------------------ task/thermal/src/control.rs | 46 +++++++++++++++++--------------- 2 files changed, 27 insertions(+), 49 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index d3bdec4ac1..f93514bfe3 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -8,8 +8,7 @@ use crate::{ Fan, control::{ ChannelType, ControllerInitError, Device, FanControl, InputChannel, - Max31790State, PidConfig, ReadingOutcome, TemperatureReading, - TemperatureSensor, + Max31790State, PidConfig, TemperatureReading, TemperatureSensor, }, i2c_config::{devices, sensors}, }; @@ -166,31 +165,8 @@ impl Bsp { } } - pub fn read_inputs( - &mut self, - mode: PowerBitmask, - i2c_task: TaskId, - mut on_success: impl FnMut(&SensorId, f32, u64), - mut on_unexp_error: impl FnMut(&SensorId, SensorReadError), - mut on_error: impl FnMut(&SensorId, SensorReadError), - mut on_unpowered: impl FnMut(&SensorId), - ) { - // TODO: Just make this an iterator and hoist the match up to the caller - // instead of mapping closures - for s in self.inputs.iter_mut() { - match s.do_reading(mode, &i2c_task) { - ReadingOutcome::Unpowered { id } => on_unpowered(&id), - ReadingOutcome::AcceptableMissing { id, err } => { - on_error(&id, err) - } - ReadingOutcome::UnacceptableMissing { id, err } => { - on_unexp_error(&id, err) - } - ReadingOutcome::Success { id, now, value } => { - on_success(&id, value.0, now) - } - }; - } + pub fn inputs_mut(&mut self) -> impl Iterator { + self.inputs.iter_mut() } // TODO: This probably needs to exist, but for cosmo we have no dynamic diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 15e1e6596c..55a2ff5001 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -1002,28 +1002,30 @@ impl<'a> ThermalControl<'a> { // potential TOCTOU issues; some sensors cannot be read if they are not // powered. let power_mode = self.bsp.power_mode(); - self.bsp.read_inputs( - power_mode, - self.i2c_task, - |id, value, now| self.sensor_api.post(*id, value, now), - |id, error| { - let id = *id; - // Record an error errors if the sensor is not removable - // or we get a unexpected error from a removable sensor - ringbuf_entry!(Trace::SensorReadFailed(id, error)); - self.err_blackbox.push(id, error); - self.sensor_api.nodata_now(id, error.into()); - }, - |id, error| { - self.sensor_api.nodata_now(*id, error.into()); - }, - |id| { - // If the device isn't supposed to be on in the current power - // state, then record it as Off in the sensors task. - self.sensor_api - .nodata_now(*id, task_sensor_api::NoData::DeviceOff); - }, - ); + for input in self.bsp.inputs_mut() { + let res = input.do_reading(power_mode, &self.i2c_task); + match res { + ReadingOutcome::Unpowered { id } => { + // If the device isn't supposed to be on in the current + // power state, then record it as Off in the sensors task. + self.sensor_api + .nodata_now(id, task_sensor_api::NoData::DeviceOff); + } + ReadingOutcome::AcceptableMissing { id, err } => { + self.sensor_api.nodata_now(id, err.into()); + } + ReadingOutcome::UnacceptableMissing { id, err } => { + // Record an error errors if the sensor is not removable + // or we get a unexpected error from a removable sensor + ringbuf_entry!(Trace::SensorReadFailed(id, err)); + self.err_blackbox.push(id, err); + self.sensor_api.nodata_now(id, err.into()); + } + ReadingOutcome::Success { id, now, value } => { + self.sensor_api.post(id, value.0, now); + } + } + } // Note that this function does not send data about dynamic temperature // inputs to the `sensors` task! This is because we don't know what From 29418d263548b02d6549a5578c17f93085e26e30 Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 24 Jul 2026 13:32:42 +0200 Subject: [PATCH 09/33] Split out InputMetadata to reduce static RAM usage --- task/thermal/src/bsp/cosmo_ab.rs | 59 ++++++++------- task/thermal/src/control.rs | 125 ++++++++++++++++++------------- 2 files changed, 102 insertions(+), 82 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index f93514bfe3..c3f6817f96 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -8,7 +8,8 @@ use crate::{ Fan, control::{ ChannelType, ControllerInitError, Device, FanControl, InputChannel, - Max31790State, PidConfig, TemperatureReading, TemperatureSensor, + InputChannelMetadata, Max31790State, PidConfig, TemperatureReading, + TemperatureSensor, }, i2c_config::{devices, sensors}, }; @@ -350,7 +351,7 @@ const T6_THERMALS: ThermalProperties = ThermalProperties { }; const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ - InputChannel::new( + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::M2, devices::nvme_bmc_m2_a, @@ -359,8 +360,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ M2_THERMALS, PowerBitmask::A0, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::M2, devices::nvme_bmc_m2_b, @@ -369,8 +370,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ M2_THERMALS, PowerBitmask::A0, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::CPU, devices::sbtsi_cpu, @@ -379,8 +380,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ CPU_THERMALS, PowerBitmask::A0, ChannelType::MustBePresent, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Tmp451(drv_i2c_devices::tmp451::Target::Remote), devices::tmp451_t6, @@ -391,9 +392,9 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ // controlled by the host OS. PowerBitmask::T6, ChannelType::MustBePresent, - ), + )), // U.2 drives - InputChannel::new( + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n0, @@ -402,8 +403,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n1, @@ -412,8 +413,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n2, @@ -422,8 +423,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n3, @@ -432,8 +433,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n4, @@ -442,8 +443,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n5, @@ -452,8 +453,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n6, @@ -462,8 +463,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n7, @@ -472,8 +473,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n8, @@ -482,8 +483,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n9, @@ -492,7 +493,7 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), + )), ]; const MISC_SENSORS: [TemperatureSensor; NUM_TEMPERATURE_SENSORS] = [ diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 55a2ff5001..8d502883e4 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -125,9 +125,7 @@ impl<'a> FanControl<'a> { //////////////////////////////////////////////////////////////////////////////// -/// An `InputChannel` represents a temperature sensor associated with a -/// particular component in the system. -pub(crate) struct InputChannel { +pub(crate) struct InputChannelMetadata { /// Temperature sensor sensor: TemperatureSensor, @@ -139,7 +137,12 @@ pub(crate) struct InputChannel { /// Channel type ty: ChannelType, +} +/// An `InputChannel` represents a temperature sensor associated with a +/// particular component in the system. +pub(crate) struct InputChannel { + metadata: &'static InputChannelMetadata, last_reading: Option, } @@ -183,18 +186,17 @@ pub(crate) enum ChannelType { RemovableAndErrorProne, } -pub enum ReadingOutcome { - Unpowered { - id: SensorId, - }, - AcceptableMissing { - id: SensorId, - err: SensorReadError, - }, - UnacceptableMissing { - id: SensorId, - err: SensorReadError, - }, +/// The outcome of [`InputChannel::do_reading()`]. +pub enum InputReadingOutcome { + /// Sensor was not read because the power mode indicated that it would not + /// be enabled in this state. + Unpowered { id: SensorId }, + /// This sensor was missing, but it's okay because it was either Removable + /// and not there, or Removable and Error Prone and had any kind of error. + AcceptableMissing { id: SensorId, err: SensorReadError }, + /// Any read error that didn't match the "Acceptable" cases listed above. + UnacceptableMissing { id: SensorId, err: SensorReadError }, + /// We read the data! Hooray! Success { id: SensorId, now: u64, @@ -202,7 +204,16 @@ pub enum ReadingOutcome { }, } -impl InputChannel { +/// InputChannelMetadata is the constant description portion of an InputChannel. +/// +/// We split it off because InputChannel is mutable to contain the last state, +/// and if we left it inlined, then we would end up including all of this +/// metadata in RAM, despite never changing it! So instead, we break it off and +/// have InputChannel hold an `&'static` reference instead, so we only waste +/// a wee little pointer (4 bytes) to flash space in each InputChannel entry, +/// instead of (at the time of writing) 36 bytes, which for example in Cosmo +/// that has 14 input channels, is over 500 bytes of RAM! +impl InputChannelMetadata { #[allow(dead_code)] // not all BSPS pub const fn new( sensor: TemperatureSensor, @@ -215,6 +226,15 @@ impl InputChannel { model, power_mode_mask, ty, + } + } +} + +impl InputChannel { + #[allow(dead_code)] // not all BSPS + pub const fn new(metadata: &'static InputChannelMetadata) -> Self { + Self { + metadata, last_reading: None, } } @@ -228,44 +248,41 @@ impl InputChannel { } pub fn sensor_id(&self) -> SensorId { - self.sensor.sensor_id + self.metadata.sensor.sensor_id } pub fn model(&self) -> ThermalProperties { - self.model + self.metadata.model } pub fn do_reading( &mut self, mode: PowerBitmask, i2c_task: &TaskId, - ) -> ReadingOutcome { - let id = self.sensor.sensor_id; - if !mode.intersects(self.power_mode_mask) { + ) -> InputReadingOutcome { + let id = self.metadata.sensor.sensor_id; + + // If we're not supposed to be on, don't even ask. + if !mode.intersects(self.metadata.power_mode_mask) { self.last_reading = Some(TemperatureReading::Inactive); - return ReadingOutcome::Unpowered { id }; + return InputReadingOutcome::Unpowered { id }; } - match self.sensor.read_temp(*i2c_task) { + match self.metadata.sensor.read_temp(*i2c_task) { Ok(value) => { - // TODO: Slightly different time than will be reported! We can - // fix this by providing the time instead of the helper - // functions that get the now time for us. - // let now = on_success(&s.sensor.sensor_id, v.0); let now = sys_get_timer().now; - self.last_reading = Some(TemperatureReading::Valid( TimestampedTemperatureReading { time_ms: now, value, }, )); - ReadingOutcome::Success { id, now, value } + InputReadingOutcome::Success { id, now, value } } Err(e) => { - // However, when we later would have stored the state value - // for persistence in `run_control`, that used slightly - // different logic, ONLY clearing the persisted value if: + // This is mimicing the old state value logic for deciding if + // we persist the data in `run_control`, that ONLY cleared the + // persisted value if: // // - The sensor is not present AND removable // - The sensor is error prone @@ -276,7 +293,7 @@ impl InputChannel { let e1 = e.clone(); let e2 = NoData::from(e1); let e3 = SensorError::from(e2); - match (self.ty, e3) { + match (self.metadata.ty, e3) { (ChannelType::Removable, SensorError::NotPresent) => { self.last_reading = None; } @@ -290,14 +307,25 @@ impl InputChannel { } } - // The current `unexpected_failure` comes from - // `read_sensors`, which is only deciding whether it's worth - // logging about. In either case, it will push NoData to the - // sensor api. - if unexpected_failure(self, e) { - ReadingOutcome::UnacceptableMissing { id, err: e } + // This logic comes from what was done in `read_sensors`, + // which is only deciding whether it's worth logging about. + // In either case, it will push NoData to the sensor api. + // + // This is *not* the same logic that is used above to decide + // whether we clear the previous state or not, despite being + // *very* similar! + let removable = matches!( + self.metadata.ty, + ChannelType::Removable + | ChannelType::RemovableAndErrorProne + ); + let removed = + e == SensorReadError::I2cError(ResponseCode::NoDevice); + let unexpected_failure = !(removable && removed); + if unexpected_failure { + InputReadingOutcome::UnacceptableMissing { id, err: e } } else { - ReadingOutcome::AcceptableMissing { id, err: e } + InputReadingOutcome::AcceptableMissing { id, err: e } } } } @@ -1005,23 +1033,23 @@ impl<'a> ThermalControl<'a> { for input in self.bsp.inputs_mut() { let res = input.do_reading(power_mode, &self.i2c_task); match res { - ReadingOutcome::Unpowered { id } => { + InputReadingOutcome::Unpowered { id } => { // If the device isn't supposed to be on in the current // power state, then record it as Off in the sensors task. self.sensor_api .nodata_now(id, task_sensor_api::NoData::DeviceOff); } - ReadingOutcome::AcceptableMissing { id, err } => { + InputReadingOutcome::AcceptableMissing { id, err } => { self.sensor_api.nodata_now(id, err.into()); } - ReadingOutcome::UnacceptableMissing { id, err } => { + InputReadingOutcome::UnacceptableMissing { id, err } => { // Record an error errors if the sensor is not removable // or we get a unexpected error from a removable sensor ringbuf_entry!(Trace::SensorReadFailed(id, err)); self.err_blackbox.push(id, err); self.sensor_api.nodata_now(id, err.into()); } - ReadingOutcome::Success { id, now, value } => { + InputReadingOutcome::Success { id, now, value } => { self.sensor_api.post(id, value.0, now); } } @@ -1472,12 +1500,3 @@ impl<'a> ThermalControl<'a> { Ok(()) } } - -pub fn unexpected_failure(s: &InputChannel, e: SensorReadError) -> bool { - let removable = matches!( - s.ty, - ChannelType::Removable | ChannelType::RemovableAndErrorProne - ); - let removed = e == SensorReadError::I2cError(ResponseCode::NoDevice); - !(removable && removed) -} From dc7a5976cabe95da7d06bd59dddf743e602898ca Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 24 Jul 2026 15:52:28 +0200 Subject: [PATCH 10/33] More putzing --- task/sensor-api/src/lib.rs | 12 ++-- task/thermal/src/bsp/cosmo_ab.rs | 51 ++++------------- task/thermal/src/control.rs | 97 ++++++++++++++++++-------------- 3 files changed, 72 insertions(+), 88 deletions(-) diff --git a/task/sensor-api/src/lib.rs b/task/sensor-api/src/lib.rs index 6d2ce98e59..6801a212e2 100644 --- a/task/sensor-api/src/lib.rs +++ b/task/sensor-api/src/lib.rs @@ -221,18 +221,14 @@ impl From for SensorError { impl Sensor { /// Post the given data with a timestamp of now #[inline] - pub fn post_now(&self, id: SensorId, value: f32) -> u64 { - let now = sys_get_timer().now; - self.post(id, value, now); - now + pub fn post_now(&self, id: SensorId, value: f32) { + self.post(id, value, sys_get_timer().now) } /// Post the given `NoData` error with a timestamp of now #[inline] - pub fn nodata_now(&self, id: SensorId, nodata: NoData) -> u64 { - let now = sys_get_timer().now; - self.nodata(id, nodata, now); - now + pub fn nodata_now(&self, id: SensorId, nodata: NoData) { + self.nodata(id, nodata, sys_get_timer().now) } } diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index c3f6817f96..f94ff0f212 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -8,7 +8,7 @@ use crate::{ Fan, control::{ ChannelType, ControllerInitError, Device, FanControl, InputChannel, - InputChannelMetadata, Max31790State, PidConfig, TemperatureReading, + InputChannelMetadata, InputStatus, Max31790State, PidConfig, TemperatureSensor, }, i2c_config::{devices, sensors}, @@ -150,20 +150,8 @@ impl Bsp { } } - pub fn read_misc_sensors( - &mut self, - i2c_task: TaskId, - mut on_success: impl FnMut(&SensorId, f32, u64), - mut on_error: impl FnMut(&SensorId, SensorReadError), - ) { - for s in self.misc_sensors.iter() { - // TODO: Record state! - let now = sys_get_timer().now; - match s.read_temp(i2c_task) { - Ok(v) => on_success(&s.sensor_id, v.0, now), - Err(e) => on_error(&s.sensor_id, e), - }; - } + pub fn misc_sensors(&self) -> impl Iterator { + self.misc_sensors.iter() } pub fn inputs_mut(&mut self) -> impl Iterator { @@ -199,41 +187,26 @@ impl Bsp { } pub fn all_inputs_present(&self) -> bool { - self.inputs.iter().all(|i| i.last_reading().is_some()) + self.inputs.iter().all(InputChannel::has_reading) // && self.dynamic_inputs... } // Visit all temperature sensors, first the inputs, then the dynamic_inputs. // Inputs and Dynamic Inputs that are missing will be skipped. - pub fn for_each_temp_allow_missing_inputs( + pub fn all_inputs_allow_missing( &self, - mut f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), - ) { - let iter = self.inputs.iter().filter_map(|input| { - let last = input.last_reading()?; - Some((input.sensor_id(), last, input.model())) - }); - for (sensor_id, reading, model) in iter { - f(sensor_id, *reading, model); - } - - // for _dinput in self.dynamic_inputs... + ) -> impl Iterator> { + self.inputs.iter().filter_map(InputChannel::status) + // .zip(self.dynamic_inputs...) } // Visit all temperature sensors, first the inputs, then the dynamic_inputs. // All inputs MUST have a previous reading or this will panic, though the // readings may be allowed to be Missing if the model allows it. Dynamic // inputs that are not present will be skipped. - pub fn for_each_temp( - &self, - mut f: impl FnMut(SensorId, TemperatureReading, ThermalProperties), - ) { - for input in self.inputs.iter() { - let reading = input.last_reading().unwrap_lite(); - f(input.sensor_id(), *reading, input.model()); - } - - // for _dinput in self.dynamic_inputs... + pub fn all_inputs(&self) -> impl Iterator> { + self.inputs.iter().map(|input| input.status().unwrap_lite()) + // .zip(self.dynamic_inputs...) } pub fn reset_all_values(&mut self) { @@ -484,7 +457,7 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ PowerBitmask::A0, ChannelType::RemovableAndErrorProne, )), - InputChannel::new(&&InputChannelMetadata::new( + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n9, diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 8d502883e4..5c2ee41d34 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -204,6 +204,13 @@ pub enum InputReadingOutcome { }, } +/// Status of a regular or dynamic input +pub struct InputStatus<'a> { + pub id: SensorId, + pub reading: &'a TemperatureReading, + pub model: &'a ThermalProperties, +} + /// InputChannelMetadata is the constant description portion of an InputChannel. /// /// We split it off because InputChannel is mutable to contain the last state, @@ -239,20 +246,24 @@ impl InputChannel { } } - pub fn last_reading(&self) -> Option<&TemperatureReading> { - self.last_reading.as_ref() - } - - pub fn reset_value(&mut self) { - self.last_reading = None; + pub fn has_reading(&self) -> bool { + self.last_reading.is_some() } - pub fn sensor_id(&self) -> SensorId { - self.metadata.sensor.sensor_id + /// Get current stored status. + /// + /// Returns None if we do not have a reading stored. + pub fn status(&self) -> Option> { + let reading = self.last_reading.as_ref()?; + Some(InputStatus { + id: self.metadata.sensor.sensor_id, + reading, + model: &self.metadata.model, + }) } - pub fn model(&self) -> ThermalProperties { - self.metadata.model + pub fn reset_value(&mut self) { + self.last_reading = None; } pub fn do_reading( @@ -290,10 +301,8 @@ impl InputChannel { // Replicate that logic here, doing some type shenanigans // because we aren't round-tripping through the Sensor API // anymore. - let e1 = e.clone(); - let e2 = NoData::from(e1); - let e3 = SensorError::from(e2); - match (self.metadata.ty, e3) { + let se = SensorError::from(NoData::from(e)); + match (self.metadata.ty, se) { (ChannelType::Removable, SensorError::NotPresent) => { self.last_reading = None; } @@ -1016,15 +1025,19 @@ impl<'a> ThermalControl<'a> { ); // Read miscellaneous temperature data and log it to the sensors task - self.bsp.read_misc_sensors( - self.i2c_task, - |id, value, now| self.sensor_api.post(*id, value, now), - |id, error| { - ringbuf_entry!(Trace::MiscReadFailed(*id, error)); - self.err_blackbox.push(*id, error); - self.sensor_api.nodata_now(*id, error.into()); - }, - ); + // + // We don't retain state for misc sensors, as that is all stored in the + // sensor task itself. We're just in charge of polling them. + for s in self.bsp.misc_sensors() { + match s.read_temp(self.i2c_task) { + Ok(v) => self.sensor_api.post_now(s.sensor_id, v.0), + Err(e) => { + ringbuf_entry!(Trace::MiscReadFailed(s.sensor_id, e)); + self.err_blackbox.push(s.sensor_id, e); + self.sensor_api.nodata_now(s.sensor_id, e.into()) + } + } + } // We read the power mode right before reading sensors, to avoid // potential TOCTOU issues; some sensors cannot be read if they are not @@ -1033,11 +1046,8 @@ impl<'a> ThermalControl<'a> { for input in self.bsp.inputs_mut() { let res = input.do_reading(power_mode, &self.i2c_task); match res { - InputReadingOutcome::Unpowered { id } => { - // If the device isn't supposed to be on in the current - // power state, then record it as Off in the sensors task. - self.sensor_api - .nodata_now(id, task_sensor_api::NoData::DeviceOff); + InputReadingOutcome::Success { id, now, value } => { + self.sensor_api.post(id, value.0, now); } InputReadingOutcome::AcceptableMissing { id, err } => { self.sensor_api.nodata_now(id, err.into()); @@ -1049,8 +1059,11 @@ impl<'a> ThermalControl<'a> { self.err_blackbox.push(id, err); self.sensor_api.nodata_now(id, err.into()); } - InputReadingOutcome::Success { id, now, value } => { - self.sensor_api.post(id, value.0, now); + InputReadingOutcome::Unpowered { id } => { + // If the device isn't supposed to be on in the current + // power state, then record it as Off in the sensors task. + self.sensor_api + .nodata_now(id, task_sensor_api::NoData::DeviceOff); } } } @@ -1102,16 +1115,16 @@ impl<'a> ThermalControl<'a> { // below their max temperature; negative means someone is // overheating. We want to pick the _smallest_ margin, since // that's the part which is most overheated. - let f = |sensor_id, v, model| { - if let TemperatureReading::Valid(v) = v { + let f = |InputStatus { id, reading, model }| { + if let TemperatureReading::Valid(v) = reading { let worst_case = v.worst_case(now_ms, &model); let temperature = worst_case.worst_case_temp; all_nominal &= model.is_nominal(temperature); if model.should_power_down(temperature) { - any_power_down = Some((sensor_id, worst_case)); + any_power_down = Some((id, worst_case)); } if model.is_critical(temperature) { - any_critical = Some((sensor_id, worst_case)); + any_critical = Some((id, worst_case)); } worst_margin = worst_margin.min(model.margin(temperature).0); } @@ -1128,20 +1141,22 @@ impl<'a> ThermalControl<'a> { // given to us to activate // // We don't necessarily *check* that is the case though, and we - // might want to in the for_each_temp(_allow_missing_inputs) + // might want to in the all_inputs(_allow_missing_inputs) // functions! }; match self.state { ThermalControlState::Boot => { - self.bsp.for_each_temp_allow_missing_inputs(f) + self.bsp.all_inputs_allow_missing().for_each(f) + } + ThermalControlState::Running { .. } + | ThermalControlState::Critical { .. } + | ThermalControlState::FanParty => { + self.bsp.all_inputs().for_each(f) } - ThermalControlState::Running { .. } => self.bsp.for_each_temp(f), - ThermalControlState::Critical { .. } => self.bsp.for_each_temp(f), - ThermalControlState::FanParty => self.bsp.for_each_temp(f), ThermalControlState::Uncontrollable => { return Ok(ControlResult::PowerDown); } - } + }; // In any state, if we've reached the "any_power_down" threshold, then // it's time to go. @@ -1189,7 +1204,7 @@ impl<'a> ThermalControl<'a> { ThermalControlState::Critical { .. } => { if all_nominal { self.transition_to_running(worst_margin, now_ms) - } else if !any_critical.is_some() { + } else if any_critical.is_none() { // If all temperatures have gone below critical, but are // still above nominal, stop the overheat timeout but // continue running at 100% PWM until things go below From 49f959dd13b9e9a97a5acac47d3877788ff72ec7 Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 24 Jul 2026 17:17:41 +0200 Subject: [PATCH 11/33] I think it was wrong to split up fan presence and rpm reading. --- drv/i2c-devices/src/max31790.rs | 16 +++ task/thermal/src/bsp/cosmo_ab.rs | 171 ++++++++++++++++++------------- task/thermal/src/control.rs | 129 ++++++++++++++++------- task/thermal/src/main.rs | 21 +--- 4 files changed, 212 insertions(+), 125 deletions(-) diff --git a/drv/i2c-devices/src/max31790.rs b/drv/i2c-devices/src/max31790.rs index 2093380a44..f615faa26e 100644 --- a/drv/i2c-devices/src/max31790.rs +++ b/drv/i2c-devices/src/max31790.rs @@ -190,6 +190,22 @@ pub const MAX_FANS: u8 = 6; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Fan(u8); +impl Fan { + pub const fn new_const(idx: u8) -> Self { + if idx >= MAX_FANS { + panic!("Out of range!"); + } else { + Self(idx) + } + } +} + +impl From for u8 { + fn from(val: Fan) -> Self { + val.0 + } +} + impl TryFrom for Fan { type Error = (); /// Fans are based on a 0-based index. This should *not* be the number diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index f94ff0f212..c966c37b0c 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -5,11 +5,10 @@ //! BSP for the Cosmo rev A hardware use crate::{ - Fan, control::{ - ChannelType, ControllerInitError, Device, FanControl, InputChannel, - InputChannelMetadata, InputStatus, Max31790State, PidConfig, - TemperatureSensor, + ChannelType, ControllerInitError, Device, FanControl, FanReading, + InputChannel, InputChannelMetadata, InputStatus, Max31790State, + PidConfig, TemperatureSensor, }, i2c_config::{devices, sensors}, }; @@ -50,6 +49,9 @@ pub(crate) struct Bsp { /// Monitored sensors misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], + /// Fans + fans: &'static mut [Fan; NUM_FANS], + /// Fan control IC fctrl: Max31790State, @@ -75,21 +77,21 @@ bitflags::bitflags! { } impl Bsp { - fn fan_control( - &mut self, - fan: crate::Fan, - ) -> Result, ControllerInitError> { - let fctrl = self.fctrl.try_initialize()?; - Ok(FanControl::Max31790(fctrl, fan.0.try_into().unwrap_lite())) - } + // fn fan_control( + // &mut self, + // fan: crate::Fan, + // ) -> Result, ControllerInitError> { + // let fctrl = self.fctrl.try_initialize()?; + // Ok(FanControl::Max31790(fctrl, fan.0.try_into().unwrap_lite())) + // } - pub fn for_each_fctrl( - &mut self, - mut fctrl: impl FnMut(FanControl<'_>), - ) -> Result<(), ControllerInitError> { - fctrl(self.fan_control(0.into())?); - Ok(()) - } + // pub fn for_each_fctrl( + // &mut self, + // mut fctrl: impl FnMut(FanControl<'_>), + // ) -> Result<(), ControllerInitError> { + // fctrl(self.fan_control(0.into())?); + // Ok(()) + // } pub fn power_down(&self) -> Result<(), SeqError> { self.seq.set_state_with_reason( @@ -109,45 +111,37 @@ impl Bsp { } } - pub fn update_fan_presence( - &mut self, - _on_added: F, - _on_remove: G, - ) -> Result<(), SeqError> - where - F: Fn(&Fan), - G: Fn(&Fan), - { - // Our fans are always here, never added or removed! - Ok(()) - } - pub fn read_fan_rpms( &mut self, - mut on_success: impl FnMut(&SensorId, f32, u64), - mut on_error: impl FnMut(&SensorId, SensorReadError), - _on_missing: impl FnMut(&SensorId), - ) { - for (idx, sensor) in sensors::MAX31790_SPEED_SENSORS.iter().enumerate() - { - // TODO: Why does this use idx? - let fctrl_res = self.fan_control(Fan::from(idx)); - let fctrl = match fctrl_res { - Ok(f) => f, - Err(e) => { - on_error(sensor, SensorReadError::from(e)); - continue; + ) -> Result, SeqError> { + let mut fctrl = + self.fctrl.try_initialize().map_err(SensorReadError::from); + + Ok(self.fans.iter_mut().map(move |f| { + let was_present = f.last_reading.is_some(); + let res = fctrl.as_mut().map_err(|e| *e).and_then(|fc| { + fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) + }); + match res { + Ok(rpm) => { + f.last_reading = Some(rpm); + FanReading::PresentSuccess { + new: !was_present, + rpm, + sensor_id: f.rpm_sensor_id, + fan_id: f.bsp_data.into(), + } } - }; - - // TODO: Record state! - let now = sys_get_timer().now; - // TODO(AJM): Keep last fan RPM? - match fctrl.fan_rpm() { - Ok(reading) => on_success(sensor, reading.0.into(), now), - Err(e) => on_error(sensor, SensorReadError::I2cError(e)), - }; - } + Err(error) => { + f.last_reading = None; + FanReading::PresentError { + error, + sensor_id: f.rpm_sensor_id, + fan_id: f.bsp_data.into(), + } + } + } + })) } pub fn misc_sensors(&self) -> impl Iterator { @@ -220,23 +214,24 @@ impl Bsp { &mut self, duty: PWMDuty, ) -> Result<(), ThermalError> { - let mut last_err = Ok(()); - for idx in 0..NUM_FANS { - let fctrl_res = self.fan_control(Fan::from(idx)); - let fctrl = match fctrl_res { - Ok(f) => f, - Err(e) => { - last_err = Err(ThermalError::from(e)); - continue; - } - }; - - if fctrl.set_pwm(duty).is_err() { - last_err = Err(ThermalError::DeviceError); - } - } - - last_err + // let mut last_err = Ok(()); + // for idx in 0..NUM_FANS { + // let fctrl_res = self.fan_control(Fan::from(idx)); + // let fctrl = match fctrl_res { + // Ok(f) => f, + // Err(e) => { + // last_err = Err(ThermalError::from(e)); + // continue; + // } + // }; + + // if fctrl.set_pwm(duty).is_err() { + // last_err = Err(ThermalError::DeviceError); + // } + // } + + // last_err + todo!() } // pub fn fan_sensor_id(&self, i: usize) -> SensorId { @@ -249,10 +244,14 @@ impl Bsp { // Handle for the sequencer task, which we check for power state let seq = Sequencer::from(SEQ.get_task_id()); + static INPUTS_ONCE: static_cell::ClaimOnceCell< [InputChannel; NUM_TEMPERATURE_INPUTS], > = static_cell::ClaimOnceCell::new(INPUTS); + static FANS_ONCE: static_cell::ClaimOnceCell<[Fan; NUM_FANS]> = + static_cell::ClaimOnceCell::new(FANS); + Self { seq, fctrl, @@ -268,6 +267,7 @@ impl Bsp { }, inputs: INPUTS_ONCE.claim(), + fans: FANS_ONCE.claim(), // We monitor and log all of the air temperatures misc_sensors: &MISC_SENSORS, @@ -323,6 +323,35 @@ const T6_THERMALS: ThermalProperties = ThermalProperties { temperature_slew_deg_per_sec: 0.5, }; +// Our "bonus data" is a u8 that represents the fan's index in the i2c register +type Fan = crate::control::Fan; +const FANS: [Fan; NUM_FANS] = [ + Fan::new( + sensors::MAX31790_SPEED_SENSORS[0], + drv_i2c_devices::max31790::Fan::new_const(0), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[1], + drv_i2c_devices::max31790::Fan::new_const(1), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[2], + drv_i2c_devices::max31790::Fan::new_const(2), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[3], + drv_i2c_devices::max31790::Fan::new_const(3), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[4], + drv_i2c_devices::max31790::Fan::new_const(4), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[5], + drv_i2c_devices::max31790::Fan::new_const(5), + ), +]; + const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 5c2ee41d34..993b81b794 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -85,6 +85,46 @@ impl TemperatureSensor { } } +/// Represents the indvidual fans in the system +/// +/// Depending on the system we have diferent numbers of fans structured in +/// different ways. Not all fans are guaranteed to be there at all times so +/// their corresponding sensor is an `Option`. We should not read the RPM of +/// fans which are not present and their PWM should only be driven low. +pub struct Fan { + pub rpm_sensor_id: SensorId, + pub last_reading: Option, + pub bsp_data: D, +} + +impl Fan { + pub const fn new(rpm_sensor_id: SensorId, bsp_data: D) -> Self { + Self { + rpm_sensor_id, + last_reading: None, + bsp_data, + } + } +} + +pub enum FanReading { + PresentSuccess { + new: bool, + rpm: Rpm, + sensor_id: SensorId, + fan_id: u8, + }, + PresentError { + error: SensorReadError, + sensor_id: SensorId, + fan_id: u8, + }, + NotPresent { + new: bool, + fan_id: u8, + }, +} + //////////////////////////////////////////////////////////////////////////////// /// Enum representing any of our fan controller types, bound to one of their @@ -973,15 +1013,20 @@ impl<'a> ThermalControl<'a> { ringbuf_entry!(Trace::AutoState(self.get_state())); } - /// Get latest fan presence state - pub fn update_fan_presence(&mut self) { + /// Reads all temperature and fan RPM sensors, posting their results + /// to the sensors task API. + /// + /// Records failed sensor reads and failed posts to the sensors task in + /// the local ringbuf. In addition, records the first few failed sensor + /// read in `self.err_blackbox` for later investigation. + pub fn read_sensors(&mut self) { // Try to configure the fan watchdog, if not yet configured // // With its longest timeout of 30 seconds, this is longer than it takes // to flash on Gimlet -- and right on the edge of how long it takes to // dump. On some platforms and/or under some conditions, "humility dump" // might be able to induce the watchdog to kick, which may induce a - // flight-or-fight reaction for whomever is near the fans when they + // fight-or-flight reaction for whomever is near the fans when they // blast off... if !self.fan_watchdog_configured { match self.set_watchdog(I2cWatchdog::ThirtySeconds) { @@ -993,36 +1038,45 @@ impl<'a> ThermalControl<'a> { } } - let result = self.bsp.update_fan_presence( - |fan| ringbuf_entry!(Trace::FanAdded(*fan)), - |fan| ringbuf_entry!(Trace::FanRemoved(*fan)), - ); - - if let Err(e) = result { - ringbuf_entry!(Trace::FanPresenceUpdateFailed(e)); - } - } - - /// Reads all temperature and fan RPM sensors, posting their results - /// to the sensors task API. - /// - /// Records failed sensor reads and failed posts to the sensors task in - /// the local ringbuf. In addition, records the first few failed sensor - /// read in `self.err_blackbox` for later investigation. - pub fn read_sensors(&mut self) { // Read fan data and log it to the sensors task - self.bsp.read_fan_rpms( - |id, value, now| self.sensor_api.post(*id, value, now), - |id, error| { - ringbuf_entry!(Trace::FanReadFailed(*id, error)); - self.err_blackbox.push(*id, error); - self.sensor_api.nodata_now(*id, error.into()); - }, - |id| { - self.sensor_api - .nodata_now(*id, task_sensor_api::NoData::DeviceNotPresent); - }, - ); + match self.bsp.read_fan_rpms() { + Ok(riter) => { + for reading in riter { + match reading { + FanReading::PresentSuccess { + new, + rpm, + sensor_id, + fan_id, + } => { + if new { + ringbuf_entry!(Trace::FanAdded(fan_id)); + } + self.sensor_api.post_now(sensor_id, rpm.0 as f32) + } + FanReading::PresentError { + error, + sensor_id, + fan_id, + } => { + ringbuf_entry!(Trace::FanReadFailed( + sensor_id, error + )); + self.err_blackbox.push(sensor_id, error); + self.sensor_api.nodata_now(sensor_id, error.into()); + } + FanReading::NotPresent { new, fan_id } => { + if new { + ringbuf_entry!(Trace::FanRemoved(fan_id)); + } + } + } + } + } + Err(e) => { + ringbuf_entry!(Trace::FanPresenceUpdateFailed(e)) + } + } // Read miscellaneous temperature data and log it to the sensors task // @@ -1462,11 +1516,12 @@ impl<'a> ThermalControl<'a> { ) -> Result<(), ThermalError> { let mut result = Ok(()); - self.bsp.for_each_fctrl(|fctrl| { - if fctrl.set_watchdog(wd).is_err() { - result = Err(ThermalError::DeviceError); - } - })?; + todo!(); + // self.bsp.for_each_fctrl(|fctrl| { + // if fctrl.set_watchdog(wd).is_err() { + // result = Err(ThermalError::DeviceError); + // } + // })?; result } diff --git a/task/thermal/src/main.rs b/task/thermal/src/main.rs index 29e1c4ad50..78a21559e7 100644 --- a/task/thermal/src/main.rs +++ b/task/thermal/src/main.rs @@ -64,17 +64,6 @@ use userlib::{ units::{Celsius, PWMDuty}, }; -// We define our own Fan type, as we may have more fans than any single -// controller supports. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct Fan(u8); - -impl From for Fan { - fn from(index: usize) -> Self { - Fan(index as u8) - } -} - task_slot!(I2C, i2c_driver); task_slot!(SENSOR, sensor); @@ -152,8 +141,8 @@ enum Trace { remaining: usize, }, FanPresenceUpdateFailed(SeqError), - FanAdded(Fan), - FanRemoved(Fan), + FanAdded(u8), + FanRemoved(u8), PowerDownAt(u64), AddedDynamicInput(usize), RemovedDynamicInput(usize), @@ -352,11 +341,9 @@ impl<'a> NotificationHandler for ServerImpl<'a> { fn handle_notification(&mut self, bits: userlib::NotificationBits) { let now = sys_get_timer().now; if bits.has_timer_fired(notifications::TIMER_MASK) { - // See if any fans were removed or added since last iteration - self.control.update_fan_presence(); - // We *always* read sensor data, which does not touch the control - // loop; this simply posts results to the `sensors` task. + // loop; this simply posts results to the `sensors` task. This also + // updates the presence status of fans. self.control.read_sensors(); match self.mode { From 952d1bf6dbf1e1ccac73c18d0e1bdb2752616542 Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 24 Jul 2026 18:22:12 +0200 Subject: [PATCH 12/33] Split presence/read back up, finish cleaning up fans --- drv/i2c-devices/src/max31790.rs | 5 + task/thermal/src/bsp/cosmo_ab.rs | 106 ++++++++++----------- task/thermal/src/control.rs | 152 ++++++++++++------------------- task/thermal/src/main.rs | 6 +- 4 files changed, 121 insertions(+), 148 deletions(-) diff --git a/drv/i2c-devices/src/max31790.rs b/drv/i2c-devices/src/max31790.rs index f615faa26e..8c8af178ac 100644 --- a/drv/i2c-devices/src/max31790.rs +++ b/drv/i2c-devices/src/max31790.rs @@ -191,6 +191,11 @@ pub const MAX_FANS: u8 = 6; pub struct Fan(u8); impl Fan { + /// Create a new fan + /// + /// This panics if idx is out of range, and is intended to be used + /// at compile time for building constant values. Prefer `TryFrom` + /// at runtime when handling user provided data. pub const fn new_const(idx: u8) -> Self { if idx >= MAX_FANS { panic!("Out of range!"); diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index c966c37b0c..51dc9820e1 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -6,18 +6,19 @@ use crate::{ control::{ - ChannelType, ControllerInitError, Device, FanControl, FanReading, - InputChannel, InputChannelMetadata, InputStatus, Max31790State, - PidConfig, TemperatureSensor, + ChannelType, Device, FanPresence, FanReading, InputChannel, + InputChannelMetadata, InputStatus, Max31790State, PidConfig, + TemperatureSensor, }, i2c_config::{devices, sensors}, }; pub use drv_cpu_seq_api::SeqError; use drv_cpu_seq_api::{PowerState, Sequencer, StateChangeReason}; +use drv_i2c_devices::max31790::I2cWatchdog; use task_sensor_api::{Sensor, SensorId}; use task_thermal_api::{SensorReadError, ThermalError, ThermalProperties}; use userlib::{ - TaskId, UnwrapLite, sys_get_timer, task_slot, + TaskId, UnwrapLite, task_slot, units::{Celsius, PWMDuty}, }; @@ -49,6 +50,8 @@ pub(crate) struct Bsp { /// Monitored sensors misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], + fans_added: bool, + /// Fans fans: &'static mut [Fan; NUM_FANS], @@ -77,22 +80,6 @@ bitflags::bitflags! { } impl Bsp { - // fn fan_control( - // &mut self, - // fan: crate::Fan, - // ) -> Result, ControllerInitError> { - // let fctrl = self.fctrl.try_initialize()?; - // Ok(FanControl::Max31790(fctrl, fan.0.try_into().unwrap_lite())) - // } - - // pub fn for_each_fctrl( - // &mut self, - // mut fctrl: impl FnMut(FanControl<'_>), - // ) -> Result<(), ControllerInitError> { - // fctrl(self.fan_control(0.into())?); - // Ok(()) - // } - pub fn power_down(&self) -> Result<(), SeqError> { self.seq.set_state_with_reason( PowerState::A2, @@ -111,25 +98,40 @@ impl Bsp { } } - pub fn read_fan_rpms( + pub fn read_fan_presence( &mut self, - ) -> Result, SeqError> { + ) -> Result, SeqError> { + let report_new = !self.fans_added; + self.fans_added = true; + Ok(self.fans.iter().map(move |f| FanPresence::Present { + fan_id: f.bsp_data.into(), + new: report_new, + })) + } + + pub fn read_fan_rpms(&mut self) -> impl Iterator { + // Try to initialize the fan controller once at the start of the loop let mut fctrl = self.fctrl.try_initialize().map_err(SensorReadError::from); - Ok(self.fans.iter_mut().map(move |f| { - let was_present = f.last_reading.is_some(); - let res = fctrl.as_mut().map_err(|e| *e).and_then(|fc| { + // TODO: Maybe there's a way to make this a method on Fan that we can + // call, kind of like InputStatus? + self.fans.iter_mut().map(move |f| { + // If initialization failed, then we short circuit to return that + // original error, copied for each fan we're going to report. + let fctrl = fctrl.as_mut().map_err(|e| *e); + + // If it was a success, attempt to read the RPMs, and either report + // that success or that error for each fan rpm. + let res = fctrl.and_then(|fc| { fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) }); match res { Ok(rpm) => { f.last_reading = Some(rpm); FanReading::PresentSuccess { - new: !was_present, rpm, sensor_id: f.rpm_sensor_id, - fan_id: f.bsp_data.into(), } } Err(error) => { @@ -137,11 +139,10 @@ impl Bsp { FanReading::PresentError { error, sensor_id: f.rpm_sensor_id, - fan_id: f.bsp_data.into(), } } } - })) + }) } pub fn misc_sensors(&self) -> impl Iterator { @@ -208,35 +209,37 @@ impl Bsp { // self.dynamic_inputs... } + pub fn set_all_watchdogs( + &mut self, + watchdog: I2cWatchdog, + ) -> Result<(), ThermalError> { + // Only one watchdog to configure here! + self.fctrl + .try_initialize()? + .set_watchdog(watchdog) + .map_err(|_| ThermalError::DeviceError) + } + // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, // even if some fail. return the LAST error if any. pub fn set_all_fan_rpms( &mut self, duty: PWMDuty, ) -> Result<(), ThermalError> { - // let mut last_err = Ok(()); - // for idx in 0..NUM_FANS { - // let fctrl_res = self.fan_control(Fan::from(idx)); - // let fctrl = match fctrl_res { - // Ok(f) => f, - // Err(e) => { - // last_err = Err(ThermalError::from(e)); - // continue; - // } - // }; - - // if fctrl.set_pwm(duty).is_err() { - // last_err = Err(ThermalError::DeviceError); - // } - // } - - // last_err - todo!() - } + let fctrl = self.fctrl.try_initialize()?; + let mut any_err = false; - // pub fn fan_sensor_id(&self, i: usize) -> SensorId { - // sensors::MAX31790_SPEED_SENSORS[i] - // } + // Note: DON'T short circuit here! + for fan in self.fans.iter_mut() { + any_err |= fctrl.set_pwm(fan.bsp_data, duty).is_err(); + } + + if any_err { + Err(ThermalError::DeviceError) + } else { + Ok(()) + } + } pub fn new(i2c_task: TaskId) -> Self { // Initializes and build a handle to the fan controller IC @@ -268,6 +271,7 @@ impl Bsp { inputs: INPUTS_ONCE.claim(), fans: FANS_ONCE.claim(), + fans_added: false, // We monitor and log all of the air temperatures misc_sensors: &MISC_SENSORS, diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 993b81b794..6f85383717 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -107,64 +107,35 @@ impl Fan { } } +pub enum FanPresence { + Present { + fan_id: u8, + new: bool, + }, + #[allow(dead_code)] // Some bsps don't have removable fans + NotPresent { + fan_id: u8, + new: bool, + }, +} + pub enum FanReading { PresentSuccess { - new: bool, rpm: Rpm, sensor_id: SensorId, - fan_id: u8, }, PresentError { error: SensorReadError, sensor_id: SensorId, - fan_id: u8, }, + #[allow(dead_code)] // Some bsps don't have removable fans NotPresent { - new: bool, - fan_id: u8, + sensor_id: SensorId, }, } //////////////////////////////////////////////////////////////////////////////// -/// Enum representing any of our fan controller types, bound to one of their -/// fans. This lets us handle heterogeneous fan controller ICs generically -/// (although there's only one at the moment) -#[allow(dead_code)] // a typical BSP uses only _one_ of these -pub enum FanControl<'a> { - Max31790(&'a Max31790, drv_i2c_devices::max31790::Fan), - Emc2305(&'a Emc2305, drv_i2c_devices::emc2305::Fan), -} - -impl<'a> FanControl<'a> { - pub fn set_pwm(&self, pwm: PWMDuty) -> Result<(), ResponseCode> { - match self { - Self::Max31790(m, fan) => m.set_pwm(*fan, pwm), - Self::Emc2305(m, fan) => m.set_pwm(*fan, pwm), - } - } - - pub fn fan_rpm(&self) -> Result { - match self { - Self::Max31790(m, fan) => m.fan_rpm(*fan), - Self::Emc2305(m, fan) => m.fan_rpm(*fan), - } - } - - pub fn set_watchdog(&self, wd: I2cWatchdog) -> Result<(), ResponseCode> { - match self { - Self::Max31790(m, _fan) => m.set_watchdog(wd), - Self::Emc2305(m, _fan) => { - // The EMC2305 doesn't support setting the watchdog time, just - // whether it's enabled or disabled - m.set_watchdog(!matches!(wd, I2cWatchdog::Disabled)) - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////// - pub(crate) struct InputChannelMetadata { /// Temperature sensor sensor: TemperatureSensor, @@ -1013,20 +984,15 @@ impl<'a> ThermalControl<'a> { ringbuf_entry!(Trace::AutoState(self.get_state())); } - /// Reads all temperature and fan RPM sensors, posting their results - /// to the sensors task API. - /// - /// Records failed sensor reads and failed posts to the sensors task in - /// the local ringbuf. In addition, records the first few failed sensor - /// read in `self.err_blackbox` for later investigation. - pub fn read_sensors(&mut self) { + /// Get latest fan presence state + pub fn update_fan_presence(&mut self) { // Try to configure the fan watchdog, if not yet configured // // With its longest timeout of 30 seconds, this is longer than it takes // to flash on Gimlet -- and right on the edge of how long it takes to // dump. On some platforms and/or under some conditions, "humility dump" // might be able to induce the watchdog to kick, which may induce a - // fight-or-flight reaction for whomever is near the fans when they + // flight-or-fight reaction for whomever is near the fans when they // blast off... if !self.fan_watchdog_configured { match self.set_watchdog(I2cWatchdog::ThirtySeconds) { @@ -1038,43 +1004,49 @@ impl<'a> ThermalControl<'a> { } } - // Read fan data and log it to the sensors task - match self.bsp.read_fan_rpms() { - Ok(riter) => { - for reading in riter { - match reading { - FanReading::PresentSuccess { - new, - rpm, - sensor_id, - fan_id, - } => { - if new { - ringbuf_entry!(Trace::FanAdded(fan_id)); - } - self.sensor_api.post_now(sensor_id, rpm.0 as f32) + match self.bsp.read_fan_presence() { + Ok(piter) => { + for fan in piter { + match fan { + FanPresence::Present { fan_id, new } if new => { + ringbuf_entry!(Trace::FanAdded(fan_id)); } - FanReading::PresentError { - error, - sensor_id, - fan_id, - } => { - ringbuf_entry!(Trace::FanReadFailed( - sensor_id, error - )); - self.err_blackbox.push(sensor_id, error); - self.sensor_api.nodata_now(sensor_id, error.into()); - } - FanReading::NotPresent { new, fan_id } => { - if new { - ringbuf_entry!(Trace::FanRemoved(fan_id)); - } + FanPresence::NotPresent { fan_id, new } if new => { + ringbuf_entry!(Trace::FanRemoved(fan_id)); } + _ => {} } } } - Err(e) => { - ringbuf_entry!(Trace::FanPresenceUpdateFailed(e)) + Err(e) => ringbuf_entry!(Trace::FanPresenceUpdateFailed(e)), + } + } + + /// Reads all temperature and fan RPM sensors, posting their results + /// to the sensors task API. + /// + /// Records failed sensor reads and failed posts to the sensors task in + /// the local ringbuf. In addition, records the first few failed sensor + /// read in `self.err_blackbox` for later investigation. + pub fn read_sensors(&mut self) { + // Read fan data and log it to the sensors task + for reading in self.bsp.read_fan_rpms() { + match reading { + FanReading::PresentSuccess { rpm, sensor_id } => { + self.sensor_api.post_now(sensor_id, rpm.0 as f32) + } + FanReading::PresentError { error, sensor_id } => { + ringbuf_entry!(Trace::FanReadFailed(sensor_id, error)); + self.err_blackbox.push(sensor_id, error); + self.sensor_api.nodata_now(sensor_id, error.into()); + } + FanReading::NotPresent { sensor_id } => { + // Invalidate fan speed readings in the sensors task + self.sensor_api.nodata_now( + sensor_id, + task_sensor_api::NoData::DeviceNotPresent, + ); + } } } @@ -1308,8 +1280,7 @@ impl<'a> ThermalControl<'a> { if let Err(e) = self.bsp.power_down() { ringbuf_entry!(Trace::PowerDownFailed(e)); } - self.set_pwm(Err(task_sensor_api::NoData::DeviceOff), now_ms)?; - Ok(()) + self.set_pwm(Err(task_sensor_api::NoData::DeviceOff), now_ms) } } } @@ -1514,16 +1485,7 @@ impl<'a> ThermalControl<'a> { &mut self, wd: I2cWatchdog, ) -> Result<(), ThermalError> { - let mut result = Ok(()); - - todo!(); - // self.bsp.for_each_fctrl(|fctrl| { - // if fctrl.set_watchdog(wd).is_err() { - // result = Err(ThermalError::DeviceError); - // } - // })?; - - result + self.bsp.set_all_watchdogs(wd) } pub fn get_state(&self) -> ThermalAutoState { diff --git a/task/thermal/src/main.rs b/task/thermal/src/main.rs index 78a21559e7..f301decb80 100644 --- a/task/thermal/src/main.rs +++ b/task/thermal/src/main.rs @@ -341,9 +341,11 @@ impl<'a> NotificationHandler for ServerImpl<'a> { fn handle_notification(&mut self, bits: userlib::NotificationBits) { let now = sys_get_timer().now; if bits.has_timer_fired(notifications::TIMER_MASK) { + // See if any fans were removed or added since last iteration + self.control.update_fan_presence(); + // We *always* read sensor data, which does not touch the control - // loop; this simply posts results to the `sensors` task. This also - // updates the presence status of fans. + // loop; this simply posts results to the `sensors` task. self.control.read_sensors(); match self.mode { From d959df314cd2b2f1069cf75532f571e557716fc4 Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 24 Jul 2026 18:35:43 +0200 Subject: [PATCH 13/33] It's a trait! --- task/thermal/src/bsp/cosmo_ab.rs | 60 +++++++++++------------- task/thermal/src/control.rs | 80 +++++++++++++++++++++++++++----- task/thermal/src/main.rs | 12 +++-- 3 files changed, 104 insertions(+), 48 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 51dc9820e1..3ae4a3c483 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -60,9 +60,6 @@ pub(crate) struct Bsp { /// Handle to the sequencer task, to query power state seq: Sequencer, - - /// Tuning for the PID controller - pub pid_config: PidConfig, } bitflags::bitflags! { @@ -79,8 +76,18 @@ bitflags::bitflags! { } } -impl Bsp { - pub fn power_down(&self) -> Result<(), SeqError> { +impl crate::control::BspInterface for Bsp { + // Based on experimental tuning! + const PID_CONFIG: PidConfig = PidConfig { + zero: 35.0, + gain_p: 5.0, + gain_i: 0.0135, + gain_d: 5.0, + min_output: 0.0, + max_output: 100.0, + }; + + fn power_down(&self) -> Result<(), SeqError> { self.seq.set_state_with_reason( PowerState::A2, StateChangeReason::Overheat, @@ -88,7 +95,7 @@ impl Bsp { Ok(()) } - pub fn power_mode(&self) -> PowerBitmask { + fn power_mode(&self) -> PowerBitmask { match self.seq.get_state() { PowerState::A0PlusHP => PowerBitmask::A0_PLUS_HP, PowerState::A0 | PowerState::A0Reset => PowerBitmask::A0, @@ -98,7 +105,7 @@ impl Bsp { } } - pub fn read_fan_presence( + fn read_fan_presence( &mut self, ) -> Result, SeqError> { let report_new = !self.fans_added; @@ -109,7 +116,7 @@ impl Bsp { })) } - pub fn read_fan_rpms(&mut self) -> impl Iterator { + fn read_fan_rpms(&mut self) -> impl Iterator { // Try to initialize the fan controller once at the start of the loop let mut fctrl = self.fctrl.try_initialize().map_err(SensorReadError::from); @@ -145,17 +152,17 @@ impl Bsp { }) } - pub fn misc_sensors(&self) -> impl Iterator { + fn misc_sensors(&self) -> impl Iterator { self.misc_sensors.iter() } - pub fn inputs_mut(&mut self) -> impl Iterator { + fn inputs_mut(&mut self) -> impl Iterator { self.inputs.iter_mut() } // TODO: This probably needs to exist, but for cosmo we have no dynamic // inputs to read back. This should read from the api and store the state - pub fn read_dynamic_inputs_back_from_sensor_api( + fn read_dynamic_inputs_back_from_sensor_api( &mut self, _sensor_api: &Sensor, ) { @@ -163,7 +170,7 @@ impl Bsp { } // returns Ok(true) if this was a new input - pub fn update_dynamic_input( + fn update_dynamic_input( &mut self, _index: usize, _model: ThermalProperties, @@ -173,7 +180,7 @@ impl Bsp { } // sets last_reading to Some(Missing), returns sensor id - pub fn remove_dynamic_input( + fn remove_dynamic_input( &mut self, _index: usize, ) -> Result { @@ -181,14 +188,14 @@ impl Bsp { Err(ThermalError::InvalidIndex) } - pub fn all_inputs_present(&self) -> bool { + fn all_inputs_present(&self) -> bool { self.inputs.iter().all(InputChannel::has_reading) // && self.dynamic_inputs... } // Visit all temperature sensors, first the inputs, then the dynamic_inputs. // Inputs and Dynamic Inputs that are missing will be skipped. - pub fn all_inputs_allow_missing( + fn all_inputs_allow_missing( &self, ) -> impl Iterator> { self.inputs.iter().filter_map(InputChannel::status) @@ -199,17 +206,17 @@ impl Bsp { // All inputs MUST have a previous reading or this will panic, though the // readings may be allowed to be Missing if the model allows it. Dynamic // inputs that are not present will be skipped. - pub fn all_inputs(&self) -> impl Iterator> { + fn all_inputs(&self) -> impl Iterator> { self.inputs.iter().map(|input| input.status().unwrap_lite()) // .zip(self.dynamic_inputs...) } - pub fn reset_all_values(&mut self) { + fn reset_all_values(&mut self) { self.inputs.iter_mut().for_each(|i| i.reset_value()); // self.dynamic_inputs... } - pub fn set_all_watchdogs( + fn set_all_watchdogs( &mut self, watchdog: I2cWatchdog, ) -> Result<(), ThermalError> { @@ -222,10 +229,7 @@ impl Bsp { // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, // even if some fail. return the LAST error if any. - pub fn set_all_fan_rpms( - &mut self, - duty: PWMDuty, - ) -> Result<(), ThermalError> { + fn set_all_fan_rpms(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { let fctrl = self.fctrl.try_initialize()?; let mut any_err = false; @@ -240,7 +244,9 @@ impl Bsp { Ok(()) } } +} +impl Bsp { pub fn new(i2c_task: TaskId) -> Self { // Initializes and build a handle to the fan controller IC let fctrl = Max31790State::new(&devices::max31790(i2c_task)[0]); @@ -259,16 +265,6 @@ impl Bsp { seq, fctrl, - // Based on experimental tuning! - pid_config: PidConfig { - zero: 35.0, - gain_p: 5.0, - gain_i: 0.0135, - gain_d: 5.0, - min_output: 0.0, - max_output: 100.0, - }, - inputs: INPUTS_ONCE.claim(), fans: FANS_ONCE.claim(), fans_added: false, diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 6f85383717..62310ff003 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -2,10 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use crate::{ - ThermalError, Trace, - bsp::{Bsp, PowerBitmask}, -}; +use crate::{ThermalError, Trace, bsp::PowerBitmask}; use drv_i2c_api::{I2cDevice, ResponseCode}; use drv_i2c_devices::{ TempSensor, @@ -553,9 +550,9 @@ impl From for SensorReadError { /// This object uses slices of sensors and fans, which must be owned /// elsewhere; the standard pattern is to create static arrays in a /// `struct Bsp` which is conditionally included based on board name. -pub(crate) struct ThermalControl<'a> { +pub(crate) struct ThermalControl<'a, B: BspInterface> { /// Reference to board-specific parameters - bsp: &'a mut Bsp, + bsp: &'a mut B, /// I2C task i2c_task: TaskId, @@ -880,7 +877,7 @@ struct OverheatTimer { critical_ms: u64, } -impl<'a> ThermalControl<'a> { +impl<'a, B: BspInterface> ThermalControl<'a, B> { /// Constructs a new `ThermalControl` based on a `struct Bsp`. This /// requires that every BSP has the same internal structure, /// @@ -888,7 +885,7 @@ impl<'a> ThermalControl<'a> { /// This function can only be called once, because it claims mutable static /// buffers. pub fn new( - bsp: &'a mut Bsp, + bsp: &'a mut B, i2c_task: TaskId, sensor_api: SensorApi, ) -> Self { @@ -899,7 +896,6 @@ impl<'a> ThermalControl<'a> { ClaimOnceCell::new([ThermalSensorErrors::new(); 2]); BLACKBOXEN.claim() }; - let pid_config = bsp.pid_config; Self { bsp, @@ -907,7 +903,7 @@ impl<'a> ThermalControl<'a> { sensor_api, target_margin: Celsius(0.0f32), state: ThermalControlState::Boot, - pid_config, + pid_config: B::PID_CONFIG, power_mode: PowerBitmask::empty(), // no sensors active @@ -971,7 +967,7 @@ impl<'a> ThermalControl<'a> { self.reset_state(); // Reset the PID configuration from the BSP - self.pid_config = self.bsp.pid_config; + self.pid_config = B::PID_CONFIG; // Set the target_margin to 0, indicating no overcooling self.target_margin = Celsius(0.0f32); @@ -1532,3 +1528,65 @@ impl<'a> ThermalControl<'a> { Ok(()) } } + +pub trait BspInterface { + const PID_CONFIG: PidConfig; + + fn power_down(&self) -> Result<(), crate::SeqError>; + + fn power_mode(&self) -> PowerBitmask; + + fn read_fan_presence( + &mut self, + ) -> Result, crate::SeqError>; + + fn read_fan_rpms(&mut self) -> impl Iterator; + + fn misc_sensors(&self) -> impl Iterator; + + fn inputs_mut(&mut self) -> impl Iterator; + + // TODO: This probably needs to exist, but for cosmo we have no dynamic + // inputs to read back. This should read from the api and store the state + fn read_dynamic_inputs_back_from_sensor_api( + &mut self, + sensor_api: &task_sensor_api::Sensor, + ); + + // returns Ok(true) if this was a new input + fn update_dynamic_input( + &mut self, + index: usize, + model: ThermalProperties, + ) -> Result; + + // sets last_reading to Some(Missing), returns sensor id + fn remove_dynamic_input( + &mut self, + index: usize, + ) -> Result; + + fn all_inputs_present(&self) -> bool; + + // Visit all temperature sensors, first the inputs, then the dynamic_inputs. + // Inputs and Dynamic Inputs that are missing will be skipped. + fn all_inputs_allow_missing(&self) + -> impl Iterator>; + + // Visit all temperature sensors, first the inputs, then the dynamic_inputs. + // All inputs MUST have a previous reading or this will panic, though the + // readings may be allowed to be Missing if the model allows it. Dynamic + // inputs that are not present will be skipped. + fn all_inputs(&self) -> impl Iterator>; + + fn reset_all_values(&mut self); + + fn set_all_watchdogs( + &mut self, + watchdog: I2cWatchdog, + ) -> Result<(), ThermalError>; + + // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, + // even if some fail. return the LAST error if any. + fn set_all_fan_rpms(&mut self, duty: PWMDuty) -> Result<(), ThermalError>; +} diff --git a/task/thermal/src/main.rs b/task/thermal/src/main.rs index f301decb80..efb76dea2a 100644 --- a/task/thermal/src/main.rs +++ b/task/thermal/src/main.rs @@ -153,16 +153,16 @@ counted_ringbuf!(Trace, 32, Trace::None); //////////////////////////////////////////////////////////////////////////////// -struct ServerImpl<'a> { +struct ServerImpl<'a, B: control::BspInterface> { mode: ThermalMode, - control: ThermalControl<'a>, + control: ThermalControl<'a, B>, deadline: u64, runtime: u64, } const TIMER_INTERVAL: u64 = 1000; -impl<'a> ServerImpl<'a> { +impl<'a, B: control::BspInterface> ServerImpl<'a, B> { /// Configures the control loop to run in manual mode, loading the given /// PWM value immediately to all fans. /// @@ -203,7 +203,9 @@ impl<'a> ServerImpl<'a> { } } -impl<'a> idl::InOrderThermalImpl for ServerImpl<'a> { +impl<'a, B: control::BspInterface> idl::InOrderThermalImpl + for ServerImpl<'a, B> +{ fn get_mode( &mut self, _: &RecvMessage, @@ -333,7 +335,7 @@ impl<'a> idl::InOrderThermalImpl for ServerImpl<'a> { } } -impl<'a> NotificationHandler for ServerImpl<'a> { +impl<'a, B: control::BspInterface> NotificationHandler for ServerImpl<'a, B> { fn current_notification_mask(&self) -> u32 { notifications::TIMER_MASK } From a05d9fbe4a20980543d69e970914362bdac75c5f Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 24 Jul 2026 20:03:31 +0200 Subject: [PATCH 14/33] Port over gimlet to new style impl --- task/thermal/src/bsp/gimlet_bcdef.rs | 384 +++++++++++++++++++-------- 1 file changed, 268 insertions(+), 116 deletions(-) diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index d1af16c9bd..0f4d4d7749 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -6,16 +6,21 @@ use crate::{ control::{ - ChannelType, ControllerInitError, Device, FanControl, Fans, - InputChannel, Max31790State, PidConfig, TemperatureSensor, + ChannelType, Device, FanPresence, FanReading, InputChannel, + InputChannelMetadata, InputStatus, Max31790State, PidConfig, + TemperatureSensor, }, i2c_config::{devices, sensors}, }; pub use drv_cpu_seq_api::SeqError; use drv_cpu_seq_api::{PowerState, Sequencer, StateChangeReason}; -use task_sensor_api::SensorId; -use task_thermal_api::ThermalProperties; -use userlib::{TaskId, UnwrapLite, task_slot, units::Celsius}; +use drv_i2c_devices::max31790::I2cWatchdog; +use task_sensor_api::{Sensor, SensorId}; +use task_thermal_api::{SensorReadError, ThermalError, ThermalProperties}; +use userlib::{ + TaskId, UnwrapLite, task_slot, + units::{Celsius, PWMDuty}, +}; task_slot!(SEQ, gimlet_seq); @@ -43,22 +48,23 @@ pub const NUM_TEMPERATURE_INPUTS: usize = sensors::NUM_SBTSI_TEMPERATURE_SENSORS + sensors::NUM_TSE2004AV_TEMPERATURE_SENSORS + NUM_NVME_BMC_TEMPERATURE_SENSORS; -// Every temperature sensor on Gimlet is owned by this task -pub const NUM_DYNAMIC_TEMPERATURE_INPUTS: usize = 0; - // We've got 6 fans, driven from a single MAX31790 IC -pub const NUM_FANS: usize = drv_i2c_devices::max31790::MAX_FANS as usize; +const NUM_FANS: usize = drv_i2c_devices::max31790::MAX_FANS as usize; /// This controller is tuned and ready to go pub const USE_CONTROLLER: bool = true; pub(crate) struct Bsp { /// Controlled sensors - pub inputs: &'static [InputChannel; NUM_TEMPERATURE_INPUTS], - pub dynamic_inputs: &'static [SensorId; NUM_DYNAMIC_TEMPERATURE_INPUTS], + inputs: &'static mut [InputChannel; NUM_TEMPERATURE_INPUTS], /// Monitored sensors - pub misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], + misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], + + fans_added: bool, + + /// Fans + fans: &'static mut [Fan; NUM_FANS], /// Fan control IC fctrl: Max31790State, @@ -68,9 +74,6 @@ pub(crate) struct Bsp { /// Id of the I2C task, to query MAX5970 status i2c_task: TaskId, - - /// Tuning for the PID controller - pub pid_config: PidConfig, } bitflags::bitflags! { @@ -94,24 +97,18 @@ bitflags::bitflags! { } } -impl Bsp { - pub fn fan_control( - &mut self, - fan: crate::Fan, - ) -> Result, ControllerInitError> { - let fctrl = self.fctrl.try_initialize()?; - Ok(FanControl::Max31790(fctrl, fan.0.try_into().unwrap_lite())) - } - - pub fn for_each_fctrl( - &mut self, - mut fctrl: impl FnMut(FanControl<'_>), - ) -> Result<(), ControllerInitError> { - fctrl(self.fan_control(0.into())?); - Ok(()) - } - - pub fn power_down(&self) -> Result<(), SeqError> { +impl crate::control::BspInterface for Bsp { + // Based on experimental tuning! + const PID_CONFIG: PidConfig = PidConfig { + zero: 35.0, + gain_p: 1.75, + gain_i: 0.0135, + gain_d: 0.4, + min_output: 0.0, + max_output: 100.0, + }; + + fn power_down(&self) -> Result<(), SeqError> { self.seq.set_state_with_reason( PowerState::A2, StateChangeReason::Overheat, @@ -119,7 +116,7 @@ impl Bsp { Ok(()) } - pub fn power_mode(&self) -> PowerBitmask { + fn power_mode(&self) -> PowerBitmask { match self.seq.get_state() { PowerState::A0PlusHP | PowerState::A0 | PowerState::A0Reset => { use drv_i2c_devices::max5970; @@ -157,20 +154,148 @@ impl Bsp { } // We assume Gimlet fan presence cannot change - pub fn get_fan_presence(&self) -> Result, SeqError> { - // Awkwardly build the fan array, because there's not a great way to - // build a fixed-size array from a function - let mut fans = Fans::new(); - for i in 0..NUM_FANS { - fans[i] = Some(sensors::MAX31790_SPEED_SENSORS[i]); - } - Ok(fans) + fn read_fan_presence( + &mut self, + ) -> Result, SeqError> { + let report_new = !self.fans_added; + self.fans_added = true; + Ok(self.fans.iter().map(move |f| FanPresence::Present { + fan_id: f.bsp_data.into(), + new: report_new, + })) + } + + fn read_fan_rpms(&mut self) -> impl Iterator { + // Try to initialize the fan controller once at the start of the loop + let mut fctrl = + self.fctrl.try_initialize().map_err(SensorReadError::from); + + // TODO: Maybe there's a way to make this a method on Fan that we can + // call, kind of like InputStatus? + self.fans.iter_mut().map(move |f| { + // If initialization failed, then we short circuit to return that + // original error, copied for each fan we're going to report. + let fctrl = fctrl.as_mut().map_err(|e| *e); + + // If it was a success, attempt to read the RPMs, and either report + // that success or that error for each fan rpm. + let res = fctrl.and_then(|fc| { + fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) + }); + match res { + Ok(rpm) => { + f.last_reading = Some(rpm); + FanReading::PresentSuccess { + rpm, + sensor_id: f.rpm_sensor_id, + } + } + Err(error) => { + f.last_reading = None; + FanReading::PresentError { + error, + sensor_id: f.rpm_sensor_id, + } + } + } + }) + } + + fn misc_sensors(&self) -> impl Iterator { + self.misc_sensors.iter() + } + + fn inputs_mut(&mut self) -> impl Iterator { + self.inputs.iter_mut() + } + + // TODO: This probably needs to exist, but for gimlet we have no dynamic + // inputs to read back. This should read from the api and store the state + fn read_dynamic_inputs_back_from_sensor_api( + &mut self, + _sensor_api: &Sensor, + ) { + // No dynamic inputs here + } + + // returns Ok(true) if this was a new input + fn update_dynamic_input( + &mut self, + _index: usize, + _model: ThermalProperties, + ) -> Result { + // No dynamic inputs here, todo: static assert this + Err(ThermalError::InvalidIndex) + } + + // sets last_reading to Some(Missing), returns sensor id + fn remove_dynamic_input( + &mut self, + _index: usize, + ) -> Result { + // No dynamic inputs here, todo: static assert this + Err(ThermalError::InvalidIndex) } - pub fn fan_sensor_id(&self, i: usize) -> SensorId { - sensors::MAX31790_SPEED_SENSORS[i] + fn all_inputs_present(&self) -> bool { + self.inputs.iter().all(InputChannel::has_reading) + // && self.dynamic_inputs... } + // Visit all temperature sensors, first the inputs, then the dynamic_inputs. + // Inputs and Dynamic Inputs that are missing will be skipped. + fn all_inputs_allow_missing( + &self, + ) -> impl Iterator> { + self.inputs.iter().filter_map(InputChannel::status) + // .zip(self.dynamic_inputs...) + } + + // Visit all temperature sensors, first the inputs, then the dynamic_inputs. + // All inputs MUST have a previous reading or this will panic, though the + // readings may be allowed to be Missing if the model allows it. Dynamic + // inputs that are not present will be skipped. + fn all_inputs(&self) -> impl Iterator> { + self.inputs.iter().map(|input| input.status().unwrap_lite()) + // .zip(self.dynamic_inputs...) + } + + fn reset_all_values(&mut self) { + self.inputs.iter_mut().for_each(|i| i.reset_value()); + // self.dynamic_inputs... + } + + fn set_all_watchdogs( + &mut self, + watchdog: I2cWatchdog, + ) -> Result<(), ThermalError> { + // Only one watchdog to configure here! + self.fctrl + .try_initialize()? + .set_watchdog(watchdog) + .map_err(|_| ThermalError::DeviceError) + } + + // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, + // even if some fail. return the LAST error if any. + fn set_all_fan_rpms(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { + let fctrl = self.fctrl.try_initialize()?; + let mut any_err = false; + + // Note: DON'T short circuit here! + for fan in self.fans.iter_mut() { + any_err |= fctrl.set_pwm(fan.bsp_data, duty).is_err(); + } + + if any_err { + Err(ThermalError::DeviceError) + } else { + Ok(()) + } + } +} + +impl Bsp { pub fn new(i2c_task: TaskId) -> Self { // Initializes and build a handle to the fan controller IC let fctrl = Max31790State::new(&devices::max31790(i2c_task)[0]); @@ -178,23 +303,21 @@ impl Bsp { // Handle for the sequencer task, which we check for power state let seq = Sequencer::from(SEQ.get_task_id()); + static INPUTS_ONCE: static_cell::ClaimOnceCell< + [InputChannel; NUM_TEMPERATURE_INPUTS], + > = static_cell::ClaimOnceCell::new(INPUTS); + + static FANS_ONCE: static_cell::ClaimOnceCell<[Fan; NUM_FANS]> = + static_cell::ClaimOnceCell::new(FANS); + Self { seq, i2c_task, fctrl, - // Based on experimental tuning! - pid_config: PidConfig { - zero: 35.0, - gain_p: 1.75, - gain_i: 0.0135, - gain_d: 0.4, - min_output: 0.0, - max_output: 100.0, - }, - - inputs: &INPUTS, - dynamic_inputs: &[], + inputs: INPUTS_ONCE.claim(), + fans: FANS_ONCE.claim(), + fans_added: false, // We monitor and log all of the air temperatures misc_sensors: &MISC_SENSORS, @@ -260,13 +383,42 @@ const T6_THERMALS: ThermalProperties = ThermalProperties { temperature_slew_deg_per_sec: 0.5, }; +// Our "bonus data" is a u8 that represents the fan's index in the i2c register +type Fan = crate::control::Fan; +const FANS: [Fan; NUM_FANS] = [ + Fan::new( + sensors::MAX31790_SPEED_SENSORS[0], + drv_i2c_devices::max31790::Fan::new_const(0), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[1], + drv_i2c_devices::max31790::Fan::new_const(1), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[2], + drv_i2c_devices::max31790::Fan::new_const(2), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[3], + drv_i2c_devices::max31790::Fan::new_const(3), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[4], + drv_i2c_devices::max31790::Fan::new_const(4), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[5], + drv_i2c_devices::max31790::Fan::new_const(5), + ), +]; + const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ // The M.2 devices are polled first deliberately: they're only polled if // powered, and we want to minimize the TOCTOU window between asking the // MAX5970 "is it powered?" and actually reading data. // // See hardware-gimlet#1804 for details; this is fixed in later revisions. - InputChannel::new( + InputChannel::new(&InputChannelMetadata::new( #[cfg(any(target_board = "gimlet-b", target_board = "gimlet-c"))] TemperatureSensor::new( Device::M2, @@ -286,8 +438,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ M2_THERMALS, PowerBitmask::M2A, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( #[cfg(any(target_board = "gimlet-b", target_board = "gimlet-c"))] TemperatureSensor::new( Device::M2, @@ -307,8 +459,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ M2_THERMALS, PowerBitmask::M2B, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::CPU, devices::sbtsi_cpu, @@ -317,8 +469,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ CPU_THERMALS, PowerBitmask::A0, ChannelType::MustBePresent, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Tmp451(drv_i2c_devices::tmp451::Target::Remote), devices::tmp451_t6, @@ -327,8 +479,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ T6_THERMALS, PowerBitmask::A0, ChannelType::MustBePresent, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_a0, @@ -337,8 +489,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_a1, @@ -347,8 +499,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_b0, @@ -357,8 +509,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_b1, @@ -367,8 +519,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_c0, @@ -377,8 +529,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_c1, @@ -387,8 +539,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_d0, @@ -397,8 +549,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_d1, @@ -407,8 +559,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_e0, @@ -417,8 +569,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_e1, @@ -427,8 +579,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_f0, @@ -437,8 +589,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_f1, @@ -447,8 +599,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_g0, @@ -457,8 +609,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_g1, @@ -467,8 +619,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_h0, @@ -477,8 +629,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Dimm, devices::tse2004av_dimm_h1, @@ -487,9 +639,9 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ DIMM_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::Removable, - ), + )), // U.2 drives - InputChannel::new( + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n0, @@ -498,8 +650,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n1, @@ -508,8 +660,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n2, @@ -518,8 +670,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n3, @@ -528,8 +680,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n4, @@ -538,8 +690,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n5, @@ -548,8 +700,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n6, @@ -558,8 +710,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n7, @@ -568,8 +720,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n8, @@ -578,8 +730,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::U2, devices::nvme_bmc_u2_n9, @@ -588,7 +740,7 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ U2_THERMALS, PowerBitmask::A0, ChannelType::RemovableAndErrorProne, - ), + )), ]; const MISC_SENSORS: [TemperatureSensor; NUM_TEMPERATURE_SENSORS] = [ From 36f5ac89cad714084c4332e37d1264cd1f907679 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 11:56:14 +0200 Subject: [PATCH 15/33] Address review comments --- task/thermal/src/bsp/cosmo_ab.rs | 31 +++++++++++---- task/thermal/src/bsp/gimlet_bcdef.rs | 26 +++++++++---- task/thermal/src/control.rs | 57 ++++++++++++++++------------ task/thermal/src/main.rs | 2 +- 4 files changed, 76 insertions(+), 40 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 3ae4a3c483..7b47dd6612 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -7,8 +7,8 @@ use crate::{ control::{ ChannelType, Device, FanPresence, FanReading, InputChannel, - InputChannelMetadata, InputStatus, Max31790State, PidConfig, - TemperatureSensor, + InputChannelMetadata, InputReadingOutcome, InputStatus, Max31790State, + PidConfig, TemperatureSensor, }, i2c_config::{devices, sensors}, }; @@ -60,6 +60,9 @@ pub(crate) struct Bsp { /// Handle to the sequencer task, to query power state seq: Sequencer, + + /// Id of the I2C task, to query MAX5970 status + i2c_task: TaskId, } bitflags::bitflags! { @@ -105,6 +108,7 @@ impl crate::control::BspInterface for Bsp { } } + // We assume Cosmo fan presence cannot change fn read_fan_presence( &mut self, ) -> Result, SeqError> { @@ -152,12 +156,24 @@ impl crate::control::BspInterface for Bsp { }) } - fn misc_sensors(&self) -> impl Iterator { - self.misc_sensors.iter() + fn read_misc_sensors( + &self, + ) -> impl Iterator)> + { + self.misc_sensors.iter().map(|s| { + let res = s.read_temp(self.i2c_task); + (s.sensor_id, res) + }) } - fn inputs_mut(&mut self) -> impl Iterator { - self.inputs.iter_mut() + fn read_inputs( + &mut self, + mode: PowerBitmask, + ) -> impl Iterator { + let task = &self.i2c_task; + self.inputs + .iter_mut() + .map(move |i| i.do_reading(mode, task)) } // TODO: This probably needs to exist, but for cosmo we have no dynamic @@ -229,7 +245,7 @@ impl crate::control::BspInterface for Bsp { // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, // even if some fail. return the LAST error if any. - fn set_all_fan_rpms(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { + fn set_all_fan_duty(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { let fctrl = self.fctrl.try_initialize()?; let mut any_err = false; @@ -263,6 +279,7 @@ impl Bsp { Self { seq, + i2c_task, fctrl, inputs: INPUTS_ONCE.claim(), diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index 0f4d4d7749..5cd6caeb6b 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -7,8 +7,8 @@ use crate::{ control::{ ChannelType, Device, FanPresence, FanReading, InputChannel, - InputChannelMetadata, InputStatus, Max31790State, PidConfig, - TemperatureSensor, + InputChannelMetadata, InputReadingOutcome, InputStatus, Max31790State, + PidConfig, TemperatureSensor, }, i2c_config::{devices, sensors}, }; @@ -201,12 +201,24 @@ impl crate::control::BspInterface for Bsp { }) } - fn misc_sensors(&self) -> impl Iterator { - self.misc_sensors.iter() + fn read_misc_sensors( + &self, + ) -> impl Iterator)> + { + self.misc_sensors.iter().map(|s| { + let res = s.read_temp(self.i2c_task); + (s.sensor_id, res) + }) } - fn inputs_mut(&mut self) -> impl Iterator { - self.inputs.iter_mut() + fn read_inputs( + &mut self, + mode: PowerBitmask, + ) -> impl Iterator { + let task = &self.i2c_task; + self.inputs + .iter_mut() + .map(move |i| i.do_reading(mode, task)) } // TODO: This probably needs to exist, but for gimlet we have no dynamic @@ -278,7 +290,7 @@ impl crate::control::BspInterface for Bsp { // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, // even if some fail. return the LAST error if any. - fn set_all_fan_rpms(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { + fn set_all_fan_duty(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { let fctrl = self.fctrl.try_initialize()?; let mut any_err = false; diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 62310ff003..d129d30f54 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -312,10 +312,10 @@ impl InputChannel { let se = SensorError::from(NoData::from(e)); match (self.metadata.ty, se) { (ChannelType::Removable, SensorError::NotPresent) => { - self.last_reading = None; + self.last_reading = Some(TemperatureReading::Inactive); } (ChannelType::RemovableAndErrorProne, _) => { - self.last_reading = None; + self.last_reading = Some(TemperatureReading::Inactive); } _ => { // In all other cases, just leave whatever the last @@ -554,9 +554,6 @@ pub(crate) struct ThermalControl<'a, B: BspInterface> { /// Reference to board-specific parameters bsp: &'a mut B, - /// I2C task - i2c_task: TaskId, - /// Task to which we should post sensor data updates sensor_api: SensorApi, @@ -884,11 +881,7 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { /// # Panics /// This function can only be called once, because it claims mutable static /// buffers. - pub fn new( - bsp: &'a mut B, - i2c_task: TaskId, - sensor_api: SensorApi, - ) -> Self { + pub fn new(bsp: &'a mut B, sensor_api: SensorApi) -> Self { use static_cell::ClaimOnceCell; let [err_blackbox, prev_err_blackbox] = { @@ -899,7 +892,6 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { Self { bsp, - i2c_task, sensor_api, target_margin: Celsius(0.0f32), state: ThermalControlState::Boot, @@ -1029,7 +1021,7 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { for reading in self.bsp.read_fan_rpms() { match reading { FanReading::PresentSuccess { rpm, sensor_id } => { - self.sensor_api.post_now(sensor_id, rpm.0 as f32) + self.sensor_api.post_now(sensor_id, rpm.0.into()) } FanReading::PresentError { error, sensor_id } => { ringbuf_entry!(Trace::FanReadFailed(sensor_id, error)); @@ -1050,13 +1042,13 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { // // We don't retain state for misc sensors, as that is all stored in the // sensor task itself. We're just in charge of polling them. - for s in self.bsp.misc_sensors() { - match s.read_temp(self.i2c_task) { - Ok(v) => self.sensor_api.post_now(s.sensor_id, v.0), + for (id, res) in self.bsp.read_misc_sensors() { + match res { + Ok(v) => self.sensor_api.post_now(id, v.0), Err(e) => { - ringbuf_entry!(Trace::MiscReadFailed(s.sensor_id, e)); - self.err_blackbox.push(s.sensor_id, e); - self.sensor_api.nodata_now(s.sensor_id, e.into()) + ringbuf_entry!(Trace::MiscReadFailed(id, e)); + self.err_blackbox.push(id, e); + self.sensor_api.nodata_now(id, e.into()) } } } @@ -1065,8 +1057,7 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { // potential TOCTOU issues; some sensors cannot be read if they are not // powered. let power_mode = self.bsp.power_mode(); - for input in self.bsp.inputs_mut() { - let res = input.do_reading(power_mode, &self.i2c_task); + for res in self.bsp.read_inputs(power_mode) { match res { InputReadingOutcome::Success { id, now, value } => { self.sensor_api.post(id, value.0, now); @@ -1111,6 +1102,17 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { self.power_mode = self.bsp.power_mode(); if prev_power_mode != self.power_mode { ringbuf_entry!(Trace::PowerModeChanged(self.power_mode)); + // TODO(AJM): the old code would now re-populate the state from the + // sensor task, while we continue on to now do control with all + // empty state. This potentially delays us in "Boot" mode for one + // extra tick which is 1s. We could return early here and ask `main` + // to re-run `read_sensors` for us, or run it automatically here. + // Either way though, this would now trigger *extra* I2C traffic, + // which is disappointing. + // + // Perhaps `reset_state` *shouldn't* clear `bsp.reset_all_values()`, + // since the old code wouldn't have sent `NoData` to all of the + // sensors? self.reset_state(); } @@ -1139,7 +1141,7 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { // that's the part which is most overheated. let f = |InputStatus { id, reading, model }| { if let TemperatureReading::Valid(v) = reading { - let worst_case = v.worst_case(now_ms, &model); + let worst_case = v.worst_case(now_ms, model); let temperature = worst_case.worst_case_temp; all_nominal &= model.is_nominal(temperature); if model.should_power_down(temperature) { @@ -1466,7 +1468,7 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { } }; self.last_pwm = pwm; - self.bsp.set_all_fan_rpms(pwm) + self.bsp.set_all_fan_duty(pwm) } /// Attempts to set the PWM of every fan to whatever the previous value was. @@ -1542,9 +1544,14 @@ pub trait BspInterface { fn read_fan_rpms(&mut self) -> impl Iterator; - fn misc_sensors(&self) -> impl Iterator; + fn read_misc_sensors( + &self, + ) -> impl Iterator)>; - fn inputs_mut(&mut self) -> impl Iterator; + fn read_inputs( + &mut self, + mode: PowerBitmask, + ) -> impl Iterator; // TODO: This probably needs to exist, but for cosmo we have no dynamic // inputs to read back. This should read from the api and store the state @@ -1588,5 +1595,5 @@ pub trait BspInterface { // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, // even if some fail. return the LAST error if any. - fn set_all_fan_rpms(&mut self, duty: PWMDuty) -> Result<(), ThermalError>; + fn set_all_fan_duty(&mut self, duty: PWMDuty) -> Result<(), ThermalError>; } diff --git a/task/thermal/src/main.rs b/task/thermal/src/main.rs index efb76dea2a..4befe3e86f 100644 --- a/task/thermal/src/main.rs +++ b/task/thermal/src/main.rs @@ -391,7 +391,7 @@ fn main() -> ! { ringbuf_entry!(Trace::Start); let mut bsp = Bsp::new(i2c_task); - let control = ThermalControl::new(&mut bsp, i2c_task, sensor_api); + let control = ThermalControl::new(&mut bsp, sensor_api); // This will put our timer in the past, and should immediately kick us. let deadline = sys_get_timer().now; From befe410391b74cbf0a6aead98db3a13eb3223b3b Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 14:24:49 +0200 Subject: [PATCH 16/33] Push down I2C logic into bsp-specific code --- task/thermal/src/bsp/common/emc2305.rs | 66 +++ task/thermal/src/bsp/common/i2c_temp_input.rs | 224 ++++++++++ task/thermal/src/bsp/common/max31790.rs | 140 +++++++ task/thermal/src/bsp/cosmo_ab.rs | 19 +- task/thermal/src/bsp/gimlet_bcdef.rs | 50 +-- task/thermal/src/control.rs | 387 ++---------------- 6 files changed, 488 insertions(+), 398 deletions(-) create mode 100644 task/thermal/src/bsp/common/emc2305.rs create mode 100644 task/thermal/src/bsp/common/i2c_temp_input.rs create mode 100644 task/thermal/src/bsp/common/max31790.rs diff --git a/task/thermal/src/bsp/common/emc2305.rs b/task/thermal/src/bsp/common/emc2305.rs new file mode 100644 index 0000000000..25e18231df --- /dev/null +++ b/task/thermal/src/bsp/common/emc2305.rs @@ -0,0 +1,66 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Common types and helpers for Emc2305 Fan Controller + +use drv_i2c_api::I2cDevice; +use drv_i2c_devices::emc2305::Emc2305; +use ringbuf::ringbuf_entry; + +use crate::{ + Trace, + control::{ControllerInitError, retry_init}, +}; + +/// Tracks whether a Emc2305 fan controller has been initialized, and +/// initializes it on demand when accessed, if necessary. +/// +/// This is copy-pasted from [`Max31790`] +pub(crate) struct Emc2305State { + emc2305: Emc2305, + fan_count: u8, + initialized: bool, +} + +impl Emc2305State { + #[allow(dead_code)] + pub(crate) fn new(dev: &I2cDevice, fan_count: u8) -> Self { + let mut this = Self { + emc2305: Emc2305::new(dev), + fan_count, + initialized: false, + }; + retry_init(|| this.initialize().map(|_| ())); + this + } + + /// Access the fan controller, attempting to initialize it if it has not yet + /// been initialized. + #[inline] + #[allow(dead_code)] + pub(crate) fn try_initialize( + &mut self, + ) -> Result<&mut Emc2305, ControllerInitError> { + if self.initialized { + return Ok(&mut self.emc2305); + } + + self.initialize() + } + + // Slow path that actually performs initialization. This is "outlined" so + // that we can avoid pushing a stack frame in the case where we just need to + // check a bool and return a pointer. + #[inline(never)] + fn initialize(&mut self) -> Result<&mut Emc2305, ControllerInitError> { + self.emc2305.initialize(self.fan_count).map_err(|e| { + ringbuf_entry!(Trace::FanControllerInitError(e)); + ControllerInitError(e) + })?; + + self.initialized = true; + ringbuf_entry!(Trace::FanControllerInitialized); + Ok(&mut self.emc2305) + } +} diff --git a/task/thermal/src/bsp/common/i2c_temp_input.rs b/task/thermal/src/bsp/common/i2c_temp_input.rs new file mode 100644 index 0000000000..4722353a37 --- /dev/null +++ b/task/thermal/src/bsp/common/i2c_temp_input.rs @@ -0,0 +1,224 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Common types and helpers for I2C Temperature Inputs + +use crate::{ + bsp::PowerBitmask, + control::{ + ChannelType, InputReadingOutcome, InputStatus, TemperatureReading, + TimestampedTemperatureReading, + }, +}; +use drv_i2c_api::ResponseCode; +use drv_i2c_devices::{ + TempSensor, nvme_bmc::NvmeBmc, pct2075::Pct2075, sbtsi::Sbtsi, + tmp117::Tmp117, tmp451::Tmp451, tse2004av::Tse2004Av, +}; +use task_sensor_api::{NoData, SensorError, SensorId}; +use task_thermal_api::{SensorReadError, ThermalProperties}; +use userlib::{TaskId, sys_get_timer, units::Celsius}; + +/// Type containing all of our temperature sensor types, so we can store them +/// generically in an array. These are all `I2cDevice`s, so functions on +/// this `enum` return an `drv_i2c_api::ResponseCode`. +#[allow(dead_code, clippy::upper_case_acronyms)] +pub enum Device { + Tmp117, + Tmp451(drv_i2c_devices::tmp451::Target), + CPU, + Dimm, + U2, + M2, + LM75, +} + +/// Represents a sensor in the system. +/// +/// The sensor includes a device type, used to decide how to read it; +/// a free function that returns the raw `I2cDevice`, so that this can be +/// `const`); and the sensor ID, to post data to the `sensors` task. +pub struct TemperatureSensor { + device: Device, + builder: fn(TaskId) -> drv_i2c_api::I2cDevice, + pub sensor_id: SensorId, +} + +impl TemperatureSensor { + pub const fn new( + device: Device, + builder: fn(TaskId) -> drv_i2c_api::I2cDevice, + sensor_id: SensorId, + ) -> Self { + Self { + device, + builder, + sensor_id, + } + } + pub fn read_temp( + &self, + i2c_task: TaskId, + ) -> Result { + let dev = (self.builder)(i2c_task); + let t = match &self.device { + Device::Tmp117 => Tmp117::new(&dev).read_temperature()?, + Device::CPU => Sbtsi::new(&dev).read_temperature()?, + Device::Tmp451(t) => Tmp451::new(&dev, *t).read_temperature()?, + Device::Dimm => Tse2004Av::new(&dev).read_temperature()?, + Device::U2 | Device::M2 => NvmeBmc::new(&dev).read_temperature()?, + Device::LM75 => Pct2075::new(&dev).read_temperature()?, + }; + Ok(t) + } +} + +pub(crate) struct InputChannelMetadata { + /// Temperature sensor + sensor: TemperatureSensor, + + /// Thermal properties of the associated component + model: ThermalProperties, + + /// Mask with bits set based on the Bsp's `power_mode` bits + power_mode_mask: PowerBitmask, + + /// Channel type + ty: ChannelType, +} + +/// An `InputChannel` represents a temperature sensor associated with a +/// particular component in the system. +pub(crate) struct InputChannel { + metadata: &'static InputChannelMetadata, + last_reading: Option, +} + +/// InputChannelMetadata is the constant description portion of an InputChannel. +/// +/// We split it off because InputChannel is mutable to contain the last state, +/// and if we left it inlined, then we would end up including all of this +/// metadata in RAM, despite never changing it! So instead, we break it off and +/// have InputChannel hold an `&'static` reference instead, so we only waste +/// a wee little pointer (4 bytes) to flash space in each InputChannel entry, +/// instead of (at the time of writing) 36 bytes, which for example in Cosmo +/// that has 14 input channels, is over 500 bytes of RAM! +impl InputChannelMetadata { + pub const fn new( + sensor: TemperatureSensor, + model: ThermalProperties, + power_mode_mask: PowerBitmask, + ty: ChannelType, + ) -> Self { + Self { + sensor, + model, + power_mode_mask, + ty, + } + } +} + +impl InputChannel { + pub const fn new(metadata: &'static InputChannelMetadata) -> Self { + Self { + metadata, + last_reading: None, + } + } + + pub fn has_reading(&self) -> bool { + self.last_reading.is_some() + } + + /// Get current stored status. + /// + /// Returns None if we do not have a reading stored. + pub fn status(&self) -> Option> { + let reading = self.last_reading.as_ref()?; + Some(InputStatus { + id: self.metadata.sensor.sensor_id, + reading, + model: &self.metadata.model, + }) + } + + pub fn reset_value(&mut self) { + self.last_reading = None; + } + + pub fn do_reading( + &mut self, + mode: PowerBitmask, + i2c_task: &TaskId, + ) -> InputReadingOutcome { + let id = self.metadata.sensor.sensor_id; + + // If we're not supposed to be on, don't even ask. + if !mode.intersects(self.metadata.power_mode_mask) { + self.last_reading = Some(TemperatureReading::Inactive); + return InputReadingOutcome::Unpowered { id }; + } + + match self.metadata.sensor.read_temp(*i2c_task) { + Ok(value) => { + let now = sys_get_timer().now; + self.last_reading = Some(TemperatureReading::Valid( + TimestampedTemperatureReading { + time_ms: now, + value, + }, + )); + InputReadingOutcome::Success { id, now, value } + } + Err(e) => { + // This is mimicing the old state value logic for deciding if + // we persist the data in `run_control`, that ONLY cleared the + // persisted value if: + // + // - The sensor is not present AND removable + // - The sensor is error prone + // + // Replicate that logic here, doing some type shenanigans + // because we aren't round-tripping through the Sensor API + // anymore. + let se = SensorError::from(NoData::from(e)); + match (self.metadata.ty, se) { + (ChannelType::Removable, SensorError::NotPresent) => { + self.last_reading = Some(TemperatureReading::Inactive); + } + (ChannelType::RemovableAndErrorProne, _) => { + self.last_reading = Some(TemperatureReading::Inactive); + } + _ => { + // In all other cases, just leave whatever the last + // present value was so that the state estimation + // can continue estimating state. + } + } + + // This logic comes from what was done in `read_sensors`, + // which is only deciding whether it's worth logging about. + // In either case, it will push NoData to the sensor api. + // + // This is *not* the same logic that is used above to decide + // whether we clear the previous state or not, despite being + // *very* similar! + let removable = matches!( + self.metadata.ty, + ChannelType::Removable + | ChannelType::RemovableAndErrorProne + ); + let removed = + e == SensorReadError::I2cError(ResponseCode::NoDevice); + let unexpected_failure = !(removable && removed); + if unexpected_failure { + InputReadingOutcome::UnacceptableMissing { id, err: e } + } else { + InputReadingOutcome::AcceptableMissing { id, err: e } + } + } + } + } +} diff --git a/task/thermal/src/bsp/common/max31790.rs b/task/thermal/src/bsp/common/max31790.rs new file mode 100644 index 0000000000..128e9ec6c7 --- /dev/null +++ b/task/thermal/src/bsp/common/max31790.rs @@ -0,0 +1,140 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Common types and helpers for Max31790 Fan Controller + +use drv_i2c_api::{I2cDevice, ResponseCode}; +use drv_i2c_devices::max31790::Max31790; +use ringbuf::ringbuf_entry; +use task_thermal_api::{SensorReadError, ThermalError}; + +use crate::{ + // control::{ControllerInitError, retry_init}, + __RINGBUF, + Trace, + control::{Fan, FanReading}, +}; + +/// Tracks whether a MAX31790 fan controller has been initialized, and +/// initializes it on demand when accessed, if necessary. +/// +/// Because initializing the fan controller can fail due to a transient bus +/// error, we don't panic if an initial attempt to initialize it as soon as the +/// `thermal` task starts fails. Because the fan controller's I2C watchdog will +/// simply run the fans at 100% if we aren't able to talk to it right away, the +/// `thermal` task should keep running, publishing sensor measurements, and +/// periodically trying to reach the fan controller until we're able to +/// initialize it successfully. Thus, we wrap it in this struct to track whether +/// it's been successfully initialized yet. +pub(crate) struct Max31790State { + max31790: Max31790, + initialized: bool, +} + +impl Max31790State { + #[allow(dead_code)] + pub(crate) fn new(dev: &I2cDevice) -> Self { + let mut this = Self { + max31790: Max31790::new(dev), + initialized: false, + }; + retry_init(|| this.initialize().map(drop)); + this + } + + /// Access the fan controller, attempting to initialize it if it has not yet + /// been initialized. + #[inline] + #[allow(dead_code)] + pub(crate) fn try_initialize( + &mut self, + ) -> Result<&mut Max31790, ControllerInitError> { + if self.initialized { + return Ok(&mut self.max31790); + } + + self.initialize() + } + + // Slow path that actually performs initialization. This is "outlined" so + // that we can avoid pushing a stack frame in the case where we just need to + // check a bool and return a pointer. + #[inline(never)] + fn initialize(&mut self) -> Result<&mut Max31790, ControllerInitError> { + self.max31790.initialize().map_err(|e| { + ringbuf_entry!(Trace::FanControllerInitError(e)); + ControllerInitError(e) + })?; + + self.initialized = true; + ringbuf_entry!(Trace::FanControllerInitialized); + Ok(&mut self.max31790) + } + + pub(crate) fn read_fan_rpms( + &mut self, + fans: &mut [Fan], + ) -> impl Iterator { + // Try to initialize the fan controller once at the start of the loop + let mut fctrl = self.try_initialize().map_err(SensorReadError::from); + + // TODO: Maybe there's a way to make this a method on Fan that we can + // call, kind of like InputStatus? + fans.iter_mut().map(move |f| { + // If initialization failed, then we short circuit to return that + // original error, copied for each fan we're going to report. + let fctrl = fctrl.as_mut().map_err(|e| *e); + + // If it was a success, attempt to read the RPMs, and either report + // that success or that error for each fan rpm. + let res = fctrl.and_then(|fc| { + fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) + }); + match res { + Ok(rpm) => { + f.last_reading = Some(rpm); + FanReading::PresentSuccess { + rpm, + sensor_id: f.rpm_sensor_id, + } + } + Err(error) => { + f.last_reading = None; + FanReading::PresentError { + error, + sensor_id: f.rpm_sensor_id, + } + } + } + }) + } +} + +/// Helper function to retry initialization several times, logging errors +pub(crate) fn retry_init Result<(), ControllerInitError>>( + mut init: F, +) { + // When we first start up, try to initialize the fan controller a few + // times, in case there's a transient I2C error. + for remaining in (0..3).rev() { + if init().is_ok() { + break; + } + ringbuf_entry!(Trace::FanControllerInitRetry { remaining }); + } +} + +pub(crate) struct ControllerInitError(pub(crate) ResponseCode); + +impl From for ThermalError { + fn from(_: ControllerInitError) -> Self { + ThermalError::FanControllerUninitialized + } +} + +impl From for SensorReadError { + fn from(ControllerInitError(code): ControllerInitError) -> Self { + SensorReadError::I2cError(code) + } +} diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 7b47dd6612..67b1514eb5 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -6,9 +6,8 @@ use crate::{ control::{ - ChannelType, Device, FanPresence, FanReading, InputChannel, - InputChannelMetadata, InputReadingOutcome, InputStatus, Max31790State, - PidConfig, TemperatureSensor, + ChannelType, FanPresence, FanReading, InputReadingOutcome, InputStatus, + Max31790State, PidConfig, }, i2c_config::{devices, sensors}, }; @@ -22,6 +21,13 @@ use userlib::{ units::{Celsius, PWMDuty}, }; +// This BSP uses i2c temperature inputs +#[path = "./common/i2c_temp_input.rs"] +mod i2c_temp_input; +use i2c_temp_input::{ + Device, InputChannel, InputChannelMetadata, TemperatureSensor, +}; + task_slot!(SEQ, cosmo_seq); // We monitor the TMP117 air temperature sensors, but don't use them as part of @@ -160,10 +166,9 @@ impl crate::control::BspInterface for Bsp { &self, ) -> impl Iterator)> { - self.misc_sensors.iter().map(|s| { - let res = s.read_temp(self.i2c_task); - (s.sensor_id, res) - }) + self.misc_sensors + .iter() + .map(|s| (s.sensor_id, s.read_temp(self.i2c_task))) } fn read_inputs( diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index 5cd6caeb6b..bb827487b2 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -6,9 +6,8 @@ use crate::{ control::{ - ChannelType, Device, FanPresence, FanReading, InputChannel, - InputChannelMetadata, InputReadingOutcome, InputStatus, Max31790State, - PidConfig, TemperatureSensor, + ChannelType, FanPresence, FanReading, InputReadingOutcome, InputStatus, + PidConfig, }, i2c_config::{devices, sensors}, }; @@ -22,6 +21,17 @@ use userlib::{ units::{Celsius, PWMDuty}, }; +// This BSP uses i2c temperature inputs +#[path = "./common/i2c_temp_input.rs"] +mod i2c_temp_input; +use i2c_temp_input::{ + Device, InputChannel, InputChannelMetadata, TemperatureSensor, +}; + +#[path = "./common/max31790.rs"] +mod max31790; +use max31790::Max31790State; + task_slot!(SEQ, gimlet_seq); // We monitor the TMP117 air temperature sensors, but don't use them as part of @@ -166,39 +176,7 @@ impl crate::control::BspInterface for Bsp { } fn read_fan_rpms(&mut self) -> impl Iterator { - // Try to initialize the fan controller once at the start of the loop - let mut fctrl = - self.fctrl.try_initialize().map_err(SensorReadError::from); - - // TODO: Maybe there's a way to make this a method on Fan that we can - // call, kind of like InputStatus? - self.fans.iter_mut().map(move |f| { - // If initialization failed, then we short circuit to return that - // original error, copied for each fan we're going to report. - let fctrl = fctrl.as_mut().map_err(|e| *e); - - // If it was a success, attempt to read the RPMs, and either report - // that success or that error for each fan rpm. - let res = fctrl.and_then(|fc| { - fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) - }); - match res { - Ok(rpm) => { - f.last_reading = Some(rpm); - FanReading::PresentSuccess { - rpm, - sensor_id: f.rpm_sensor_id, - } - } - Err(error) => { - f.last_reading = None; - FanReading::PresentError { - error, - sensor_id: f.rpm_sensor_id, - } - } - } - }) + self.fctrl.read_fan_rpms(self.fans) } fn read_misc_sensors( diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index d129d30f54..aa8f588e52 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -3,85 +3,18 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::{ThermalError, Trace, bsp::PowerBitmask}; -use drv_i2c_api::{I2cDevice, ResponseCode}; -use drv_i2c_devices::{ - TempSensor, - emc2305::Emc2305, - max31790::{I2cWatchdog, Max31790}, - nvme_bmc::NvmeBmc, - pct2075::Pct2075, - sbtsi::Sbtsi, - tmp117::Tmp117, - tmp451::Tmp451, - tse2004av::Tse2004Av, -}; +use drv_i2c_devices::max31790::I2cWatchdog; use ringbuf::ringbuf_entry_root as ringbuf_entry; -use task_sensor_api::{NoData, Sensor as SensorApi, SensorError, SensorId}; +use task_sensor_api::{Sensor as SensorApi, SensorId}; use task_thermal_api::{SensorReadError, ThermalAutoState, ThermalProperties}; use userlib::{ - TaskId, sys_get_timer, + sys_get_timer, units::{Celsius, PWMDuty, Rpm}, }; //////////////////////////////////////////////////////////////////////////////// -/// Type containing all of our temperature sensor types, so we can store them -/// generically in an array. These are all `I2cDevice`s, so functions on -/// this `enum` return an `drv_i2c_api::ResponseCode`. -#[allow(dead_code, clippy::upper_case_acronyms)] -pub enum Device { - Tmp117, - Tmp451(drv_i2c_devices::tmp451::Target), - CPU, - Dimm, - U2, - M2, - LM75, -} - -/// Represents a sensor in the system. -/// -/// The sensor includes a device type, used to decide how to read it; -/// a free function that returns the raw `I2cDevice`, so that this can be -/// `const`); and the sensor ID, to post data to the `sensors` task. -#[allow(dead_code)] // not all BSPS -pub struct TemperatureSensor { - device: Device, - builder: fn(TaskId) -> drv_i2c_api::I2cDevice, - pub sensor_id: SensorId, -} - -impl TemperatureSensor { - #[allow(dead_code)] // not all BSPS - pub const fn new( - device: Device, - builder: fn(TaskId) -> drv_i2c_api::I2cDevice, - sensor_id: SensorId, - ) -> Self { - Self { - device, - builder, - sensor_id, - } - } - pub fn read_temp( - &self, - i2c_task: TaskId, - ) -> Result { - let dev = (self.builder)(i2c_task); - let t = match &self.device { - Device::Tmp117 => Tmp117::new(&dev).read_temperature()?, - Device::CPU => Sbtsi::new(&dev).read_temperature()?, - Device::Tmp451(t) => Tmp451::new(&dev, *t).read_temperature()?, - Device::Dimm => Tse2004Av::new(&dev).read_temperature()?, - Device::U2 | Device::M2 => NvmeBmc::new(&dev).read_temperature()?, - Device::LM75 => Pct2075::new(&dev).read_temperature()?, - }; - Ok(t) - } -} - /// Represents the indvidual fans in the system /// /// Depending on the system we have diferent numbers of fans structured in @@ -90,6 +23,8 @@ impl TemperatureSensor { /// fans which are not present and their PWM should only be driven low. pub struct Fan { pub rpm_sensor_id: SensorId, + // TODO(AJM): Distinguish between "have never heard from" and "have heard + // from but has since gone missing"? pub last_reading: Option, pub bsp_data: D, } @@ -133,27 +68,6 @@ pub enum FanReading { //////////////////////////////////////////////////////////////////////////////// -pub(crate) struct InputChannelMetadata { - /// Temperature sensor - sensor: TemperatureSensor, - - /// Thermal properties of the associated component - model: ThermalProperties, - - /// Mask with bits set based on the Bsp's `power_mode` bits - power_mode_mask: PowerBitmask, - - /// Channel type - ty: ChannelType, -} - -/// An `InputChannel` represents a temperature sensor associated with a -/// particular component in the system. -pub(crate) struct InputChannel { - metadata: &'static InputChannelMetadata, - last_reading: Option, -} - #[derive(Copy, Clone, Eq, PartialEq)] #[allow(dead_code)] // a typical BSP uses only a subset of these. pub(crate) enum ChannelType { @@ -219,136 +133,6 @@ pub struct InputStatus<'a> { pub model: &'a ThermalProperties, } -/// InputChannelMetadata is the constant description portion of an InputChannel. -/// -/// We split it off because InputChannel is mutable to contain the last state, -/// and if we left it inlined, then we would end up including all of this -/// metadata in RAM, despite never changing it! So instead, we break it off and -/// have InputChannel hold an `&'static` reference instead, so we only waste -/// a wee little pointer (4 bytes) to flash space in each InputChannel entry, -/// instead of (at the time of writing) 36 bytes, which for example in Cosmo -/// that has 14 input channels, is over 500 bytes of RAM! -impl InputChannelMetadata { - #[allow(dead_code)] // not all BSPS - pub const fn new( - sensor: TemperatureSensor, - model: ThermalProperties, - power_mode_mask: PowerBitmask, - ty: ChannelType, - ) -> Self { - Self { - sensor, - model, - power_mode_mask, - ty, - } - } -} - -impl InputChannel { - #[allow(dead_code)] // not all BSPS - pub const fn new(metadata: &'static InputChannelMetadata) -> Self { - Self { - metadata, - last_reading: None, - } - } - - pub fn has_reading(&self) -> bool { - self.last_reading.is_some() - } - - /// Get current stored status. - /// - /// Returns None if we do not have a reading stored. - pub fn status(&self) -> Option> { - let reading = self.last_reading.as_ref()?; - Some(InputStatus { - id: self.metadata.sensor.sensor_id, - reading, - model: &self.metadata.model, - }) - } - - pub fn reset_value(&mut self) { - self.last_reading = None; - } - - pub fn do_reading( - &mut self, - mode: PowerBitmask, - i2c_task: &TaskId, - ) -> InputReadingOutcome { - let id = self.metadata.sensor.sensor_id; - - // If we're not supposed to be on, don't even ask. - if !mode.intersects(self.metadata.power_mode_mask) { - self.last_reading = Some(TemperatureReading::Inactive); - return InputReadingOutcome::Unpowered { id }; - } - - match self.metadata.sensor.read_temp(*i2c_task) { - Ok(value) => { - let now = sys_get_timer().now; - self.last_reading = Some(TemperatureReading::Valid( - TimestampedTemperatureReading { - time_ms: now, - value, - }, - )); - InputReadingOutcome::Success { id, now, value } - } - Err(e) => { - // This is mimicing the old state value logic for deciding if - // we persist the data in `run_control`, that ONLY cleared the - // persisted value if: - // - // - The sensor is not present AND removable - // - The sensor is error prone - // - // Replicate that logic here, doing some type shenanigans - // because we aren't round-tripping through the Sensor API - // anymore. - let se = SensorError::from(NoData::from(e)); - match (self.metadata.ty, se) { - (ChannelType::Removable, SensorError::NotPresent) => { - self.last_reading = Some(TemperatureReading::Inactive); - } - (ChannelType::RemovableAndErrorProne, _) => { - self.last_reading = Some(TemperatureReading::Inactive); - } - _ => { - // In all other cases, just leave whatever the last - // present value was so that the state estimation - // can continue estimating state. - } - } - - // This logic comes from what was done in `read_sensors`, - // which is only deciding whether it's worth logging about. - // In either case, it will push NoData to the sensor api. - // - // This is *not* the same logic that is used above to decide - // whether we clear the previous state or not, despite being - // *very* similar! - let removable = matches!( - self.metadata.ty, - ChannelType::Removable - | ChannelType::RemovableAndErrorProne - ); - let removed = - e == SensorReadError::I2cError(ResponseCode::NoDevice); - let unexpected_failure = !(removable && removed); - if unexpected_failure { - InputReadingOutcome::UnacceptableMissing { id, err: e } - } else { - InputReadingOutcome::AcceptableMissing { id, err: e } - } - } - } - } -} - //////////////////////////////////////////////////////////////////////////////// /// A `DynamicInputChannel` represents a temperature input channel with thermal @@ -408,140 +192,33 @@ impl ThermalSensorErrors { //////////////////////////////////////////////////////////////////////////////// -/// Tracks whether a MAX31790 fan controller has been initialized, and -/// initializes it on demand when accessed, if necessary. -/// -/// Because initializing the fan controller can fail due to a transient bus -/// error, we don't panic if an initial attempt to initialize it as soon as the -/// `thermal` task starts fails. Because the fan controller's I2C watchdog will -/// simply run the fans at 100% if we aren't able to talk to it right away, the -/// `thermal` task should keep running, publishing sensor measurements, and -/// periodically trying to reach the fan controller until we're able to -/// initialize it successfully. Thus, we wrap it in this struct to track whether -/// it's been successfully initialized yet. -pub(crate) struct Max31790State { - max31790: Max31790, - initialized: bool, -} - -impl Max31790State { - #[allow(dead_code)] - pub(crate) fn new(dev: &I2cDevice) -> Self { - let mut this = Self { - max31790: Max31790::new(dev), - initialized: false, - }; - retry_init(|| this.initialize().map(|_| ())); - this - } - - /// Access the fan controller, attempting to initialize it if it has not yet - /// been initialized. - #[inline] - #[allow(dead_code)] - pub(crate) fn try_initialize( - &mut self, - ) -> Result<&mut Max31790, ControllerInitError> { - if self.initialized { - return Ok(&mut self.max31790); - } - - self.initialize() - } - - // Slow path that actually performs initialization. This is "outlined" so - // that we can avoid pushing a stack frame in the case where we just need to - // check a bool and return a pointer. - #[inline(never)] - fn initialize(&mut self) -> Result<&mut Max31790, ControllerInitError> { - self.max31790.initialize().map_err(|e| { - ringbuf_entry!(Trace::FanControllerInitError(e)); - ControllerInitError(e) - })?; - - self.initialized = true; - ringbuf_entry!(Trace::FanControllerInitialized); - Ok(&mut self.max31790) - } -} - -/// Tracks whether a EMC2305 fan controller has been initialized, and -/// initializes it on demand when accessed, if necessary. -/// -/// This is copy-pasted from [`Max31790`] -pub(crate) struct Emc2305State { - emc2305: Emc2305, - fan_count: u8, - initialized: bool, -} - -impl Emc2305State { - #[allow(dead_code)] - pub(crate) fn new(dev: &I2cDevice, fan_count: u8) -> Self { - let mut this = Self { - emc2305: Emc2305::new(dev), - fan_count, - initialized: false, - }; - retry_init(|| this.initialize().map(|_| ())); - this - } - - /// Access the fan controller, attempting to initialize it if it has not yet - /// been initialized. - #[inline] - #[allow(dead_code)] - pub(crate) fn try_initialize( - &mut self, - ) -> Result<&mut Emc2305, ControllerInitError> { - if self.initialized { - return Ok(&mut self.emc2305); - } - - self.initialize() - } - - // Slow path that actually performs initialization. This is "outlined" so - // that we can avoid pushing a stack frame in the case where we just need to - // check a bool and return a pointer. - #[inline(never)] - fn initialize(&mut self) -> Result<&mut Emc2305, ControllerInitError> { - self.emc2305.initialize(self.fan_count).map_err(|e| { - ringbuf_entry!(Trace::FanControllerInitError(e)); - ControllerInitError(e) - })?; - - self.initialized = true; - ringbuf_entry!(Trace::FanControllerInitialized); - Ok(&mut self.emc2305) - } -} - -/// Helper function to retry initialization several times, logging errors -fn retry_init Result<(), ControllerInitError>>(mut init: F) { - // When we first start up, try to initialize the fan controller a few - // times, in case there's a transient I2C error. - for remaining in (0..3).rev() { - if init().is_ok() { - break; - } - ringbuf_entry!(Trace::FanControllerInitRetry { remaining }); - } -} - -pub(crate) struct ControllerInitError(ResponseCode); - -impl From for ThermalError { - fn from(_: ControllerInitError) -> Self { - ThermalError::FanControllerUninitialized - } -} - -impl From for SensorReadError { - fn from(ControllerInitError(code): ControllerInitError) -> Self { - SensorReadError::I2cError(code) - } -} +// /// Helper function to retry initialization several times, logging errors +// pub(crate) fn retry_init Result<(), ControllerInitError>>( +// mut init: F, +// ) { +// // When we first start up, try to initialize the fan controller a few +// // times, in case there's a transient I2C error. +// for remaining in (0..3).rev() { +// if init().is_ok() { +// break; +// } +// ringbuf_entry!(Trace::FanControllerInitRetry { remaining }); +// } +// } + +// pub(crate) struct ControllerInitError(pub(crate) ResponseCode); + +// impl From for ThermalError { +// fn from(_: ControllerInitError) -> Self { +// ThermalError::FanControllerUninitialized +// } +// } + +// impl From for SensorReadError { +// fn from(ControllerInitError(code): ControllerInitError) -> Self { +// SensorReadError::I2cError(code) +// } +// } //////////////////////////////////////////////////////////////////////////////// From e1e8befe8e39b79f0e89ad1f582d21d25f617ff4 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 14:51:25 +0200 Subject: [PATCH 17/33] Port grapefruit --- drv/i2c-devices/src/emc2305.rs | 21 +++ task/thermal/src/bsp/common/emc2305.rs | 73 +++++++- task/thermal/src/bsp/grapefruit.rs | 250 +++++++++++++++++++------ 3 files changed, 284 insertions(+), 60 deletions(-) diff --git a/drv/i2c-devices/src/emc2305.rs b/drv/i2c-devices/src/emc2305.rs index 3079b864bc..b39e58b6ff 100644 --- a/drv/i2c-devices/src/emc2305.rs +++ b/drv/i2c-devices/src/emc2305.rs @@ -148,6 +148,27 @@ pub const MAX_FANS: u8 = 5; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Fan(u8); +impl Fan { + /// Create a new fan + /// + /// This panics if idx is out of range, and is intended to be used + /// at compile time for building constant values. Prefer `TryFrom` + /// at runtime when handling user provided data. + pub const fn new_const(idx: u8) -> Self { + if idx >= MAX_FANS { + panic!("Out of range!"); + } else { + Self(idx) + } + } +} + +impl From for u8 { + fn from(val: Fan) -> Self { + val.0 + } +} + impl TryFrom for Fan { type Error = (); /// Fans are based on a 0-based index. This should *not* be the number diff --git a/task/thermal/src/bsp/common/emc2305.rs b/task/thermal/src/bsp/common/emc2305.rs index 25e18231df..3af9b2f3ab 100644 --- a/task/thermal/src/bsp/common/emc2305.rs +++ b/task/thermal/src/bsp/common/emc2305.rs @@ -4,13 +4,14 @@ //! Common types and helpers for Emc2305 Fan Controller -use drv_i2c_api::I2cDevice; +use drv_i2c_api::{I2cDevice, ResponseCode}; use drv_i2c_devices::emc2305::Emc2305; use ringbuf::ringbuf_entry; +use task_thermal_api::{SensorReadError, ThermalError}; use crate::{ - Trace, - control::{ControllerInitError, retry_init}, + __RINGBUF, Trace, + control::{Fan, FanReading}, }; /// Tracks whether a Emc2305 fan controller has been initialized, and @@ -63,4 +64,70 @@ impl Emc2305State { ringbuf_entry!(Trace::FanControllerInitialized); Ok(&mut self.emc2305) } + + pub(crate) fn read_fan_rpms( + &mut self, + fans: &mut [Fan], + ) -> impl Iterator { + // Try to initialize the fan controller once at the start of the loop + let mut fctrl = self.try_initialize().map_err(SensorReadError::from); + + // TODO: Maybe there's a way to make this a method on Fan that we can + // call, kind of like InputStatus? + fans.iter_mut().map(move |f| { + // If initialization failed, then we short circuit to return that + // original error, copied for each fan we're going to report. + let fctrl = fctrl.as_mut().map_err(|e| *e); + + // If it was a success, attempt to read the RPMs, and either report + // that success or that error for each fan rpm. + let res = fctrl.and_then(|fc| { + fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) + }); + match res { + Ok(rpm) => { + f.last_reading = Some(rpm); + FanReading::PresentSuccess { + rpm, + sensor_id: f.rpm_sensor_id, + } + } + Err(error) => { + f.last_reading = None; + FanReading::PresentError { + error, + sensor_id: f.rpm_sensor_id, + } + } + } + }) + } +} + +/// Helper function to retry initialization several times, logging errors +pub(crate) fn retry_init Result<(), ControllerInitError>>( + mut init: F, +) { + // When we first start up, try to initialize the fan controller a few + // times, in case there's a transient I2C error. + for remaining in (0..3).rev() { + if init().is_ok() { + break; + } + ringbuf_entry!(Trace::FanControllerInitRetry { remaining }); + } +} + +pub(crate) struct ControllerInitError(pub(crate) ResponseCode); + +impl From for ThermalError { + fn from(_: ControllerInitError) -> Self { + ThermalError::FanControllerUninitialized + } +} + +impl From for SensorReadError { + fn from(ControllerInitError(code): ControllerInitError) -> Self { + SensorReadError::I2cError(code) + } } diff --git a/task/thermal/src/bsp/grapefruit.rs b/task/thermal/src/bsp/grapefruit.rs index bf97d18687..cd4b4294cb 100644 --- a/task/thermal/src/bsp/grapefruit.rs +++ b/task/thermal/src/bsp/grapefruit.rs @@ -4,34 +4,38 @@ //! BSP for Medusa -use crate::control::{ - ChannelType, ControllerInitError, Device, Emc2305State, FanControl, Fans, - InputChannel, PidConfig, TemperatureSensor, -}; +use crate::control::{ChannelType, PidConfig}; +use crate::control::{FanPresence, InputStatus}; +use drv_i2c_devices::max31790::I2cWatchdog; use task_sensor_api::SensorId; -use task_thermal_api::ThermalProperties; +use task_thermal_api::{ThermalError, ThermalProperties}; use userlib::TaskId; use userlib::UnwrapLite; -use userlib::units::Celsius; +use userlib::units::{Celsius, PWMDuty}; include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); use i2c_config::devices; use i2c_config::sensors; +// This BSP uses i2c temperature inputs +#[path = "./common/i2c_temp_input.rs"] +mod i2c_temp_input; +use i2c_temp_input::{ + Device, InputChannel, InputChannelMetadata, TemperatureSensor, +}; + +#[path = "./common/emc2305.rs"] +mod emc2305; +use emc2305::Emc2305State; + //////////////////////////////////////////////////////////////////////////////// // Constants! -// Air temperature sensors, which aren't used in the control loop -const NUM_TEMPERATURE_SENSORS: usize = 0; - // Temperature inputs (I2C devices), which are used in the control loop. -pub const NUM_TEMPERATURE_INPUTS: usize = 1; - -// External temperature inputs, which are provided to the task over IPC -pub const NUM_DYNAMIC_TEMPERATURE_INPUTS: usize = 0; +const NUM_TEMPERATURE_INPUTS: usize = 1; // Number of individual fans -pub const NUM_FANS: usize = 4; +const NUM_FANS: usize = 4; // Run the PID loop on startup pub const USE_CONTROLLER: bool = true; @@ -51,59 +55,170 @@ pub enum SeqError {} #[allow(dead_code)] pub(crate) struct Bsp { /// Controlled sensors - pub inputs: &'static [InputChannel; NUM_TEMPERATURE_INPUTS], - pub dynamic_inputs: &'static [SensorId; NUM_DYNAMIC_TEMPERATURE_INPUTS], + pub inputs: &'static mut [InputChannel; NUM_TEMPERATURE_INPUTS], - /// Monitored sensors - pub misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], + fans: &'static mut [Fan; NUM_FANS], + fans_added: bool, + i2c_task: TaskId, pub pid_config: PidConfig, fctrl: Emc2305State, } -impl Bsp { - pub fn fan_control( +impl crate::control::BspInterface for Bsp { + // // TODO: this is all made up, copied from tuned Gimlet values + const PID_CONFIG: PidConfig = PidConfig { + zero: 35.0, + gain_p: 1.75, + gain_i: 0.0135, + gain_d: 0.4, + min_output: 15.0, + max_output: 100.0, + }; + + fn power_down(&self) -> Result<(), crate::SeqError> { + Ok(()) + } + + fn power_mode(&self) -> PowerBitmask { + PowerBitmask::ON + } + + fn read_fan_presence( &mut self, - fan: crate::Fan, - ) -> Result, ControllerInitError> { - Ok(FanControl::Emc2305( - self.fctrl.try_initialize()?, - fan.0.try_into().unwrap_lite(), - )) + ) -> Result< + impl Iterator, + crate::SeqError, + > { + let report_new = !self.fans_added; + self.fans_added = true; + Ok(self.fans.iter().map(move |f| FanPresence::Present { + fan_id: f.bsp_data.into(), + new: report_new, + })) } - pub fn for_each_fctrl( + fn read_fan_rpms( &mut self, - fctrl: impl FnMut(FanControl<'_>), - ) -> Result<(), ControllerInitError> { - self.fan_control(0.into()).map(fctrl) + ) -> impl Iterator { + self.fctrl.read_fan_rpms(self.fans) } - pub fn power_mode(&self) -> PowerBitmask { - PowerBitmask::ON + fn read_misc_sensors( + &self, + ) -> impl Iterator< + Item = (SensorId, Result), + > { + core::iter::empty() } - pub fn power_down(&self) -> Result<(), SeqError> { - Ok(()) + fn read_inputs( + &mut self, + mode: PowerBitmask, + ) -> impl Iterator { + let task = &self.i2c_task; + self.inputs + .iter_mut() + .map(move |i| i.do_reading(mode, task)) } - pub fn get_fan_presence(&self) -> Result, SeqError> { - let mut fans = Fans::new(); - for i in 0..NUM_FANS { - fans[i] = Some(sensors::EMC2305_SPEED_SENSORS[i]); - } - Ok(fans) + fn read_dynamic_inputs_back_from_sensor_api( + &mut self, + _sensor_api: &task_sensor_api::Sensor, + ) { + // No dynamic inputs + } + + // returns Ok(true) if this was a new input + fn update_dynamic_input( + &mut self, + _index: usize, + _model: ThermalProperties, + ) -> Result { + // No dynamic inputs here, todo: static assert this + Err(ThermalError::InvalidIndex) + } + + // sets last_reading to Some(Missing), returns sensor id + fn remove_dynamic_input( + &mut self, + _index: usize, + ) -> Result { + // No dynamic inputs here, todo: static assert this + Err(ThermalError::InvalidIndex) + } + + fn all_inputs_present(&self) -> bool { + self.inputs.iter().all(InputChannel::has_reading) + // && self.dynamic_inputs... + } + + // Visit all temperature sensors, first the inputs, then the dynamic_inputs. + // Inputs and Dynamic Inputs that are missing will be skipped. + fn all_inputs_allow_missing( + &self, + ) -> impl Iterator> { + self.inputs.iter().filter_map(InputChannel::status) + // .zip(self.dynamic_inputs...) + } + + // Visit all temperature sensors, first the inputs, then the dynamic_inputs. + // All inputs MUST have a previous reading or this will panic, though the + // readings may be allowed to be Missing if the model allows it. Dynamic + // inputs that are not present will be skipped. + fn all_inputs(&self) -> impl Iterator> { + self.inputs.iter().map(|input| input.status().unwrap_lite()) + // .zip(self.dynamic_inputs...) + } + + fn reset_all_values(&mut self) { + self.inputs.iter_mut().for_each(|i| i.reset_value()); + // self.dynamic_inputs... } - pub fn fan_sensor_id(&self, i: usize) -> SensorId { - sensors::EMC2305_SPEED_SENSORS[i] + fn set_all_watchdogs( + &mut self, + watchdog: I2cWatchdog, + ) -> Result<(), ThermalError> { + // Only one watchdog to configure here! + self.fctrl + .try_initialize()? + .set_watchdog(!matches!(watchdog, I2cWatchdog::Disabled)) + .map_err(|_| ThermalError::DeviceError) + } + + // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, + // even if some fail. return the LAST error if any. + fn set_all_fan_duty(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { + let fctrl = self.fctrl.try_initialize()?; + let mut any_err = false; + + // Note: DON'T short circuit here! + for fan in self.fans.iter_mut() { + any_err |= fctrl.set_pwm(fan.bsp_data, duty).is_err(); + } + + if any_err { + Err(ThermalError::DeviceError) + } else { + Ok(()) + } } +} +impl Bsp { pub fn new(i2c_task: TaskId) -> Self { let fctrl = Emc2305State::new(&devices::emc2305(i2c_task)[0], NUM_FANS as u8); + static INPUTS_ONCE: static_cell::ClaimOnceCell< + [InputChannel; NUM_TEMPERATURE_INPUTS], + > = static_cell::ClaimOnceCell::new(INPUTS); + + static FANS_ONCE: static_cell::ClaimOnceCell<[Fan; NUM_FANS]> = + static_cell::ClaimOnceCell::new(FANS); + Self { // TODO: this is all made up, copied from tuned Gimlet values pid_config: PidConfig { @@ -115,15 +230,37 @@ impl Bsp { max_output: 100.0, }, - inputs: &INPUTS, - dynamic_inputs: &[], - misc_sensors: &MISC_SENSORS, + fans_added: false, + fans: FANS_ONCE.claim(), + + inputs: INPUTS_ONCE.claim(), fctrl, + i2c_task, } } } +type Fan = crate::control::Fan; +const FANS: [Fan; NUM_FANS] = [ + Fan::new( + sensors::EMC2305_SPEED_SENSORS[0], + drv_i2c_devices::emc2305::Fan::new_const(0), + ), + Fan::new( + sensors::EMC2305_SPEED_SENSORS[1], + drv_i2c_devices::emc2305::Fan::new_const(1), + ), + Fan::new( + sensors::EMC2305_SPEED_SENSORS[2], + drv_i2c_devices::emc2305::Fan::new_const(2), + ), + Fan::new( + sensors::EMC2305_SPEED_SENSORS[3], + drv_i2c_devices::emc2305::Fan::new_const(3), + ), +]; + // This is completely made up! const LM75_THERMALS: ThermalProperties = ThermalProperties { target_temperature: Celsius(60f32), @@ -132,15 +269,14 @@ const LM75_THERMALS: ThermalProperties = ThermalProperties { temperature_slew_deg_per_sec: 0.5, }; -const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [InputChannel::new( - TemperatureSensor::new( - Device::LM75, - devices::pct2075_lm75_a, - sensors::PCT2075_LM75_A_TEMPERATURE_SENSOR, - ), - LM75_THERMALS, - PowerBitmask::ON, - ChannelType::MustBePresent, -)]; - -const MISC_SENSORS: [TemperatureSensor; NUM_TEMPERATURE_SENSORS] = []; +const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = + [InputChannel::new(&InputChannelMetadata::new( + TemperatureSensor::new( + Device::LM75, + devices::pct2075_lm75_a, + sensors::PCT2075_LM75_A_TEMPERATURE_SENSOR, + ), + LM75_THERMALS, + PowerBitmask::ON, + ChannelType::MustBePresent, + ))]; From 9abcdfec439b66efc70b168e585d498234299285 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 14:57:04 +0200 Subject: [PATCH 18/33] Medusa does not have a thermal task --- task/thermal/src/bsp/medusa_a.rs | 110 ------------------------------- 1 file changed, 110 deletions(-) delete mode 100644 task/thermal/src/bsp/medusa_a.rs diff --git a/task/thermal/src/bsp/medusa_a.rs b/task/thermal/src/bsp/medusa_a.rs deleted file mode 100644 index 115a02a94e..0000000000 --- a/task/thermal/src/bsp/medusa_a.rs +++ /dev/null @@ -1,110 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! BSP for Medusa - -use crate::control::{ - FanControl, Fans, InputChannel, PidConfig, TemperatureSensor, -}; -use task_sensor_api::SensorId; -use userlib::TaskId; - -include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); - -//////////////////////////////////////////////////////////////////////////////// -// Constants! - -// Air temperature sensors, which aren't used in the control loop -const NUM_TEMPERATURE_SENSORS: usize = 0; - -// Temperature inputs (I2C devices), which are used in the control loop. -pub const NUM_TEMPERATURE_INPUTS: usize = 0; - -// External temperature inputs, which are provided to the task over IPC -// In practice, these are our transceivers. -pub const NUM_DYNAMIC_TEMPERATURE_INPUTS: usize = - drv_transceivers_api::NUM_PORTS as usize; - -// Number of individual fans - Medusa has none! -pub const NUM_FANS: usize = 0; - -// Run the PID loop on startup -pub const USE_CONTROLLER: bool = false; - -//////////////////////////////////////////////////////////////////////////////// - -bitflags::bitflags! { - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub struct PowerBitmask: u32 {} -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum SeqError {} - -#[allow(dead_code)] -pub(crate) struct Bsp { - /// Controlled sensors - pub inputs: &'static [InputChannel; NUM_TEMPERATURE_INPUTS], - pub dynamic_inputs: &'static [SensorId; NUM_DYNAMIC_TEMPERATURE_INPUTS], - - /// Monitored sensors - pub misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], - - pub pid_config: PidConfig, -} - -impl Bsp { - pub fn fan_control( - &self, - _fan: crate::Fan, - ) -> crate::control::FanControl<'_> { - // Because we have zero fans, nothing should ever call fan_control. - unreachable!() - } - - pub fn for_each_fctrl(&self, mut _fctrl: impl FnMut(FanControl<'_>)) { - // This one's reeeeal easy. - } - - pub fn power_mode(&self) -> PowerBitmask { - PowerBitmask::empty() - } - - pub fn power_down(&self) -> Result<(), SeqError> { - Ok(()) - } - - pub fn get_fan_presence(&self) -> Result, SeqError> { - Ok(Fans::new()) - } - - pub fn fan_sensor_id(&self, i: usize) -> SensorId { - panic!("no fans, this should not be called"); - } - - pub fn new(_i2c_task: TaskId) -> Self { - Self { - // PID config doesn't matter since we have no fans. - pid_config: PidConfig { - zero: 0., - gain_p: 0., - gain_i: 0., - gain_d: 0., - min_output: 0., - max_output: 100., - }, - - inputs: &INPUTS, - dynamic_inputs: - &drv_transceivers_api::TRANSCEIVER_TEMPERATURE_SENSORS, - - // We monitor and log all of the air temperatures - misc_sensors: &MISC_SENSORS, - } - } -} - -const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = []; - -const MISC_SENSORS: [TemperatureSensor; NUM_TEMPERATURE_SENSORS] = []; From eac9093184fa70e1f051ad8c2375e9549922eadf Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 15:06:26 +0200 Subject: [PATCH 19/33] Port over minibar --- task/thermal/src/bsp/grapefruit.rs | 12 --- task/thermal/src/bsp/minibar.rs | 166 ++++++++++++++++++----------- task/thermal/src/control.rs | 36 ++----- task/thermal/src/main.rs | 3 + 4 files changed, 110 insertions(+), 107 deletions(-) diff --git a/task/thermal/src/bsp/grapefruit.rs b/task/thermal/src/bsp/grapefruit.rs index cd4b4294cb..c42c2e3ba2 100644 --- a/task/thermal/src/bsp/grapefruit.rs +++ b/task/thermal/src/bsp/grapefruit.rs @@ -61,8 +61,6 @@ pub(crate) struct Bsp { fans_added: bool, i2c_task: TaskId, - pub pid_config: PidConfig, - fctrl: Emc2305State, } @@ -220,16 +218,6 @@ impl Bsp { static_cell::ClaimOnceCell::new(FANS); Self { - // TODO: this is all made up, copied from tuned Gimlet values - pid_config: PidConfig { - zero: 35.0, - gain_p: 1.75, - gain_i: 0.0135, - gain_d: 0.4, - min_output: 15.0, - max_output: 100.0, - }, - fans_added: false, fans: FANS_ONCE.claim(), diff --git a/task/thermal/src/bsp/minibar.rs b/task/thermal/src/bsp/minibar.rs index b80cf65e7a..af6719e387 100644 --- a/task/thermal/src/bsp/minibar.rs +++ b/task/thermal/src/bsp/minibar.rs @@ -4,11 +4,9 @@ //! BSP for Minibar -use crate::control::{ - ControllerInitError, FanControl, Fans, InputChannel, PidConfig, - TemperatureSensor, -}; +use crate::control::PidConfig; use task_sensor_api::SensorId; +use task_thermal_api::{ThermalError, ThermalProperties}; use userlib::TaskId; include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); @@ -16,19 +14,6 @@ include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); //////////////////////////////////////////////////////////////////////////////// // Constants! -// Air temperature sensors, which aren't used in the control loop -const NUM_TEMPERATURE_SENSORS: usize = 0; - -// Temperature inputs (I2C devices), which are used in the control loop. -pub const NUM_TEMPERATURE_INPUTS: usize = 0; - -// External temperature inputs, which are provided to the task over IPC -// In practice, these are our transceivers. -pub const NUM_DYNAMIC_TEMPERATURE_INPUTS: usize = 0; - -// Number of individual fans - Minibar has none! -pub const NUM_FANS: usize = 0; - // Run the PID loop on startup pub const USE_CONTROLLER: bool = false; @@ -43,71 +28,122 @@ bitflags::bitflags! { pub enum SeqError {} #[allow(dead_code)] -pub(crate) struct Bsp { - /// Controlled sensors - pub inputs: &'static [InputChannel; NUM_TEMPERATURE_INPUTS], - pub dynamic_inputs: &'static [SensorId; NUM_DYNAMIC_TEMPERATURE_INPUTS], +pub(crate) struct Bsp {} + +impl crate::control::BspInterface for Bsp { + const PID_CONFIG: PidConfig = PidConfig { + zero: 0., + gain_p: 0., + gain_i: 0., + gain_d: 0., + min_output: 0., + max_output: 100., + }; + + fn power_down(&self) -> Result<(), crate::SeqError> { + Ok(()) + } - /// Monitored sensors - pub misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], + fn power_mode(&self) -> PowerBitmask { + PowerBitmask::empty() + } - pub pid_config: PidConfig, -} + fn read_fan_presence( + &mut self, + ) -> Result< + impl Iterator, + crate::SeqError, + > { + Ok(core::iter::empty()) + } -impl Bsp { - pub fn fan_control( + fn read_fan_rpms( + &mut self, + ) -> impl Iterator { + core::iter::empty() + } + + fn read_misc_sensors( &self, - _fan: crate::Fan, - ) -> Result, ControllerInitError> { - // Because we have zero fans, nothing should ever call fan_control. - unreachable!() + ) -> impl Iterator< + Item = ( + SensorId, + Result, + ), + > { + core::iter::empty() + } + + fn read_inputs( + &mut self, + _mode: PowerBitmask, + ) -> impl Iterator { + core::iter::empty() + } + + fn read_dynamic_inputs_back_from_sensor_api( + &mut self, + _sensor_api: &task_sensor_api::Sensor, + ) { + // no dynamic inputs } - pub fn for_each_fctrl( + // returns Ok(true) if this was a new input + fn update_dynamic_input( + &mut self, + _index: usize, + _model: ThermalProperties, + ) -> Result { + // No dynamic inputs here, todo: static assert this + Err(ThermalError::InvalidIndex) + } + + // sets last_reading to Some(Missing), returns sensor id + fn remove_dynamic_input( + &mut self, + _index: usize, + ) -> Result { + // No dynamic inputs here, todo: static assert this + Err(ThermalError::InvalidIndex) + } + + fn all_inputs_present(&self) -> bool { + true + } + + fn all_inputs_allow_missing( &self, - mut _fctrl: impl FnMut(FanControl<'_>), - ) -> Result<(), ControllerInitError> { - // This one's reeeeal easy. - Ok(()) + ) -> impl Iterator> { + core::iter::empty() } - pub fn power_mode(&self) -> PowerBitmask { - PowerBitmask::empty() + fn all_inputs( + &self, + ) -> impl Iterator> { + core::iter::empty() } - pub fn power_down(&self) -> Result<(), SeqError> { - Ok(()) + fn reset_all_values(&mut self) { + // nothing to reset! } - pub fn get_fan_presence(&self) -> Result, SeqError> { - Ok(Fans::new()) + fn set_all_watchdogs( + &mut self, + _watchdog: drv_i2c_devices::max31790::I2cWatchdog, + ) -> Result<(), task_thermal_api::ThermalError> { + Ok(()) } - pub fn fan_sensor_id(&self, _i: usize) -> SensorId { - panic!("no fans, this should not be called"); + fn set_all_fan_duty( + &mut self, + _duty: userlib::units::PWMDuty, + ) -> Result<(), task_thermal_api::ThermalError> { + Ok(()) } +} +impl Bsp { pub fn new(_i2c_task: TaskId) -> Self { - Self { - // PID config doesn't matter since we have no fans. - pid_config: PidConfig { - zero: 0., - gain_p: 0., - gain_i: 0., - gain_d: 0., - min_output: 0., - max_output: 100., - }, - - inputs: &INPUTS, - dynamic_inputs: &[], - - // We monitor and log all of the air temperatures - misc_sensors: &MISC_SENSORS, - } + Self {} } } - -const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = []; - -const MISC_SENSORS: [TemperatureSensor; NUM_TEMPERATURE_SENSORS] = []; diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index aa8f588e52..e5c22c5b27 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -21,6 +21,7 @@ use userlib::{ /// different ways. Not all fans are guaranteed to be there at all times so /// their corresponding sensor is an `Option`. We should not read the RPM of /// fans which are not present and their PWM should only be driven low. +#[allow(dead_code)] // Not all bsps have fans! pub struct Fan { pub rpm_sensor_id: SensorId, // TODO(AJM): Distinguish between "have never heard from" and "have heard @@ -29,6 +30,7 @@ pub struct Fan { pub bsp_data: D, } +#[allow(dead_code)] // Not all bsps have fans! impl Fan { pub const fn new(rpm_sensor_id: SensorId, bsp_data: D) -> Self { Self { @@ -39,6 +41,7 @@ impl Fan { } } +#[allow(dead_code)] // Not all bsps have fans! pub enum FanPresence { Present { fan_id: u8, @@ -51,6 +54,7 @@ pub enum FanPresence { }, } +#[allow(dead_code)] // Not all bsps have fans! pub enum FanReading { PresentSuccess { rpm: Rpm, @@ -109,6 +113,7 @@ pub(crate) enum ChannelType { } /// The outcome of [`InputChannel::do_reading()`]. +#[allow(dead_code)] // Not all bsps have inputs! pub enum InputReadingOutcome { /// Sensor was not read because the power mode indicated that it would not /// be enabled in this state. @@ -192,36 +197,6 @@ impl ThermalSensorErrors { //////////////////////////////////////////////////////////////////////////////// -// /// Helper function to retry initialization several times, logging errors -// pub(crate) fn retry_init Result<(), ControllerInitError>>( -// mut init: F, -// ) { -// // When we first start up, try to initialize the fan controller a few -// // times, in case there's a transient I2C error. -// for remaining in (0..3).rev() { -// if init().is_ok() { -// break; -// } -// ringbuf_entry!(Trace::FanControllerInitRetry { remaining }); -// } -// } - -// pub(crate) struct ControllerInitError(pub(crate) ResponseCode); - -// impl From for ThermalError { -// fn from(_: ControllerInitError) -> Self { -// ThermalError::FanControllerUninitialized -// } -// } - -// impl From for SensorReadError { -// fn from(ControllerInitError(code): ControllerInitError) -> Self { -// SensorReadError::I2cError(code) -// } -// } - -//////////////////////////////////////////////////////////////////////////////// - /// The thermal control loop. /// /// This object uses slices of sensors and fans, which must be owned @@ -271,6 +246,7 @@ pub(crate) struct ThermalControl<'a, B: BspInterface> { /// Represents the state of a temperature sensor, which either has a valid /// reading or is marked as inactive (due to power state or being missing) #[derive(Copy, Clone, Debug)] +#[allow(dead_code)] // Not all bsps have inputs! pub enum TemperatureReading { /// Normal reading, timestamped using monotonic system time Valid(TimestampedTemperatureReading), diff --git a/task/thermal/src/main.rs b/task/thermal/src/main.rs index 4befe3e86f..1ffabb177d 100644 --- a/task/thermal/src/main.rs +++ b/task/thermal/src/main.rs @@ -135,8 +135,11 @@ enum Trace { PowerModeChanged(PowerBitmask), PowerDownFailed(SeqError), ControlError(#[count(children)] ThermalError), + #[allow(dead_code)] // no fans? no trace! FanControllerInitialized, + #[allow(dead_code)] // no fans? no trace! FanControllerInitError(#[count(children)] ResponseCode), + #[allow(dead_code)] // no fans? no trace! FanControllerInitRetry { remaining: usize, }, From b2de0ab9bc24597a8688de5db968d6d5ac206fb9 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 15:59:37 +0200 Subject: [PATCH 20/33] WIP sidecar, but I think I need to check something --- task/thermal/src/bsp/sidecar_bcd.rs | 365 +++++++++++++++++++++------- 1 file changed, 275 insertions(+), 90 deletions(-) diff --git a/task/thermal/src/bsp/sidecar_bcd.rs b/task/thermal/src/bsp/sidecar_bcd.rs index bb062eac2b..52249abb03 100644 --- a/task/thermal/src/bsp/sidecar_bcd.rs +++ b/task/thermal/src/bsp/sidecar_bcd.rs @@ -5,13 +5,15 @@ //! BSP for Sidecar use crate::control::{ - ChannelType, ControllerInitError, Device, FanControl, Fans, InputChannel, - Max31790State, PidConfig, TemperatureSensor, + ChannelType, PidConfig, TemperatureReading, TimestampedTemperatureReading, }; +use crate::control::{DynamicInputChannel, FanReading}; use drv_i2c_devices::tmp451::*; pub use drv_sidecar_seq_api::SeqError; use drv_sidecar_seq_api::{Sequencer, TofinoSeqState, TofinoSequencerPolicy}; use task_sensor_api::SensorId; +use task_thermal_api::SensorReadError; +use task_thermal_api::ThermalError; use task_thermal_api::ThermalProperties; use userlib::{TaskId, UnwrapLite, task_slot, units::Celsius}; @@ -19,6 +21,17 @@ include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); use i2c_config::devices; use i2c_config::sensors; +// This BSP uses i2c temperature inputs +#[path = "./common/i2c_temp_input.rs"] +mod i2c_temp_input; +use i2c_temp_input::{ + Device, InputChannel, InputChannelMetadata, TemperatureSensor, +}; + +#[path = "./common/max31790.rs"] +mod max31790; +use max31790::Max31790State; + task_slot!(SEQUENCER, sequencer); //////////////////////////////////////////////////////////////////////////////// @@ -58,8 +71,9 @@ bitflags::bitflags! { #[allow(dead_code)] pub(crate) struct Bsp { /// Controlled sensors - pub inputs: &'static [InputChannel; NUM_TEMPERATURE_INPUTS], - pub dynamic_inputs: &'static [SensorId; NUM_DYNAMIC_TEMPERATURE_INPUTS], + pub inputs: &'static mut [InputChannel; NUM_TEMPERATURE_INPUTS], + pub dynamic_inputs: + &'static mut [DynamicInputChannel; NUM_DYNAMIC_TEMPERATURE_INPUTS], /// Monitored sensors pub misc_sensors: &'static [TemperatureSensor; NUM_TEMPERATURE_SENSORS], @@ -69,75 +83,28 @@ pub(crate) struct Bsp { fctrl_west: Max31790State, seq: Sequencer, + i2c_task: TaskId, pub pid_config: PidConfig, } -impl Bsp { - pub fn fan_control( - &mut self, - fan: crate::Fan, - ) -> Result, ControllerInitError> { - // - // Fan module 0/1 are on the east max31790; fan module 2/3 are on west - // max31790. Each fan module has two fans which are not mapped in a - // straightforward way. Additionally, our MAX31790 code has zero-indexed - // fan indices, but the part's datasheet and schematic symbol are - // one-indexed. Here is the mapping of the system level index to - // controller and fan index: - // - // System Index Controller Fan MAX31790 Fan (Datasheet) - // 0 East ESE 2 (3) - // 1 East ENE 3 (4) - // 2 East SE 0 (1) - // 3 East NE 1 (2) - // 4 West SW 2 (3) - // 5 West NW 3 (4) - // 6 West WSW 0 (1) - // 7 West WNW 1 (2) - // - - // The supplied `fan` is the System Index. From that we can map to a fan - // and controller. - let (fan_logical, controller) = if fan.0 < 4 { - (fan.0, &mut self.fctrl_east) - } else if fan.0 < 8 { - (fan.0 - 4, &mut self.fctrl_west) - } else { - panic!(); - }; - // These are hooked up weird on the board; handle that here - let fan_physical = match fan_logical { - 0 => 2, - 1 => 3, - 2 => 0, - 3 => 1, - _ => panic!(), - }; - Ok(FanControl::Max31790( - controller.try_initialize()?, - fan_physical.try_into().unwrap_lite(), - )) - } - - pub fn for_each_fctrl( - &mut self, - mut fctrl: impl FnMut(FanControl<'_>), - ) -> Result<(), ControllerInitError> { - let mut last_err = Ok(()); - // Run the function on each fan control chip - match self.fan_control(0.into()) { - Ok(c) => fctrl(c), - Err(e) => last_err = Err(e), - } - match self.fan_control(4.into()) { - Ok(c) => fctrl(c), - Err(e) => last_err = Err(e), - } - last_err +impl crate::control::BspInterface for Bsp { + // TODO: this is all made up, copied from tuned Gimlet values + const PID_CONFIG: PidConfig = PidConfig { + zero: 35.0, + gain_p: 1.75, + gain_i: 0.0135, + gain_d: 0.4, + min_output: 0.0, + max_output: 100.0, + }; + + fn power_down(&self) -> Result<(), crate::SeqError> { + self.seq + .set_tofino_seq_policy(TofinoSequencerPolicy::Disabled) } - pub fn power_mode(&self) -> PowerBitmask { + fn power_mode(&self) -> PowerBitmask { match self.seq.tofino_seq_state() { Ok(r) => match r { TofinoSeqState::A0 => PowerBitmask::A0, @@ -150,28 +117,240 @@ impl Bsp { } } - pub fn power_down(&self) -> Result<(), SeqError> { - self.seq - .set_tofino_seq_policy(TofinoSequencerPolicy::Disabled) + fn read_fan_presence( + &mut self, + ) -> Result< + impl Iterator, + crate::SeqError, + > { + todo!(); + Ok(core::iter::empty()) + } + + fn read_fan_rpms(&mut self) -> impl Iterator { + // TODO: This is wrong, I think + // self.fctrl_east + // .read_fan_rpms(self.fans) + // .chain(self.fctrl_west.read_fan_rpms(self.fans)) + todo!(); + core::iter::empty() + } + + fn read_misc_sensors( + &self, + ) -> impl Iterator)> + { + self.misc_sensors.iter().map(|s| { + let res = s.read_temp(self.i2c_task); + (s.sensor_id, res) + }) + } + + fn read_inputs( + &mut self, + mode: PowerBitmask, + ) -> impl Iterator { + let task = &self.i2c_task; + self.inputs + .iter_mut() + .map(move |i| i.do_reading(mode, task)) + } + + fn read_dynamic_inputs_back_from_sensor_api( + &mut self, + sensor_api: &task_sensor_api::Sensor, + ) { + self.dynamic_inputs + .iter_mut() + .filter(|di| di.last_reading.is_some()) + .for_each(|di| { + let res = sensor_api.get_reading(di.sensor_id).map(|r| { + TemperatureReading::Valid(TimestampedTemperatureReading { + time_ms: r.timestamp, + value: Celsius(r.value), + }) + }); + di.last_reading = Some(match res { + Ok(r) => r, + Err(_) => TemperatureReading::Inactive, + }); + }); + } + + fn update_dynamic_input( + &mut self, + index: usize, + model: ThermalProperties, + ) -> Result { + let Some(di) = self.dynamic_inputs.get_mut(index) else { + return Err(ThermalError::InvalidIndex); + }; + if di.last_reading.is_some() { + // TODO: I think the old code just ignored this? + return Ok(false); + } + di.model = model; + di.last_reading = Some(TemperatureReading::Inactive); + Ok(true) + } + + fn remove_dynamic_input( + &mut self, + index: usize, + ) -> Result { + let Some(di) = self.dynamic_inputs.get_mut(index) else { + return Err(ThermalError::InvalidIndex); + }; + // TODO: do we return an err if this was already removed? Check old code + di.last_reading = None; + Ok(di.sensor_id) + } + + fn all_inputs_present(&self) -> bool { + // self.inputs.iter().all(InputChannel::has_reading) + // && self + // .dynamic_inputs + // .iter() + // .all(DynamicInputChannel::has_reading) + todo!() + } + + fn all_inputs_allow_missing( + &self, + ) -> impl Iterator> { + todo!(); + core::iter::empty() + } + + fn all_inputs( + &self, + ) -> impl Iterator> { + todo!(); + core::iter::empty() } - pub fn get_fan_presence(&self) -> Result, SeqError> { - let presence = self.seq.fan_module_presence()?; - let mut next = Fans::new(); - for (i, present) in presence.0.iter().enumerate() { - // two fans per module - let idx = i * 2; - if *present { - next[idx] = Some(sensors::MAX31790_SPEED_SENSORS[idx]); - next[idx + 1] = Some(sensors::MAX31790_SPEED_SENSORS[idx + 1]); - } + fn reset_all_values(&mut self) { + todo!() + } + + fn set_all_watchdogs( + &mut self, + watchdog: drv_i2c_devices::max31790::I2cWatchdog, + ) -> Result<(), ThermalError> { + // Try setting both, NOT returning early if either failed + let res_east = self + .fctrl_east + .try_initialize() + .map_err(ThermalError::from) + .and_then(|east| { + east.set_watchdog(watchdog) + .map_err(|_| ThermalError::DeviceError) + }); + let res_west = self + .fctrl_west + .try_initialize() + .map_err(ThermalError::from) + .and_then(|west| { + west.set_watchdog(watchdog) + .map_err(|_| ThermalError::DeviceError) + }); + + match (res_east, res_west) { + (Err(e), _) => Err(e), + (_, Err(w)) => Err(w), + (Ok(()), Ok(())) => Ok(()), } - Ok(next) } - pub fn fan_sensor_id(&self, i: usize) -> SensorId { - sensors::MAX31790_SPEED_SENSORS[i] + fn set_all_fan_duty( + &mut self, + duty: userlib::units::PWMDuty, + ) -> Result<(), ThermalError> { + todo!() } +} + +impl Bsp { + // pub fn fan_control( + // &mut self, + // fan: crate::Fan, + // ) -> Result, ControllerInitError> { + // // + // // Fan module 0/1 are on the east max31790; fan module 2/3 are on west + // // max31790. Each fan module has two fans which are not mapped in a + // // straightforward way. Additionally, our MAX31790 code has zero-indexed + // // fan indices, but the part's datasheet and schematic symbol are + // // one-indexed. Here is the mapping of the system level index to + // // controller and fan index: + // // + // // System Index Controller Fan MAX31790 Fan (Datasheet) + // // 0 East ESE 2 (3) + // // 1 East ENE 3 (4) + // // 2 East SE 0 (1) + // // 3 East NE 1 (2) + // // 4 West SW 2 (3) + // // 5 West NW 3 (4) + // // 6 West WSW 0 (1) + // // 7 West WNW 1 (2) + // // + + // // The supplied `fan` is the System Index. From that we can map to a fan + // // and controller. + // let (fan_logical, controller) = if fan.0 < 4 { + // (fan.0, &mut self.fctrl_east) + // } else if fan.0 < 8 { + // (fan.0 - 4, &mut self.fctrl_west) + // } else { + // panic!(); + // }; + // // These are hooked up weird on the board; handle that here + // let fan_physical = match fan_logical { + // 0 => 2, + // 1 => 3, + // 2 => 0, + // 3 => 1, + // _ => panic!(), + // }; + // Ok(FanControl::Max31790( + // controller.try_initialize()?, + // fan_physical.try_into().unwrap_lite(), + // )) + // } + + // pub fn for_each_fctrl( + // &mut self, + // mut fctrl: impl FnMut(FanControl<'_>), + // ) -> Result<(), ControllerInitError> { + // let mut last_err = Ok(()); + // // Run the function on each fan control chip + // match self.fan_control(0.into()) { + // Ok(c) => fctrl(c), + // Err(e) => last_err = Err(e), + // } + // match self.fan_control(4.into()) { + // Ok(c) => fctrl(c), + // Err(e) => last_err = Err(e), + // } + // last_err + // } + + // pub fn get_fan_presence(&self) -> Result, SeqError> { + // let presence = self.seq.fan_module_presence()?; + // let mut next = Fans::new(); + // for (i, present) in presence.0.iter().enumerate() { + // // two fans per module + // let idx = i * 2; + // if *present { + // next[idx] = Some(sensors::MAX31790_SPEED_SENSORS[idx]); + // next[idx + 1] = Some(sensors::MAX31790_SPEED_SENSORS[idx + 1]); + // } + // } + // Ok(next) + // } + + // pub fn fan_sensor_id(&self, i: usize) -> SensorId { + // sensors::MAX31790_SPEED_SENSORS[i] + // } pub fn new(i2c_task: TaskId) -> Self { // Handle for the sequencer task, which we check for power state and @@ -181,6 +360,10 @@ impl Bsp { let fctrl_east = Max31790State::new(&devices::max31790_east(i2c_task)); let fctrl_west = Max31790State::new(&devices::max31790_west(i2c_task)); + static INPUTS_ONCE: static_cell::ClaimOnceCell< + [InputChannel; NUM_TEMPERATURE_INPUTS], + > = static_cell::ClaimOnceCell::new(INPUTS); + Self { seq, fctrl_east, @@ -196,12 +379,14 @@ impl Bsp { max_output: 100.0, }, - inputs: &INPUTS, - dynamic_inputs: - &drv_transceivers_api::TRANSCEIVER_TEMPERATURE_SENSORS, + inputs: INPUTS_ONCE.claim(), + dynamic_inputs: todo!(), + // dynamic_inputs: + // &drv_transceivers_api::TRANSCEIVER_TEMPERATURE_SENSORS, // We monitor and log all of the air temperatures misc_sensors: &MISC_SENSORS, + i2c_task, } } } @@ -226,7 +411,7 @@ const VSC7448_THERMALS: ThermalProperties = ThermalProperties { }; const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ - InputChannel::new( + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Tmp451(Target::Remote), devices::tmp451_tf2, @@ -235,8 +420,8 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ TF2_THERMALS, PowerBitmask::A0, ChannelType::MustBePresent, - ), - InputChannel::new( + )), + InputChannel::new(&InputChannelMetadata::new( TemperatureSensor::new( Device::Tmp451(Target::Remote), devices::tmp451_vsc7448, @@ -245,7 +430,7 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ VSC7448_THERMALS, PowerBitmask::A0_OR_A2, ChannelType::MustBePresent, - ), + )), ]; const MISC_SENSORS: [TemperatureSensor; NUM_TEMPERATURE_SENSORS] = [ From 8102d449f63941b3a75e4a1385caad7e64d449a3 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 16:48:59 +0200 Subject: [PATCH 21/33] Clarify state tracking of InputChannel --- task/thermal/src/bsp/common/i2c_temp_input.rs | 25 +++--- task/thermal/src/bsp/cosmo_ab.rs | 64 ++++----------- task/thermal/src/bsp/gimlet_bcdef.rs | 17 ++-- task/thermal/src/bsp/grapefruit.rs | 16 +--- task/thermal/src/bsp/minibar.rs | 8 +- task/thermal/src/bsp/sidecar_bcd.rs | 9 +-- task/thermal/src/control.rs | 81 ++++++++----------- 7 files changed, 74 insertions(+), 146 deletions(-) diff --git a/task/thermal/src/bsp/common/i2c_temp_input.rs b/task/thermal/src/bsp/common/i2c_temp_input.rs index 4722353a37..6305c9df5f 100644 --- a/task/thermal/src/bsp/common/i2c_temp_input.rs +++ b/task/thermal/src/bsp/common/i2c_temp_input.rs @@ -92,7 +92,7 @@ pub(crate) struct InputChannelMetadata { /// particular component in the system. pub(crate) struct InputChannel { metadata: &'static InputChannelMetadata, - last_reading: Option, + last_reading: TemperatureReading, } /// InputChannelMetadata is the constant description portion of an InputChannel. @@ -124,19 +124,21 @@ impl InputChannel { pub const fn new(metadata: &'static InputChannelMetadata) -> Self { Self { metadata, - last_reading: None, + last_reading: TemperatureReading::Inactive, } } pub fn has_reading(&self) -> bool { - self.last_reading.is_some() + matches!(self.last_reading, TemperatureReading::Valid(..)) } /// Get current stored status. /// /// Returns None if we do not have a reading stored. pub fn status(&self) -> Option> { - let reading = self.last_reading.as_ref()?; + let TemperatureReading::Valid(ref reading) = self.last_reading else { + return None; + }; Some(InputStatus { id: self.metadata.sensor.sensor_id, reading, @@ -145,7 +147,7 @@ impl InputChannel { } pub fn reset_value(&mut self) { - self.last_reading = None; + self.last_reading = TemperatureReading::Inactive; } pub fn do_reading( @@ -157,19 +159,18 @@ impl InputChannel { // If we're not supposed to be on, don't even ask. if !mode.intersects(self.metadata.power_mode_mask) { - self.last_reading = Some(TemperatureReading::Inactive); + self.last_reading = TemperatureReading::Inactive; return InputReadingOutcome::Unpowered { id }; } match self.metadata.sensor.read_temp(*i2c_task) { Ok(value) => { let now = sys_get_timer().now; - self.last_reading = Some(TemperatureReading::Valid( - TimestampedTemperatureReading { + self.last_reading = + TemperatureReading::Valid(TimestampedTemperatureReading { time_ms: now, value, - }, - )); + }); InputReadingOutcome::Success { id, now, value } } Err(e) => { @@ -186,10 +187,10 @@ impl InputChannel { let se = SensorError::from(NoData::from(e)); match (self.metadata.ty, se) { (ChannelType::Removable, SensorError::NotPresent) => { - self.last_reading = Some(TemperatureReading::Inactive); + self.last_reading = TemperatureReading::Inactive; } (ChannelType::RemovableAndErrorProne, _) => { - self.last_reading = Some(TemperatureReading::Inactive); + self.last_reading = TemperatureReading::Inactive; } _ => { // In all other cases, just leave whatever the last diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 67b1514eb5..bee3375a85 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -7,7 +7,7 @@ use crate::{ control::{ ChannelType, FanPresence, FanReading, InputReadingOutcome, InputStatus, - Max31790State, PidConfig, + PidConfig, }, i2c_config::{devices, sensors}, }; @@ -17,7 +17,7 @@ use drv_i2c_devices::max31790::I2cWatchdog; use task_sensor_api::{Sensor, SensorId}; use task_thermal_api::{SensorReadError, ThermalError, ThermalProperties}; use userlib::{ - TaskId, UnwrapLite, task_slot, + TaskId, task_slot, units::{Celsius, PWMDuty}, }; @@ -28,6 +28,10 @@ use i2c_temp_input::{ Device, InputChannel, InputChannelMetadata, TemperatureSensor, }; +#[path = "./common/max31790.rs"] +mod max31790; +use max31790::Max31790State; + task_slot!(SEQ, cosmo_seq); // We monitor the TMP117 air temperature sensors, but don't use them as part of @@ -127,48 +131,17 @@ impl crate::control::BspInterface for Bsp { } fn read_fan_rpms(&mut self) -> impl Iterator { - // Try to initialize the fan controller once at the start of the loop - let mut fctrl = - self.fctrl.try_initialize().map_err(SensorReadError::from); - - // TODO: Maybe there's a way to make this a method on Fan that we can - // call, kind of like InputStatus? - self.fans.iter_mut().map(move |f| { - // If initialization failed, then we short circuit to return that - // original error, copied for each fan we're going to report. - let fctrl = fctrl.as_mut().map_err(|e| *e); - - // If it was a success, attempt to read the RPMs, and either report - // that success or that error for each fan rpm. - let res = fctrl.and_then(|fc| { - fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) - }); - match res { - Ok(rpm) => { - f.last_reading = Some(rpm); - FanReading::PresentSuccess { - rpm, - sensor_id: f.rpm_sensor_id, - } - } - Err(error) => { - f.last_reading = None; - FanReading::PresentError { - error, - sensor_id: f.rpm_sensor_id, - } - } - } - }) + self.fctrl.read_fan_rpms(self.fans) } fn read_misc_sensors( &self, ) -> impl Iterator)> { - self.misc_sensors - .iter() - .map(|s| (s.sensor_id, s.read_temp(self.i2c_task))) + self.misc_sensors.iter().map(|s| { + let res = s.read_temp(self.i2c_task); + (s.sensor_id, res) + }) } fn read_inputs( @@ -214,21 +187,14 @@ impl crate::control::BspInterface for Bsp { // && self.dynamic_inputs... } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // Inputs and Dynamic Inputs that are missing will be skipped. - fn all_inputs_allow_missing( - &self, - ) -> impl Iterator> { - self.inputs.iter().filter_map(InputChannel::status) - // .zip(self.dynamic_inputs...) - } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. // All inputs MUST have a previous reading or this will panic, though the // readings may be allowed to be Missing if the model allows it. Dynamic // inputs that are not present will be skipped. - fn all_inputs(&self) -> impl Iterator> { - self.inputs.iter().map(|input| input.status().unwrap_lite()) + fn all_present_inputs_status( + &self, + ) -> impl Iterator> { + self.inputs.iter().filter_map(|input| input.status()) // .zip(self.dynamic_inputs...) } diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index bb827487b2..beebed03a3 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -17,7 +17,7 @@ use drv_i2c_devices::max31790::I2cWatchdog; use task_sensor_api::{Sensor, SensorId}; use task_thermal_api::{SensorReadError, ThermalError, ThermalProperties}; use userlib::{ - TaskId, UnwrapLite, task_slot, + TaskId, task_slot, units::{Celsius, PWMDuty}, }; @@ -232,21 +232,14 @@ impl crate::control::BspInterface for Bsp { // && self.dynamic_inputs... } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // Inputs and Dynamic Inputs that are missing will be skipped. - fn all_inputs_allow_missing( - &self, - ) -> impl Iterator> { - self.inputs.iter().filter_map(InputChannel::status) - // .zip(self.dynamic_inputs...) - } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. // All inputs MUST have a previous reading or this will panic, though the // readings may be allowed to be Missing if the model allows it. Dynamic // inputs that are not present will be skipped. - fn all_inputs(&self) -> impl Iterator> { - self.inputs.iter().map(|input| input.status().unwrap_lite()) + fn all_present_inputs_status( + &self, + ) -> impl Iterator> { + self.inputs.iter().filter_map(|input| input.status()) // .zip(self.dynamic_inputs...) } diff --git a/task/thermal/src/bsp/grapefruit.rs b/task/thermal/src/bsp/grapefruit.rs index c42c2e3ba2..9c15adbf70 100644 --- a/task/thermal/src/bsp/grapefruit.rs +++ b/task/thermal/src/bsp/grapefruit.rs @@ -10,7 +10,6 @@ use drv_i2c_devices::max31790::I2cWatchdog; use task_sensor_api::SensorId; use task_thermal_api::{ThermalError, ThermalProperties}; use userlib::TaskId; -use userlib::UnwrapLite; use userlib::units::{Celsius, PWMDuty}; include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); @@ -152,21 +151,14 @@ impl crate::control::BspInterface for Bsp { // && self.dynamic_inputs... } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // Inputs and Dynamic Inputs that are missing will be skipped. - fn all_inputs_allow_missing( - &self, - ) -> impl Iterator> { - self.inputs.iter().filter_map(InputChannel::status) - // .zip(self.dynamic_inputs...) - } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. // All inputs MUST have a previous reading or this will panic, though the // readings may be allowed to be Missing if the model allows it. Dynamic // inputs that are not present will be skipped. - fn all_inputs(&self) -> impl Iterator> { - self.inputs.iter().map(|input| input.status().unwrap_lite()) + fn all_present_inputs_status( + &self, + ) -> impl Iterator> { + self.inputs.iter().filter_map(|input| input.status()) // .zip(self.dynamic_inputs...) } diff --git a/task/thermal/src/bsp/minibar.rs b/task/thermal/src/bsp/minibar.rs index af6719e387..13d1912f97 100644 --- a/task/thermal/src/bsp/minibar.rs +++ b/task/thermal/src/bsp/minibar.rs @@ -111,13 +111,7 @@ impl crate::control::BspInterface for Bsp { true } - fn all_inputs_allow_missing( - &self, - ) -> impl Iterator> { - core::iter::empty() - } - - fn all_inputs( + fn all_present_inputs_status( &self, ) -> impl Iterator> { core::iter::empty() diff --git a/task/thermal/src/bsp/sidecar_bcd.rs b/task/thermal/src/bsp/sidecar_bcd.rs index 52249abb03..9c0a25527e 100644 --- a/task/thermal/src/bsp/sidecar_bcd.rs +++ b/task/thermal/src/bsp/sidecar_bcd.rs @@ -215,14 +215,7 @@ impl crate::control::BspInterface for Bsp { todo!() } - fn all_inputs_allow_missing( - &self, - ) -> impl Iterator> { - todo!(); - core::iter::empty() - } - - fn all_inputs( + fn all_present_inputs_status( &self, ) -> impl Iterator> { todo!(); diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index e5c22c5b27..014dae34e2 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -25,7 +25,7 @@ use userlib::{ pub struct Fan { pub rpm_sensor_id: SensorId, // TODO(AJM): Distinguish between "have never heard from" and "have heard - // from but has since gone missing"? + // from but has since gone missing"? Like TemperatureReading? pub last_reading: Option, pub bsp_data: D, } @@ -134,7 +134,7 @@ pub enum InputReadingOutcome { /// Status of a regular or dynamic input pub struct InputStatus<'a> { pub id: SensorId, - pub reading: &'a TemperatureReading, + pub reading: &'a TimestampedTemperatureReading, pub model: &'a ThermalProperties, } @@ -251,7 +251,8 @@ pub enum TemperatureReading { /// Normal reading, timestamped using monotonic system time Valid(TimestampedTemperatureReading), - /// This sensor is not used in the current power state + /// This sensor is not used in the current power state, or has not been + /// read since changing operational state of the device Inactive, } @@ -787,14 +788,34 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { let mut any_power_down = None; let mut any_critical = None; let mut worst_margin = f32::MAX; + let all_inputs_present = self.bsp.all_inputs_present(); + + // TODO(AJM): Assert all present if !Boot + match self.state { + ThermalControlState::Uncontrollable => { + return Ok(ControlResult::PowerDown); + } + ThermalControlState::Boot => { + // We allow boot to be missing present items + } + ThermalControlState::Running { .. } + | ThermalControlState::Critical { .. } + | ThermalControlState::FanParty => { + // TODO: Should this just move us back to the Boot state? This + // should not be possible by construction, as moving from + // Invalid to Valid reading is "latching" unless we reset the + // state. + assert!(all_inputs_present); + } + }; // Remember, positive margin means that all parts are happily // below their max temperature; negative means someone is // overheating. We want to pick the _smallest_ margin, since // that's the part which is most overheated. - let f = |InputStatus { id, reading, model }| { - if let TemperatureReading::Valid(v) = reading { - let worst_case = v.worst_case(now_ms, model); + self.bsp.all_present_inputs_status().for_each( + |InputStatus { id, reading, model }| { + let worst_case = reading.worst_case(now_ms, model); let temperature = worst_case.worst_case_temp; all_nominal &= model.is_nominal(temperature); if model.should_power_down(temperature) { @@ -804,36 +825,8 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { any_critical = Some((id, worst_case)); } worst_margin = worst_margin.min(model.margin(temperature).0); - } - - // Hidden assumption here! ALL `TemperatureReading`s here will be - // valid, UNLESS: - // - // 1. We are in Boot state and we allow missing inputs, which will - // skip anything we haven't heard yet (but will report `false` - // for `bsp.all_inputs_present()`) - // 2. For input sensors: the given input is not active in this - // power state - // 3. For dynamic sensors: The given dynamic input has not been - // given to us to activate - // - // We don't necessarily *check* that is the case though, and we - // might want to in the all_inputs(_allow_missing_inputs) - // functions! - }; - match self.state { - ThermalControlState::Boot => { - self.bsp.all_inputs_allow_missing().for_each(f) - } - ThermalControlState::Running { .. } - | ThermalControlState::Critical { .. } - | ThermalControlState::FanParty => { - self.bsp.all_inputs().for_each(f) - } - ThermalControlState::Uncontrollable => { - return Ok(ControlResult::PowerDown); - } - }; + }, + ); // In any state, if we've reached the "any_power_down" threshold, then // it's time to go. @@ -850,7 +843,7 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { // that's a bit more invasive of a change Ok(match &mut self.state { ThermalControlState::Boot => { - if self.bsp.all_inputs_present() { + if all_inputs_present { self.transition_to_running(worst_margin, now_ms) } else { ControlResult::Pwm(PWMDuty( @@ -1229,15 +1222,11 @@ pub trait BspInterface { fn all_inputs_present(&self) -> bool; // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // Inputs and Dynamic Inputs that are missing will be skipped. - fn all_inputs_allow_missing(&self) - -> impl Iterator>; - - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // All inputs MUST have a previous reading or this will panic, though the - // readings may be allowed to be Missing if the model allows it. Dynamic - // inputs that are not present will be skipped. - fn all_inputs(&self) -> impl Iterator>; + // Inputs or Dynamic Inputs that are Invalid will be skipped. Dynamic Inputs + // that are not present will be skipped. + fn all_present_inputs_status( + &self, + ) -> impl Iterator>; fn reset_all_values(&mut self); From 96e716c0c6e019b16cbdc378ed83e2f45c2bdfe0 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 18:33:31 +0200 Subject: [PATCH 22/33] Implement the rest of sidecar --- task/thermal/src/bsp/common/emc2305.rs | 8 +- task/thermal/src/bsp/common/max31790.rs | 23 +- task/thermal/src/bsp/cosmo_ab.rs | 6 +- task/thermal/src/bsp/gimlet_bcdef.rs | 6 +- task/thermal/src/bsp/grapefruit.rs | 4 +- task/thermal/src/bsp/minibar.rs | 2 +- task/thermal/src/bsp/sidecar_bcd.rs | 302 +++++++++++++++--------- task/thermal/src/control.rs | 36 ++- 8 files changed, 243 insertions(+), 144 deletions(-) diff --git a/task/thermal/src/bsp/common/emc2305.rs b/task/thermal/src/bsp/common/emc2305.rs index 3af9b2f3ab..d3cd79a37e 100644 --- a/task/thermal/src/bsp/common/emc2305.rs +++ b/task/thermal/src/bsp/common/emc2305.rs @@ -11,7 +11,7 @@ use task_thermal_api::{SensorReadError, ThermalError}; use crate::{ __RINGBUF, Trace, - control::{Fan, FanReading}, + control::{Fan, FanStatus}, }; /// Tracks whether a Emc2305 fan controller has been initialized, and @@ -68,7 +68,7 @@ impl Emc2305State { pub(crate) fn read_fan_rpms( &mut self, fans: &mut [Fan], - ) -> impl Iterator { + ) -> impl Iterator { // Try to initialize the fan controller once at the start of the loop let mut fctrl = self.try_initialize().map_err(SensorReadError::from); @@ -87,14 +87,14 @@ impl Emc2305State { match res { Ok(rpm) => { f.last_reading = Some(rpm); - FanReading::PresentSuccess { + FanStatus::PresentSuccess { rpm, sensor_id: f.rpm_sensor_id, } } Err(error) => { f.last_reading = None; - FanReading::PresentError { + FanStatus::PresentError { error, sensor_id: f.rpm_sensor_id, } diff --git a/task/thermal/src/bsp/common/max31790.rs b/task/thermal/src/bsp/common/max31790.rs index 128e9ec6c7..6a414bff43 100644 --- a/task/thermal/src/bsp/common/max31790.rs +++ b/task/thermal/src/bsp/common/max31790.rs @@ -13,7 +13,7 @@ use crate::{ // control::{ControllerInitError, retry_init}, __RINGBUF, Trace, - control::{Fan, FanReading}, + control::{Fan, FanReading, FanStatus}, }; /// Tracks whether a MAX31790 fan controller has been initialized, and @@ -75,13 +75,18 @@ impl Max31790State { pub(crate) fn read_fan_rpms( &mut self, fans: &mut [Fan], - ) -> impl Iterator { + ) -> impl Iterator { // Try to initialize the fan controller once at the start of the loop let mut fctrl = self.try_initialize().map_err(SensorReadError::from); // TODO: Maybe there's a way to make this a method on Fan that we can // call, kind of like InputStatus? fans.iter_mut().map(move |f| { + let sensor_id = f.rpm_sensor_id; + if f.last_reading.is_none() { + return FanStatus::NotPresent { sensor_id }; + } + // If initialization failed, then we short circuit to return that // original error, copied for each fan we're going to report. let fctrl = fctrl.as_mut().map_err(|e| *e); @@ -93,18 +98,12 @@ impl Max31790State { }); match res { Ok(rpm) => { - f.last_reading = Some(rpm); - FanReading::PresentSuccess { - rpm, - sensor_id: f.rpm_sensor_id, - } + f.last_reading = Some(FanReading::Valid); + FanStatus::PresentSuccess { rpm, sensor_id } } Err(error) => { - f.last_reading = None; - FanReading::PresentError { - error, - sensor_id: f.rpm_sensor_id, - } + f.last_reading = Some(FanReading::Invalid); + FanStatus::PresentError { error, sensor_id } } } }) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index bee3375a85..18c825952a 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -6,7 +6,7 @@ use crate::{ control::{ - ChannelType, FanPresence, FanReading, InputReadingOutcome, InputStatus, + ChannelType, FanPresence, FanStatus, InputReadingOutcome, InputStatus, PidConfig, }, i2c_config::{devices, sensors}, @@ -130,7 +130,7 @@ impl crate::control::BspInterface for Bsp { })) } - fn read_fan_rpms(&mut self) -> impl Iterator { + fn read_fan_rpms(&mut self) -> impl Iterator { self.fctrl.read_fan_rpms(self.fans) } @@ -195,7 +195,7 @@ impl crate::control::BspInterface for Bsp { &self, ) -> impl Iterator> { self.inputs.iter().filter_map(|input| input.status()) - // .zip(self.dynamic_inputs...) + // .chain(self.dynamic_inputs...) } fn reset_all_values(&mut self) { diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index beebed03a3..444270c1ef 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -6,7 +6,7 @@ use crate::{ control::{ - ChannelType, FanPresence, FanReading, InputReadingOutcome, InputStatus, + ChannelType, FanPresence, FanStatus, InputReadingOutcome, InputStatus, PidConfig, }, i2c_config::{devices, sensors}, @@ -175,7 +175,7 @@ impl crate::control::BspInterface for Bsp { })) } - fn read_fan_rpms(&mut self) -> impl Iterator { + fn read_fan_rpms(&mut self) -> impl Iterator { self.fctrl.read_fan_rpms(self.fans) } @@ -240,7 +240,7 @@ impl crate::control::BspInterface for Bsp { &self, ) -> impl Iterator> { self.inputs.iter().filter_map(|input| input.status()) - // .zip(self.dynamic_inputs...) + // .chain(self.dynamic_inputs...) } fn reset_all_values(&mut self) { diff --git a/task/thermal/src/bsp/grapefruit.rs b/task/thermal/src/bsp/grapefruit.rs index 9c15adbf70..32d9f9bdcd 100644 --- a/task/thermal/src/bsp/grapefruit.rs +++ b/task/thermal/src/bsp/grapefruit.rs @@ -98,7 +98,7 @@ impl crate::control::BspInterface for Bsp { fn read_fan_rpms( &mut self, - ) -> impl Iterator { + ) -> impl Iterator { self.fctrl.read_fan_rpms(self.fans) } @@ -159,7 +159,7 @@ impl crate::control::BspInterface for Bsp { &self, ) -> impl Iterator> { self.inputs.iter().filter_map(|input| input.status()) - // .zip(self.dynamic_inputs...) + // .chain(self.dynamic_inputs...) } fn reset_all_values(&mut self) { diff --git a/task/thermal/src/bsp/minibar.rs b/task/thermal/src/bsp/minibar.rs index 13d1912f97..ae00c1684a 100644 --- a/task/thermal/src/bsp/minibar.rs +++ b/task/thermal/src/bsp/minibar.rs @@ -59,7 +59,7 @@ impl crate::control::BspInterface for Bsp { fn read_fan_rpms( &mut self, - ) -> impl Iterator { + ) -> impl Iterator { core::iter::empty() } diff --git a/task/thermal/src/bsp/sidecar_bcd.rs b/task/thermal/src/bsp/sidecar_bcd.rs index 9c0a25527e..c965592a2c 100644 --- a/task/thermal/src/bsp/sidecar_bcd.rs +++ b/task/thermal/src/bsp/sidecar_bcd.rs @@ -5,9 +5,11 @@ //! BSP for Sidecar use crate::control::{ - ChannelType, PidConfig, TemperatureReading, TimestampedTemperatureReading, + ChannelType, FanPresence, FanReading, InputStatus, PidConfig, + TemperatureReading, TimestampedTemperatureReading, }; -use crate::control::{DynamicInputChannel, FanReading}; +use crate::control::{DynamicInputChannel, FanStatus}; +use drv_i2c_devices::max31790::Max31790; use drv_i2c_devices::tmp451::*; pub use drv_sidecar_seq_api::SeqError; use drv_sidecar_seq_api::{Sequencer, TofinoSeqState, TofinoSequencerPolicy}; @@ -15,7 +17,7 @@ use task_sensor_api::SensorId; use task_thermal_api::SensorReadError; use task_thermal_api::ThermalError; use task_thermal_api::ThermalProperties; -use userlib::{TaskId, UnwrapLite, task_slot, units::Celsius}; +use userlib::{TaskId, task_slot, units::Celsius}; include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); use i2c_config::devices; @@ -81,6 +83,7 @@ pub(crate) struct Bsp { /// Our two fan controllers: east for 0/1 and west for 1/2 fctrl_east: Max31790State, fctrl_west: Max31790State, + fans: &'static mut [Fan; NUM_FANS], seq: Sequencer, i2c_task: TaskId, @@ -119,21 +122,51 @@ impl crate::control::BspInterface for Bsp { fn read_fan_presence( &mut self, - ) -> Result< - impl Iterator, - crate::SeqError, - > { - todo!(); - Ok(core::iter::empty()) + ) -> Result, crate::SeqError> { + // Get presence bits from the sequencer + let iter = self + .seq + .fan_module_presence()? + // First, get an iterator over all of the presence bools. + .0 + .into_iter() + // Since each bool represets the state of two fans at a time, we + // chunk up the fans in pairs, and dupe the presence bit onto each + // one, THEN flatten it back into a single linear iterator. + .zip(self.fans.chunks_exact_mut(2)) + .flat_map(|(p, c)| core::iter::repeat(p).zip(c.iter_mut())) + // Finally, for each fan, see if it is newly here/gone, and report + // that with its "fan" ID, which is just the order that we define + // our fans. We don't change the order, so it's okay to enumerate + // "late" instead of earlier in the chain. + .enumerate() + .map(|(fan_id, (present, c))| { + let fan_id = fan_id as u8; + let was = c.last_reading.is_some(); + let new = was ^ present; + + if present { + // If this fan *wasn't* here before, and *is* now, we + // haven't polled it yet. Mark it as present but invalid. + if c.last_reading.is_none() { + c.last_reading = Some(FanReading::Invalid); + } + FanPresence::Present { fan_id, new } + } else { + c.last_reading = None; + FanPresence::NotPresent { fan_id, new } + } + }); + Ok(iter) } - fn read_fan_rpms(&mut self) -> impl Iterator { - // TODO: This is wrong, I think - // self.fctrl_east - // .read_fan_rpms(self.fans) - // .chain(self.fctrl_west.read_fan_rpms(self.fans)) - todo!(); - core::iter::empty() + fn read_fan_rpms(&mut self) -> impl Iterator { + // Load bearing assumption: the first 4 fans are the EAST fans, and the + // last 4 fans are the WEST fans. + let (east, west) = self.fans.split_at_mut(4); + self.fctrl_east + .read_fan_rpms(east) + .chain(self.fctrl_west.read_fan_rpms(west)) } fn read_misc_sensors( @@ -207,23 +240,43 @@ impl crate::control::BspInterface for Bsp { } fn all_inputs_present(&self) -> bool { - // self.inputs.iter().all(InputChannel::has_reading) - // && self - // .dynamic_inputs - // .iter() - // .all(DynamicInputChannel::has_reading) - todo!() + self.inputs.iter().all(InputChannel::has_reading) + && self + .dynamic_inputs + .iter() + .filter_map(|di| di.last_reading) + .all(|lr| matches!(lr, TemperatureReading::Valid(..))) } fn all_present_inputs_status( &self, - ) -> impl Iterator> { - todo!(); - core::iter::empty() + ) -> impl Iterator> { + let inputs = self.inputs.iter().filter_map(|input| input.status()); + let dynamic_inputs = self.dynamic_inputs.iter().filter_map(|di| { + // If the input isn't present, skip + let last = di.last_reading.as_ref()?; + + // If the last reading isn't valid, skip + let TemperatureReading::Valid(reading) = last else { + return None; + }; + + Some(InputStatus { + id: di.sensor_id, + reading, + model: &di.model, + }) + }); + + inputs.chain(dynamic_inputs) } fn reset_all_values(&mut self) { - todo!() + self.inputs.iter_mut().for_each(|i| i.reset_value()); + self.dynamic_inputs + .iter_mut() + .filter_map(|di| di.last_reading.as_mut()) + .for_each(|r| *r = TemperatureReading::Inactive) } fn set_all_watchdogs( @@ -259,92 +312,43 @@ impl crate::control::BspInterface for Bsp { &mut self, duty: userlib::units::PWMDuty, ) -> Result<(), ThermalError> { - todo!() + let mut any_err = false; + let mut set_all = |fctrl: &mut Max31790, fans: &mut [Fan]| { + for fan in fans.iter_mut() { + let val = if fan.last_reading.is_none() { + userlib::units::PWMDuty(0) + } else { + duty + }; + any_err |= fctrl.set_pwm(fan.bsp_data, val).is_err(); + } + }; + + // Load bearing assumption: the first 4 fans are the EAST fans, and the + // last 4 fans are the WEST fans. + let (east, west) = self.fans.split_at_mut(4); + + let mut init_err = false; + if let Ok(fctrl) = self.fctrl_east.try_initialize() { + set_all(fctrl, east); + } else { + init_err = true; + } + if let Ok(fctrl) = self.fctrl_west.try_initialize() { + set_all(fctrl, west); + } else { + init_err = true; + } + + if any_err | init_err { + Err(ThermalError::DeviceError) + } else { + Ok(()) + } } } impl Bsp { - // pub fn fan_control( - // &mut self, - // fan: crate::Fan, - // ) -> Result, ControllerInitError> { - // // - // // Fan module 0/1 are on the east max31790; fan module 2/3 are on west - // // max31790. Each fan module has two fans which are not mapped in a - // // straightforward way. Additionally, our MAX31790 code has zero-indexed - // // fan indices, but the part's datasheet and schematic symbol are - // // one-indexed. Here is the mapping of the system level index to - // // controller and fan index: - // // - // // System Index Controller Fan MAX31790 Fan (Datasheet) - // // 0 East ESE 2 (3) - // // 1 East ENE 3 (4) - // // 2 East SE 0 (1) - // // 3 East NE 1 (2) - // // 4 West SW 2 (3) - // // 5 West NW 3 (4) - // // 6 West WSW 0 (1) - // // 7 West WNW 1 (2) - // // - - // // The supplied `fan` is the System Index. From that we can map to a fan - // // and controller. - // let (fan_logical, controller) = if fan.0 < 4 { - // (fan.0, &mut self.fctrl_east) - // } else if fan.0 < 8 { - // (fan.0 - 4, &mut self.fctrl_west) - // } else { - // panic!(); - // }; - // // These are hooked up weird on the board; handle that here - // let fan_physical = match fan_logical { - // 0 => 2, - // 1 => 3, - // 2 => 0, - // 3 => 1, - // _ => panic!(), - // }; - // Ok(FanControl::Max31790( - // controller.try_initialize()?, - // fan_physical.try_into().unwrap_lite(), - // )) - // } - - // pub fn for_each_fctrl( - // &mut self, - // mut fctrl: impl FnMut(FanControl<'_>), - // ) -> Result<(), ControllerInitError> { - // let mut last_err = Ok(()); - // // Run the function on each fan control chip - // match self.fan_control(0.into()) { - // Ok(c) => fctrl(c), - // Err(e) => last_err = Err(e), - // } - // match self.fan_control(4.into()) { - // Ok(c) => fctrl(c), - // Err(e) => last_err = Err(e), - // } - // last_err - // } - - // pub fn get_fan_presence(&self) -> Result, SeqError> { - // let presence = self.seq.fan_module_presence()?; - // let mut next = Fans::new(); - // for (i, present) in presence.0.iter().enumerate() { - // // two fans per module - // let idx = i * 2; - // if *present { - // next[idx] = Some(sensors::MAX31790_SPEED_SENSORS[idx]); - // next[idx + 1] = Some(sensors::MAX31790_SPEED_SENSORS[idx + 1]); - // } - // } - // Ok(next) - // } - - // pub fn fan_sensor_id(&self, i: usize) -> SensorId { - // sensors::MAX31790_SPEED_SENSORS[i] - // } - pub fn new(i2c_task: TaskId) -> Self { // Handle for the sequencer task, which we check for power state and // fan presence @@ -357,6 +361,13 @@ impl Bsp { [InputChannel; NUM_TEMPERATURE_INPUTS], > = static_cell::ClaimOnceCell::new(INPUTS); + static FANS_ONCE: static_cell::ClaimOnceCell<[Fan; NUM_FANS]> = + static_cell::ClaimOnceCell::new(FANS); + + static DYN_INS_ONCE: static_cell::ClaimOnceCell< + [DynamicInputChannel; NUM_DYNAMIC_TEMPERATURE_INPUTS], + > = static_cell::ClaimOnceCell::new(DYNAMIC_INPUTS); + Self { seq, fctrl_east, @@ -373,13 +384,12 @@ impl Bsp { }, inputs: INPUTS_ONCE.claim(), - dynamic_inputs: todo!(), - // dynamic_inputs: - // &drv_transceivers_api::TRANSCEIVER_TEMPERATURE_SENSORS, + dynamic_inputs: DYN_INS_ONCE.claim(), // We monitor and log all of the air temperatures misc_sensors: &MISC_SENSORS, i2c_task, + fans: FANS_ONCE.claim(), } } } @@ -426,6 +436,22 @@ const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ )), ]; +const fn make_dynamic() -> [DynamicInputChannel; NUM_DYNAMIC_TEMPERATURE_INPUTS] +{ + const INIT: DynamicInputChannel = + DynamicInputChannel::new(SensorId::new(0)); + let mut out = [INIT; NUM_DYNAMIC_TEMPERATURE_INPUTS]; + let mut idx = 0; + while idx < NUM_DYNAMIC_TEMPERATURE_INPUTS { + let sensor = drv_transceivers_api::TRANSCEIVER_TEMPERATURE_SENSORS[idx]; + out[idx] = DynamicInputChannel::new(sensor); + idx += 1; + } + out +} +const DYNAMIC_INPUTS: [DynamicInputChannel; NUM_DYNAMIC_TEMPERATURE_INPUTS] = + make_dynamic(); + const MISC_SENSORS: [TemperatureSensor; NUM_TEMPERATURE_SENSORS] = [ TemperatureSensor::new( Device::Tmp117, @@ -463,3 +489,57 @@ const MISC_SENSORS: [TemperatureSensor; NUM_TEMPERATURE_SENSORS] = [ sensors::TMP117_SOUTHWEST_TEMPERATURE_SENSOR, ), ]; + +// Fan module 0/1 are on the east max31790; fan module 2/3 are on west +// max31790. Each fan module has two fans which are not mapped in a +// straightforward way. Additionally, our MAX31790 code has zero-indexed +// fan indices, but the part's datasheet and schematic symbol are +// one-indexed. Here is the mapping of the system level index to +// controller and fan index: +// +// System Index Controller Fan MAX31790 Fan (Datasheet) +// 0 East ESE 2 (3) +// 1 East ENE 3 (4) +// 2 East SE 0 (1) +// 3 East NE 1 (2) +// 4 West SW 2 (3) +// 5 West NW 3 (4) +// 6 West WSW 0 (1) +// 7 West WNW 1 (2) +type Fan = crate::control::Fan; +const FANS: [Fan; NUM_FANS] = [ + // EAST FANS + Fan::new( + sensors::MAX31790_SPEED_SENSORS[0], + drv_i2c_devices::max31790::Fan::new_const(2), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[1], + drv_i2c_devices::max31790::Fan::new_const(3), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[2], + drv_i2c_devices::max31790::Fan::new_const(0), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[3], + drv_i2c_devices::max31790::Fan::new_const(1), + ), + // WEST FANS + Fan::new( + sensors::MAX31790_SPEED_SENSORS[4], + drv_i2c_devices::max31790::Fan::new_const(2), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[5], + drv_i2c_devices::max31790::Fan::new_const(3), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[6], + drv_i2c_devices::max31790::Fan::new_const(0), + ), + Fan::new( + sensors::MAX31790_SPEED_SENSORS[7], + drv_i2c_devices::max31790::Fan::new_const(1), + ), +]; diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 014dae34e2..1be70e2380 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -24,12 +24,16 @@ use userlib::{ #[allow(dead_code)] // Not all bsps have fans! pub struct Fan { pub rpm_sensor_id: SensorId, - // TODO(AJM): Distinguish between "have never heard from" and "have heard - // from but has since gone missing"? Like TemperatureReading? - pub last_reading: Option, + pub last_reading: Option, pub bsp_data: D, } +pub enum FanReading { + // Valid(Rpm), + Valid, + Invalid, +} + #[allow(dead_code)] // Not all bsps have fans! impl Fan { pub const fn new(rpm_sensor_id: SensorId, bsp_data: D) -> Self { @@ -55,7 +59,7 @@ pub enum FanPresence { } #[allow(dead_code)] // Not all bsps have fans! -pub enum FanReading { +pub enum FanStatus { PresentSuccess { rpm: Rpm, sensor_id: SensorId, @@ -158,6 +162,22 @@ pub(crate) struct DynamicInputChannel { pub last_reading: Option, } +impl DynamicInputChannel { + #[allow(dead_code)] + pub(crate) const fn new(sensor_id: SensorId) -> Self { + Self { + sensor_id, + model: ThermalProperties { + target_temperature: Celsius(0.), + critical_temperature: Celsius(0.), + power_down_temperature: Celsius(0.), + temperature_slew_deg_per_sec: 0., + }, + last_reading: None, + } + } +} + //////////////////////////////////////////////////////////////////////////////// #[derive(Copy, Clone)] @@ -674,15 +694,15 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { // Read fan data and log it to the sensors task for reading in self.bsp.read_fan_rpms() { match reading { - FanReading::PresentSuccess { rpm, sensor_id } => { + FanStatus::PresentSuccess { rpm, sensor_id } => { self.sensor_api.post_now(sensor_id, rpm.0.into()) } - FanReading::PresentError { error, sensor_id } => { + FanStatus::PresentError { error, sensor_id } => { ringbuf_entry!(Trace::FanReadFailed(sensor_id, error)); self.err_blackbox.push(sensor_id, error); self.sensor_api.nodata_now(sensor_id, error.into()); } - FanReading::NotPresent { sensor_id } => { + FanStatus::NotPresent { sensor_id } => { // Invalidate fan speed readings in the sensors task self.sensor_api.nodata_now( sensor_id, @@ -1188,7 +1208,7 @@ pub trait BspInterface { &mut self, ) -> Result, crate::SeqError>; - fn read_fan_rpms(&mut self) -> impl Iterator; + fn read_fan_rpms(&mut self) -> impl Iterator; fn read_misc_sensors( &self, From 908f0c6e8fca44745645afd551afa4dcebba9a88 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 18:42:55 +0200 Subject: [PATCH 23/33] Helper function for const init --- task/thermal/src/bsp/common/max31790.rs | 23 ++++++++++++++++++++ task/thermal/src/bsp/cosmo_ab.rs | 28 ++----------------------- task/thermal/src/bsp/gimlet_bcdef.rs | 28 ++----------------------- 3 files changed, 27 insertions(+), 52 deletions(-) diff --git a/task/thermal/src/bsp/common/max31790.rs b/task/thermal/src/bsp/common/max31790.rs index 6a414bff43..6362f51c5a 100644 --- a/task/thermal/src/bsp/common/max31790.rs +++ b/task/thermal/src/bsp/common/max31790.rs @@ -7,6 +7,7 @@ use drv_i2c_api::{I2cDevice, ResponseCode}; use drv_i2c_devices::max31790::Max31790; use ringbuf::ringbuf_entry; +use task_sensor_api::SensorId; use task_thermal_api::{SensorReadError, ThermalError}; use crate::{ @@ -137,3 +138,25 @@ impl From for SensorReadError { SensorReadError::I2cError(code) } } + +pub(crate) const fn make_consecutive_fans( + sensors: &'static [SensorId; N], +) -> [crate::control::Fan; N] { + const ONE: crate::control::Fan = + crate::control::Fan::new( + SensorId::new(0), + drv_i2c_devices::max31790::Fan::new_const(0), + ); + + let mut out = [ONE; N]; + let mut idx = 0; + while idx < N { + out[idx] = crate::control::Fan::new( + sensors[idx], + drv_i2c_devices::max31790::Fan::new_const(idx as u8), + ); + idx += 1; + } + + out +} diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 18c825952a..5ff6902e37 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -313,32 +313,8 @@ const T6_THERMALS: ThermalProperties = ThermalProperties { // Our "bonus data" is a u8 that represents the fan's index in the i2c register type Fan = crate::control::Fan; -const FANS: [Fan; NUM_FANS] = [ - Fan::new( - sensors::MAX31790_SPEED_SENSORS[0], - drv_i2c_devices::max31790::Fan::new_const(0), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[1], - drv_i2c_devices::max31790::Fan::new_const(1), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[2], - drv_i2c_devices::max31790::Fan::new_const(2), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[3], - drv_i2c_devices::max31790::Fan::new_const(3), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[4], - drv_i2c_devices::max31790::Fan::new_const(4), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[5], - drv_i2c_devices::max31790::Fan::new_const(5), - ), -]; +const FANS: [Fan; NUM_FANS] = + max31790::make_consecutive_fans(&sensors::MAX31790_SPEED_SENSORS); const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ InputChannel::new(&InputChannelMetadata::new( diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index 444270c1ef..0a015c1b86 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -368,32 +368,8 @@ const T6_THERMALS: ThermalProperties = ThermalProperties { // Our "bonus data" is a u8 that represents the fan's index in the i2c register type Fan = crate::control::Fan; -const FANS: [Fan; NUM_FANS] = [ - Fan::new( - sensors::MAX31790_SPEED_SENSORS[0], - drv_i2c_devices::max31790::Fan::new_const(0), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[1], - drv_i2c_devices::max31790::Fan::new_const(1), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[2], - drv_i2c_devices::max31790::Fan::new_const(2), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[3], - drv_i2c_devices::max31790::Fan::new_const(3), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[4], - drv_i2c_devices::max31790::Fan::new_const(4), - ), - Fan::new( - sensors::MAX31790_SPEED_SENSORS[5], - drv_i2c_devices::max31790::Fan::new_const(5), - ), -]; +const FANS: [Fan; NUM_FANS] = + max31790::make_consecutive_fans(&sensors::MAX31790_SPEED_SENSORS); const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ // The M.2 devices are polled first deliberately: they're only polled if From 265893630a414f859ac1b018193a8948adfc3cbc Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 18:49:44 +0200 Subject: [PATCH 24/33] Fix grapefruit --- task/thermal/src/bsp/common/emc2305.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/task/thermal/src/bsp/common/emc2305.rs b/task/thermal/src/bsp/common/emc2305.rs index d3cd79a37e..6f8d3f72df 100644 --- a/task/thermal/src/bsp/common/emc2305.rs +++ b/task/thermal/src/bsp/common/emc2305.rs @@ -11,7 +11,7 @@ use task_thermal_api::{SensorReadError, ThermalError}; use crate::{ __RINGBUF, Trace, - control::{Fan, FanStatus}, + control::{Fan, FanReading, FanStatus}, }; /// Tracks whether a Emc2305 fan controller has been initialized, and @@ -75,6 +75,11 @@ impl Emc2305State { // TODO: Maybe there's a way to make this a method on Fan that we can // call, kind of like InputStatus? fans.iter_mut().map(move |f| { + let sensor_id = f.rpm_sensor_id; + if f.last_reading.is_none() { + return FanStatus::NotPresent { sensor_id }; + } + // If initialization failed, then we short circuit to return that // original error, copied for each fan we're going to report. let fctrl = fctrl.as_mut().map_err(|e| *e); @@ -86,18 +91,12 @@ impl Emc2305State { }); match res { Ok(rpm) => { - f.last_reading = Some(rpm); - FanStatus::PresentSuccess { - rpm, - sensor_id: f.rpm_sensor_id, - } + f.last_reading = Some(FanReading::Valid); + FanStatus::PresentSuccess { rpm, sensor_id } } Err(error) => { - f.last_reading = None; - FanStatus::PresentError { - error, - sensor_id: f.rpm_sensor_id, - } + f.last_reading = Some(FanReading::Invalid); + FanStatus::PresentError { error, sensor_id } } } }) From c1c18f4e595d0f91f52dfbe91e50cf43a2efe2fc Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 18:57:31 +0200 Subject: [PATCH 25/33] More dead code annotations --- task/thermal/src/control.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 1be70e2380..474c4363e0 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -28,8 +28,8 @@ pub struct Fan { pub bsp_data: D, } +#[allow(dead_code)] // Not all bsps have fans! pub enum FanReading { - // Valid(Rpm), Valid, Invalid, } From d7650777f627e7f86ec8c369712fee2267c0340c Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 19:07:24 +0200 Subject: [PATCH 26/33] More dead code --- task/thermal/src/bsp/common/max31790.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/task/thermal/src/bsp/common/max31790.rs b/task/thermal/src/bsp/common/max31790.rs index 6362f51c5a..8d2b68abd5 100644 --- a/task/thermal/src/bsp/common/max31790.rs +++ b/task/thermal/src/bsp/common/max31790.rs @@ -139,6 +139,7 @@ impl From for SensorReadError { } } +#[allow(dead_code)] pub(crate) const fn make_consecutive_fans( sensors: &'static [SensorId; N], ) -> [crate::control::Fan; N] { From e06c5b0827b47c5e5af203414e553d6d89813044 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 27 Jul 2026 20:15:48 +0200 Subject: [PATCH 27/33] Track fan presence instead of a pseudo-status --- task/thermal/src/bsp/common/emc2305.rs | 39 ++++++++++++++++++------- task/thermal/src/bsp/common/max31790.rs | 17 ++++------- task/thermal/src/bsp/cosmo_ab.rs | 5 ++-- task/thermal/src/bsp/gimlet_bcdef.rs | 5 ++-- task/thermal/src/bsp/grapefruit.rs | 21 ++----------- task/thermal/src/bsp/sidecar_bcd.rs | 15 ++++------ task/thermal/src/control.rs | 10 ++----- 7 files changed, 51 insertions(+), 61 deletions(-) diff --git a/task/thermal/src/bsp/common/emc2305.rs b/task/thermal/src/bsp/common/emc2305.rs index 6f8d3f72df..bd1860052f 100644 --- a/task/thermal/src/bsp/common/emc2305.rs +++ b/task/thermal/src/bsp/common/emc2305.rs @@ -7,11 +7,12 @@ use drv_i2c_api::{I2cDevice, ResponseCode}; use drv_i2c_devices::emc2305::Emc2305; use ringbuf::ringbuf_entry; +use task_sensor_api::SensorId; use task_thermal_api::{SensorReadError, ThermalError}; use crate::{ __RINGBUF, Trace, - control::{Fan, FanReading, FanStatus}, + control::{Fan, FanStatus}, }; /// Tracks whether a Emc2305 fan controller has been initialized, and @@ -76,7 +77,7 @@ impl Emc2305State { // call, kind of like InputStatus? fans.iter_mut().map(move |f| { let sensor_id = f.rpm_sensor_id; - if f.last_reading.is_none() { + if !f.is_present { return FanStatus::NotPresent { sensor_id }; } @@ -90,14 +91,8 @@ impl Emc2305State { fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) }); match res { - Ok(rpm) => { - f.last_reading = Some(FanReading::Valid); - FanStatus::PresentSuccess { rpm, sensor_id } - } - Err(error) => { - f.last_reading = Some(FanReading::Invalid); - FanStatus::PresentError { error, sensor_id } - } + Ok(rpm) => FanStatus::PresentSuccess { rpm, sensor_id }, + Err(error) => FanStatus::PresentError { error, sensor_id }, } }) } @@ -130,3 +125,27 @@ impl From for SensorReadError { SensorReadError::I2cError(code) } } + +#[allow(dead_code)] +pub(crate) const fn make_consecutive_nonremovable_fans( + sensors: &'static [SensorId; N], +) -> [crate::control::Fan; N] { + const ONE: crate::control::Fan = + crate::control::Fan::new( + SensorId::new(0), + drv_i2c_devices::emc2305::Fan::new_const(0), + ); + + let mut out = [ONE; N]; + let mut idx = 0; + while idx < N { + out[idx] = crate::control::Fan::new( + sensors[idx], + drv_i2c_devices::emc2305::Fan::new_const(idx as u8), + ); + out[idx].is_present = true; + idx += 1; + } + + out +} diff --git a/task/thermal/src/bsp/common/max31790.rs b/task/thermal/src/bsp/common/max31790.rs index 8d2b68abd5..1e756126ba 100644 --- a/task/thermal/src/bsp/common/max31790.rs +++ b/task/thermal/src/bsp/common/max31790.rs @@ -14,7 +14,7 @@ use crate::{ // control::{ControllerInitError, retry_init}, __RINGBUF, Trace, - control::{Fan, FanReading, FanStatus}, + control::{Fan, FanStatus}, }; /// Tracks whether a MAX31790 fan controller has been initialized, and @@ -84,7 +84,7 @@ impl Max31790State { // call, kind of like InputStatus? fans.iter_mut().map(move |f| { let sensor_id = f.rpm_sensor_id; - if f.last_reading.is_none() { + if !f.is_present { return FanStatus::NotPresent { sensor_id }; } @@ -98,14 +98,8 @@ impl Max31790State { fc.fan_rpm(f.bsp_data).map_err(SensorReadError::I2cError) }); match res { - Ok(rpm) => { - f.last_reading = Some(FanReading::Valid); - FanStatus::PresentSuccess { rpm, sensor_id } - } - Err(error) => { - f.last_reading = Some(FanReading::Invalid); - FanStatus::PresentError { error, sensor_id } - } + Ok(rpm) => FanStatus::PresentSuccess { rpm, sensor_id }, + Err(error) => FanStatus::PresentError { error, sensor_id }, } }) } @@ -140,7 +134,7 @@ impl From for SensorReadError { } #[allow(dead_code)] -pub(crate) const fn make_consecutive_fans( +pub(crate) const fn make_consecutive_nonremovable_fans( sensors: &'static [SensorId; N], ) -> [crate::control::Fan; N] { const ONE: crate::control::Fan = @@ -156,6 +150,7 @@ pub(crate) const fn make_consecutive_fans( sensors[idx], drv_i2c_devices::max31790::Fan::new_const(idx as u8), ); + out[idx].is_present = true; idx += 1; } diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 5ff6902e37..ff2ae6fb2f 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -313,8 +313,9 @@ const T6_THERMALS: ThermalProperties = ThermalProperties { // Our "bonus data" is a u8 that represents the fan's index in the i2c register type Fan = crate::control::Fan; -const FANS: [Fan; NUM_FANS] = - max31790::make_consecutive_fans(&sensors::MAX31790_SPEED_SENSORS); +const FANS: [Fan; NUM_FANS] = max31790::make_consecutive_nonremovable_fans( + &sensors::MAX31790_SPEED_SENSORS, +); const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ InputChannel::new(&InputChannelMetadata::new( diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index 0a015c1b86..157044e9a8 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -368,8 +368,9 @@ const T6_THERMALS: ThermalProperties = ThermalProperties { // Our "bonus data" is a u8 that represents the fan's index in the i2c register type Fan = crate::control::Fan; -const FANS: [Fan; NUM_FANS] = - max31790::make_consecutive_fans(&sensors::MAX31790_SPEED_SENSORS); +const FANS: [Fan; NUM_FANS] = max31790::make_consecutive_nonremovable_fans( + &sensors::MAX31790_SPEED_SENSORS, +); const INPUTS: [InputChannel; NUM_TEMPERATURE_INPUTS] = [ // The M.2 devices are polled first deliberately: they're only polled if diff --git a/task/thermal/src/bsp/grapefruit.rs b/task/thermal/src/bsp/grapefruit.rs index 32d9f9bdcd..ff3560b4ac 100644 --- a/task/thermal/src/bsp/grapefruit.rs +++ b/task/thermal/src/bsp/grapefruit.rs @@ -222,24 +222,9 @@ impl Bsp { } type Fan = crate::control::Fan; -const FANS: [Fan; NUM_FANS] = [ - Fan::new( - sensors::EMC2305_SPEED_SENSORS[0], - drv_i2c_devices::emc2305::Fan::new_const(0), - ), - Fan::new( - sensors::EMC2305_SPEED_SENSORS[1], - drv_i2c_devices::emc2305::Fan::new_const(1), - ), - Fan::new( - sensors::EMC2305_SPEED_SENSORS[2], - drv_i2c_devices::emc2305::Fan::new_const(2), - ), - Fan::new( - sensors::EMC2305_SPEED_SENSORS[3], - drv_i2c_devices::emc2305::Fan::new_const(3), - ), -]; +const FANS: [Fan; NUM_FANS] = emc2305::make_consecutive_nonremovable_fans( + &sensors::EMC2305_SPEED_SENSORS, +); // This is completely made up! const LM75_THERMALS: ThermalProperties = ThermalProperties { diff --git a/task/thermal/src/bsp/sidecar_bcd.rs b/task/thermal/src/bsp/sidecar_bcd.rs index c965592a2c..71bfb1faf2 100644 --- a/task/thermal/src/bsp/sidecar_bcd.rs +++ b/task/thermal/src/bsp/sidecar_bcd.rs @@ -5,8 +5,8 @@ //! BSP for Sidecar use crate::control::{ - ChannelType, FanPresence, FanReading, InputStatus, PidConfig, - TemperatureReading, TimestampedTemperatureReading, + ChannelType, FanPresence, InputStatus, PidConfig, TemperatureReading, + TimestampedTemperatureReading, }; use crate::control::{DynamicInputChannel, FanStatus}; use drv_i2c_devices::max31790::Max31790; @@ -142,18 +142,13 @@ impl crate::control::BspInterface for Bsp { .enumerate() .map(|(fan_id, (present, c))| { let fan_id = fan_id as u8; - let was = c.last_reading.is_some(); + let was = c.is_present; let new = was ^ present; + c.is_present = new; if present { - // If this fan *wasn't* here before, and *is* now, we - // haven't polled it yet. Mark it as present but invalid. - if c.last_reading.is_none() { - c.last_reading = Some(FanReading::Invalid); - } FanPresence::Present { fan_id, new } } else { - c.last_reading = None; FanPresence::NotPresent { fan_id, new } } }); @@ -315,7 +310,7 @@ impl crate::control::BspInterface for Bsp { let mut any_err = false; let mut set_all = |fctrl: &mut Max31790, fans: &mut [Fan]| { for fan in fans.iter_mut() { - let val = if fan.last_reading.is_none() { + let val = if !fan.is_present { userlib::units::PWMDuty(0) } else { duty diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 474c4363e0..39c4a6ca8d 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -24,22 +24,16 @@ use userlib::{ #[allow(dead_code)] // Not all bsps have fans! pub struct Fan { pub rpm_sensor_id: SensorId, - pub last_reading: Option, + pub is_present: bool, pub bsp_data: D, } -#[allow(dead_code)] // Not all bsps have fans! -pub enum FanReading { - Valid, - Invalid, -} - #[allow(dead_code)] // Not all bsps have fans! impl Fan { pub const fn new(rpm_sensor_id: SensorId, bsp_data: D) -> Self { Self { rpm_sensor_id, - last_reading: None, + is_present: false, bsp_data, } } From 35ccf1aea3655753475c313fafe723fd99446f56 Mon Sep 17 00:00:00 2001 From: James Munns Date: Tue, 28 Jul 2026 00:03:16 +0200 Subject: [PATCH 28/33] Make Input states *really* explicit. I kept getting these confused, and making it just very very verbose is I think the best way to make this clear. --- task/thermal/src/bsp/common/i2c_temp_input.rs | 41 +++++--- task/thermal/src/bsp/cosmo_ab.rs | 11 +-- task/thermal/src/bsp/gimlet_bcdef.rs | 11 +-- task/thermal/src/bsp/grapefruit.rs | 11 +-- task/thermal/src/bsp/minibar.rs | 2 +- task/thermal/src/bsp/sidecar_bcd.rs | 94 ++++++++++--------- task/thermal/src/control.rs | 64 ++++++++++--- 7 files changed, 144 insertions(+), 90 deletions(-) diff --git a/task/thermal/src/bsp/common/i2c_temp_input.rs b/task/thermal/src/bsp/common/i2c_temp_input.rs index 6305c9df5f..94a1e10211 100644 --- a/task/thermal/src/bsp/common/i2c_temp_input.rs +++ b/task/thermal/src/bsp/common/i2c_temp_input.rs @@ -124,19 +124,31 @@ impl InputChannel { pub const fn new(metadata: &'static InputChannelMetadata) -> Self { Self { metadata, - last_reading: TemperatureReading::Inactive, + last_reading: TemperatureReading::NotYetQueried, } } - pub fn has_reading(&self) -> bool { - matches!(self.last_reading, TemperatureReading::Valid(..)) + pub fn has_been_queried(&self) -> bool { + match self.last_reading { + // If we haven't queried it, then no! + TemperatureReading::NotYetQueried => false, + + // If we have queried it, and it is not relevant in the current + // state, or removable and disconnected, or we've gotten data for + // it at least once, it has been queried! + TemperatureReading::Unpowered => true, + TemperatureReading::Disconnected => true, + TemperatureReading::ValidAtLeastOnce(..) => true, + } } /// Get current stored status. /// /// Returns None if we do not have a reading stored. pub fn status(&self) -> Option> { - let TemperatureReading::Valid(ref reading) = self.last_reading else { + let TemperatureReading::ValidAtLeastOnce(ref reading) = + self.last_reading + else { return None; }; Some(InputStatus { @@ -146,8 +158,12 @@ impl InputChannel { }) } - pub fn reset_value(&mut self) { - self.last_reading = TemperatureReading::Inactive; + pub fn reset_value(&mut self, mode: PowerBitmask) { + if mode.intersects(self.metadata.power_mode_mask) { + self.last_reading = TemperatureReading::Unpowered; + } else { + self.last_reading = TemperatureReading::NotYetQueried; + } } pub fn do_reading( @@ -159,18 +175,19 @@ impl InputChannel { // If we're not supposed to be on, don't even ask. if !mode.intersects(self.metadata.power_mode_mask) { - self.last_reading = TemperatureReading::Inactive; + self.last_reading = TemperatureReading::Unpowered; return InputReadingOutcome::Unpowered { id }; } match self.metadata.sensor.read_temp(*i2c_task) { Ok(value) => { let now = sys_get_timer().now; - self.last_reading = - TemperatureReading::Valid(TimestampedTemperatureReading { + self.last_reading = TemperatureReading::ValidAtLeastOnce( + TimestampedTemperatureReading { time_ms: now, value, - }); + }, + ); InputReadingOutcome::Success { id, now, value } } Err(e) => { @@ -187,10 +204,10 @@ impl InputChannel { let se = SensorError::from(NoData::from(e)); match (self.metadata.ty, se) { (ChannelType::Removable, SensorError::NotPresent) => { - self.last_reading = TemperatureReading::Inactive; + self.last_reading = TemperatureReading::Disconnected; } (ChannelType::RemovableAndErrorProne, _) => { - self.last_reading = TemperatureReading::Inactive; + self.last_reading = TemperatureReading::Disconnected; } _ => { // In all other cases, just leave whatever the last diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index ff2ae6fb2f..42eff8d5d7 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -182,15 +182,11 @@ impl crate::control::BspInterface for Bsp { Err(ThermalError::InvalidIndex) } - fn all_inputs_present(&self) -> bool { - self.inputs.iter().all(InputChannel::has_reading) + fn all_inputs_queried(&self) -> bool { + self.inputs.iter().all(InputChannel::has_been_queried) // && self.dynamic_inputs... } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // All inputs MUST have a previous reading or this will panic, though the - // readings may be allowed to be Missing if the model allows it. Dynamic - // inputs that are not present will be skipped. fn all_present_inputs_status( &self, ) -> impl Iterator> { @@ -199,7 +195,8 @@ impl crate::control::BspInterface for Bsp { } fn reset_all_values(&mut self) { - self.inputs.iter_mut().for_each(|i| i.reset_value()); + let power = self.power_mode(); + self.inputs.iter_mut().for_each(|i| i.reset_value(power)); // self.dynamic_inputs... } diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index 157044e9a8..f14eca2cb2 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -227,15 +227,11 @@ impl crate::control::BspInterface for Bsp { Err(ThermalError::InvalidIndex) } - fn all_inputs_present(&self) -> bool { - self.inputs.iter().all(InputChannel::has_reading) + fn all_inputs_queried(&self) -> bool { + self.inputs.iter().all(InputChannel::has_been_queried) // && self.dynamic_inputs... } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // All inputs MUST have a previous reading or this will panic, though the - // readings may be allowed to be Missing if the model allows it. Dynamic - // inputs that are not present will be skipped. fn all_present_inputs_status( &self, ) -> impl Iterator> { @@ -244,7 +240,8 @@ impl crate::control::BspInterface for Bsp { } fn reset_all_values(&mut self) { - self.inputs.iter_mut().for_each(|i| i.reset_value()); + let power = self.power_mode(); + self.inputs.iter_mut().for_each(|i| i.reset_value(power)); // self.dynamic_inputs... } diff --git a/task/thermal/src/bsp/grapefruit.rs b/task/thermal/src/bsp/grapefruit.rs index ff3560b4ac..459bb2b094 100644 --- a/task/thermal/src/bsp/grapefruit.rs +++ b/task/thermal/src/bsp/grapefruit.rs @@ -146,15 +146,11 @@ impl crate::control::BspInterface for Bsp { Err(ThermalError::InvalidIndex) } - fn all_inputs_present(&self) -> bool { - self.inputs.iter().all(InputChannel::has_reading) + fn all_inputs_queried(&self) -> bool { + self.inputs.iter().all(InputChannel::has_been_queried) // && self.dynamic_inputs... } - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // All inputs MUST have a previous reading or this will panic, though the - // readings may be allowed to be Missing if the model allows it. Dynamic - // inputs that are not present will be skipped. fn all_present_inputs_status( &self, ) -> impl Iterator> { @@ -163,7 +159,8 @@ impl crate::control::BspInterface for Bsp { } fn reset_all_values(&mut self) { - self.inputs.iter_mut().for_each(|i| i.reset_value()); + let power = self.power_mode(); + self.inputs.iter_mut().for_each(|i| i.reset_value(power)); // self.dynamic_inputs... } diff --git a/task/thermal/src/bsp/minibar.rs b/task/thermal/src/bsp/minibar.rs index ae00c1684a..7c57f082bb 100644 --- a/task/thermal/src/bsp/minibar.rs +++ b/task/thermal/src/bsp/minibar.rs @@ -107,7 +107,7 @@ impl crate::control::BspInterface for Bsp { Err(ThermalError::InvalidIndex) } - fn all_inputs_present(&self) -> bool { + fn all_inputs_queried(&self) -> bool { true } diff --git a/task/thermal/src/bsp/sidecar_bcd.rs b/task/thermal/src/bsp/sidecar_bcd.rs index 71bfb1faf2..609e61806e 100644 --- a/task/thermal/src/bsp/sidecar_bcd.rs +++ b/task/thermal/src/bsp/sidecar_bcd.rs @@ -5,8 +5,8 @@ //! BSP for Sidecar use crate::control::{ - ChannelType, FanPresence, InputStatus, PidConfig, TemperatureReading, - TimestampedTemperatureReading, + ChannelType, DynamicTemperatureReading, FanPresence, InputStatus, + PidConfig, TimestampedTemperatureReading, }; use crate::control::{DynamicInputChannel, FanStatus}; use drv_i2c_devices::max31790::Max31790; @@ -188,21 +188,23 @@ impl crate::control::BspInterface for Bsp { &mut self, sensor_api: &task_sensor_api::Sensor, ) { - self.dynamic_inputs - .iter_mut() - .filter(|di| di.last_reading.is_some()) - .for_each(|di| { - let res = sensor_api.get_reading(di.sensor_id).map(|r| { - TemperatureReading::Valid(TimestampedTemperatureReading { + for di in self.dynamic_inputs.iter_mut() { + // If the input is disabled, don't attempt to read. + if let DynamicTemperatureReading::Disabled = di.last_reading { + continue; + } + + // If there is a valid reading, update, otherwise leave in the + // current state (either unqueried or the last valid value) + if let Ok(r) = sensor_api.get_reading(di.sensor_id) { + di.last_reading = DynamicTemperatureReading::ValidAtLeastOnce( + TimestampedTemperatureReading { time_ms: r.timestamp, value: Celsius(r.value), - }) - }); - di.last_reading = Some(match res { - Ok(r) => r, - Err(_) => TemperatureReading::Inactive, - }); - }); + }, + ); + } + } } fn update_dynamic_input( @@ -213,13 +215,18 @@ impl crate::control::BspInterface for Bsp { let Some(di) = self.dynamic_inputs.get_mut(index) else { return Err(ThermalError::InvalidIndex); }; - if di.last_reading.is_some() { - // TODO: I think the old code just ignored this? - return Ok(false); + match di.last_reading { + DynamicTemperatureReading::Disabled => { + di.model = model; + di.last_reading = DynamicTemperatureReading::NotYetQueried; + Ok(true) + } + DynamicTemperatureReading::NotYetQueried + | DynamicTemperatureReading::ValidAtLeastOnce(..) => { + // TODO: I think the old code just ignored this? + Ok(false) + } } - di.model = model; - di.last_reading = Some(TemperatureReading::Inactive); - Ok(true) } fn remove_dynamic_input( @@ -230,17 +237,16 @@ impl crate::control::BspInterface for Bsp { return Err(ThermalError::InvalidIndex); }; // TODO: do we return an err if this was already removed? Check old code - di.last_reading = None; + di.last_reading = DynamicTemperatureReading::Disabled; Ok(di.sensor_id) } - fn all_inputs_present(&self) -> bool { - self.inputs.iter().all(InputChannel::has_reading) + fn all_inputs_queried(&self) -> bool { + self.inputs.iter().all(InputChannel::has_been_queried) && self .dynamic_inputs .iter() - .filter_map(|di| di.last_reading) - .all(|lr| matches!(lr, TemperatureReading::Valid(..))) + .all(DynamicInputChannel::has_been_queried) } fn all_present_inputs_status( @@ -248,30 +254,34 @@ impl crate::control::BspInterface for Bsp { ) -> impl Iterator> { let inputs = self.inputs.iter().filter_map(|input| input.status()); let dynamic_inputs = self.dynamic_inputs.iter().filter_map(|di| { - // If the input isn't present, skip - let last = di.last_reading.as_ref()?; - - // If the last reading isn't valid, skip - let TemperatureReading::Valid(reading) = last else { - return None; - }; - - Some(InputStatus { - id: di.sensor_id, - reading, - model: &di.model, - }) + match &di.last_reading { + DynamicTemperatureReading::Disabled => None, + DynamicTemperatureReading::NotYetQueried => None, + DynamicTemperatureReading::ValidAtLeastOnce(reading) => { + Some(InputStatus { + id: di.sensor_id, + reading, + model: &di.model, + }) + } + } }); inputs.chain(dynamic_inputs) } fn reset_all_values(&mut self) { - self.inputs.iter_mut().for_each(|i| i.reset_value()); + let mode = self.power_mode(); + self.inputs.iter_mut().for_each(|i| i.reset_value(mode)); self.dynamic_inputs .iter_mut() - .filter_map(|di| di.last_reading.as_mut()) - .for_each(|r| *r = TemperatureReading::Inactive) + .for_each(|di| match di.last_reading { + DynamicTemperatureReading::Disabled => {} + DynamicTemperatureReading::NotYetQueried => {} + DynamicTemperatureReading::ValidAtLeastOnce(..) => { + di.last_reading = DynamicTemperatureReading::NotYetQueried; + } + }); } fn set_all_watchdogs( diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 39c4a6ca8d..35283e57c2 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -153,11 +153,28 @@ pub struct InputStatus<'a> { pub(crate) struct DynamicInputChannel { pub sensor_id: SensorId, pub model: ThermalProperties, - pub last_reading: Option, + pub last_reading: DynamicTemperatureReading, } +/// Represents the state of a temperature sensor, which either has a valid +/// reading or is marked as inactive (due to power state or being missing) +#[derive(Copy, Clone, Debug)] +#[allow(dead_code)] // Not all bsps have inputs! +pub enum DynamicTemperatureReading { + /// Device has not been enabled + Disabled, + + /// The device is powered in the current mode, but has not yet been + /// queried successfully + NotYetQueried, + + /// This device has been queried successfully at least once, and this + /// contains the most recent valid reply + ValidAtLeastOnce(TimestampedTemperatureReading), +} + +#[allow(dead_code)] impl DynamicInputChannel { - #[allow(dead_code)] pub(crate) const fn new(sensor_id: SensorId) -> Self { Self { sensor_id, @@ -167,7 +184,19 @@ impl DynamicInputChannel { power_down_temperature: Celsius(0.), temperature_slew_deg_per_sec: 0., }, - last_reading: None, + last_reading: DynamicTemperatureReading::Disabled, + } + } + + pub(crate) fn has_been_queried(&self) -> bool { + match self.last_reading { + // Not queried? No! + DynamicTemperatureReading::NotYetQueried => false, + + // Either the input is disabled (so we have done all the querying + // necessary), or it has been valid in the past. + DynamicTemperatureReading::Disabled => true, + DynamicTemperatureReading::ValidAtLeastOnce(..) => true, } } } @@ -262,12 +291,19 @@ pub(crate) struct ThermalControl<'a, B: BspInterface> { #[derive(Copy, Clone, Debug)] #[allow(dead_code)] // Not all bsps have inputs! pub enum TemperatureReading { - /// Normal reading, timestamped using monotonic system time - Valid(TimestampedTemperatureReading), + /// Device is not powered, and has not been read + Unpowered, + + /// The device is powered in the current mode, but has not yet been + /// queried successfully + NotYetQueried, + + /// The device is removable, and has been removed + Disconnected, - /// This sensor is not used in the current power state, or has not been - /// read since changing operational state of the device - Inactive, + /// This device has been queried successfully at least once, and this + /// contains the most recent valid reply + ValidAtLeastOnce(TimestampedTemperatureReading), } /// Represents a temperature reading at the time at which it was taken @@ -802,15 +838,15 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { let mut any_power_down = None; let mut any_critical = None; let mut worst_margin = f32::MAX; - let all_inputs_present = self.bsp.all_inputs_present(); + let all_inputs_queried = self.bsp.all_inputs_queried(); - // TODO(AJM): Assert all present if !Boot + // TODO(AJM): Assert all queried if !Boot match self.state { ThermalControlState::Uncontrollable => { return Ok(ControlResult::PowerDown); } ThermalControlState::Boot => { - // We allow boot to be missing present items + // We allow boot to have not yet queried all items successfully } ThermalControlState::Running { .. } | ThermalControlState::Critical { .. } @@ -819,7 +855,7 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { // should not be possible by construction, as moving from // Invalid to Valid reading is "latching" unless we reset the // state. - assert!(all_inputs_present); + assert!(all_inputs_queried); } }; @@ -857,7 +893,7 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { // that's a bit more invasive of a change Ok(match &mut self.state { ThermalControlState::Boot => { - if all_inputs_present { + if all_inputs_queried { self.transition_to_running(worst_margin, now_ms) } else { ControlResult::Pwm(PWMDuty( @@ -1233,7 +1269,7 @@ pub trait BspInterface { index: usize, ) -> Result; - fn all_inputs_present(&self) -> bool; + fn all_inputs_queried(&self) -> bool; // Visit all temperature sensors, first the inputs, then the dynamic_inputs. // Inputs or Dynamic Inputs that are Invalid will be skipped. Dynamic Inputs From 9df220f656ad114196b60d21c025ecdde9231508 Mon Sep 17 00:00:00 2001 From: James Munns Date: Tue, 28 Jul 2026 00:29:03 +0200 Subject: [PATCH 29/33] Address a couple small review comments --- task/thermal/src/bsp/common/i2c_temp_input.rs | 2 +- task/thermal/src/bsp/sidecar_bcd.rs | 2 +- task/thermal/src/main.rs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/task/thermal/src/bsp/common/i2c_temp_input.rs b/task/thermal/src/bsp/common/i2c_temp_input.rs index 94a1e10211..73a3b28044 100644 --- a/task/thermal/src/bsp/common/i2c_temp_input.rs +++ b/task/thermal/src/bsp/common/i2c_temp_input.rs @@ -159,7 +159,7 @@ impl InputChannel { } pub fn reset_value(&mut self, mode: PowerBitmask) { - if mode.intersects(self.metadata.power_mode_mask) { + if !mode.intersects(self.metadata.power_mode_mask) { self.last_reading = TemperatureReading::Unpowered; } else { self.last_reading = TemperatureReading::NotYetQueried; diff --git a/task/thermal/src/bsp/sidecar_bcd.rs b/task/thermal/src/bsp/sidecar_bcd.rs index 609e61806e..317cc49ea8 100644 --- a/task/thermal/src/bsp/sidecar_bcd.rs +++ b/task/thermal/src/bsp/sidecar_bcd.rs @@ -144,7 +144,7 @@ impl crate::control::BspInterface for Bsp { let fan_id = fan_id as u8; let was = c.is_present; let new = was ^ present; - c.is_present = new; + c.is_present = present; if present { FanPresence::Present { fan_id, new } diff --git a/task/thermal/src/main.rs b/task/thermal/src/main.rs index 1ffabb177d..8a0ce15126 100644 --- a/task/thermal/src/main.rs +++ b/task/thermal/src/main.rs @@ -30,7 +30,6 @@ ), path = "bsp/sidecar_bcd.rs" )] -#[cfg_attr(any(target_board = "medusa-a"), path = "bsp/medusa_a.rs")] #[cfg_attr( any(target_board = "grapefruit-a", target_board = "grapefruit-b",), path = "bsp/grapefruit.rs" From 5c803a76e4db05edf8505ef1c5c64ce518b1751b Mon Sep 17 00:00:00 2001 From: James Munns Date: Tue, 28 Jul 2026 14:56:29 +0200 Subject: [PATCH 30/33] Make USE_CONTROLLER a trait const, add docs --- task/thermal/src/bsp/cosmo_ab.rs | 6 +- task/thermal/src/bsp/gimlet_bcdef.rs | 6 +- task/thermal/src/bsp/grapefruit.rs | 8 +-- task/thermal/src/bsp/minibar.rs | 11 +--- task/thermal/src/bsp/sidecar_bcd.rs | 6 +- task/thermal/src/control.rs | 89 +++++++++++++++++++++++++--- task/thermal/src/main.rs | 4 +- 7 files changed, 98 insertions(+), 32 deletions(-) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 42eff8d5d7..72ed3cb613 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -50,9 +50,6 @@ const NUM_TEMPERATURE_INPUTS: usize = sensors::NUM_SBTSI_TEMPERATURE_SENSORS // We've got 6 fans, driven from a single MAX31790 IC const NUM_FANS: usize = drv_i2c_devices::max31790::MAX_FANS as usize; -/// This controller is tuned and ready to go -pub const USE_CONTROLLER: bool = true; - pub(crate) struct Bsp { /// Controlled sensors inputs: &'static mut [InputChannel; NUM_TEMPERATURE_INPUTS], @@ -90,6 +87,9 @@ bitflags::bitflags! { } impl crate::control::BspInterface for Bsp { + /// This controller is tuned and ready to go + const USE_CONTROLLER: bool = true; + // Based on experimental tuning! const PID_CONFIG: PidConfig = PidConfig { zero: 35.0, diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index f14eca2cb2..009379f64e 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -61,9 +61,6 @@ pub const NUM_TEMPERATURE_INPUTS: usize = sensors::NUM_SBTSI_TEMPERATURE_SENSORS // We've got 6 fans, driven from a single MAX31790 IC const NUM_FANS: usize = drv_i2c_devices::max31790::MAX_FANS as usize; -/// This controller is tuned and ready to go -pub const USE_CONTROLLER: bool = true; - pub(crate) struct Bsp { /// Controlled sensors inputs: &'static mut [InputChannel; NUM_TEMPERATURE_INPUTS], @@ -108,6 +105,9 @@ bitflags::bitflags! { } impl crate::control::BspInterface for Bsp { + /// This controller is tuned and ready to go + const USE_CONTROLLER: bool = true; + // Based on experimental tuning! const PID_CONFIG: PidConfig = PidConfig { zero: 35.0, diff --git a/task/thermal/src/bsp/grapefruit.rs b/task/thermal/src/bsp/grapefruit.rs index 459bb2b094..2b025995d1 100644 --- a/task/thermal/src/bsp/grapefruit.rs +++ b/task/thermal/src/bsp/grapefruit.rs @@ -36,9 +36,6 @@ const NUM_TEMPERATURE_INPUTS: usize = 1; // Number of individual fans const NUM_FANS: usize = 4; -// Run the PID loop on startup -pub const USE_CONTROLLER: bool = true; - //////////////////////////////////////////////////////////////////////////////// bitflags::bitflags! { @@ -64,7 +61,10 @@ pub(crate) struct Bsp { } impl crate::control::BspInterface for Bsp { - // // TODO: this is all made up, copied from tuned Gimlet values + // Run the PID loop on startup + const USE_CONTROLLER: bool = true; + + // TODO: this is all made up, copied from tuned Gimlet values const PID_CONFIG: PidConfig = PidConfig { zero: 35.0, gain_p: 1.75, diff --git a/task/thermal/src/bsp/minibar.rs b/task/thermal/src/bsp/minibar.rs index 7c57f082bb..abea5e8529 100644 --- a/task/thermal/src/bsp/minibar.rs +++ b/task/thermal/src/bsp/minibar.rs @@ -11,14 +11,6 @@ use userlib::TaskId; include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); -//////////////////////////////////////////////////////////////////////////////// -// Constants! - -// Run the PID loop on startup -pub const USE_CONTROLLER: bool = false; - -//////////////////////////////////////////////////////////////////////////////// - bitflags::bitflags! { #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct PowerBitmask: u32 {} @@ -31,6 +23,9 @@ pub enum SeqError {} pub(crate) struct Bsp {} impl crate::control::BspInterface for Bsp { + // Run the PID loop on startup + const USE_CONTROLLER: bool = false; + const PID_CONFIG: PidConfig = PidConfig { zero: 0., gain_p: 0., diff --git a/task/thermal/src/bsp/sidecar_bcd.rs b/task/thermal/src/bsp/sidecar_bcd.rs index 317cc49ea8..6e8d00676b 100644 --- a/task/thermal/src/bsp/sidecar_bcd.rs +++ b/task/thermal/src/bsp/sidecar_bcd.rs @@ -54,9 +54,6 @@ pub const NUM_DYNAMIC_TEMPERATURE_INPUTS: usize = // Number of individual fans pub const NUM_FANS: usize = sensors::NUM_MAX31790_SPEED_SENSORS; -// Run the PID loop on startup -pub const USE_CONTROLLER: bool = true; - //////////////////////////////////////////////////////////////////////////////// bitflags::bitflags! { @@ -92,6 +89,9 @@ pub(crate) struct Bsp { } impl crate::control::BspInterface for Bsp { + // Run the PID loop on startup + const USE_CONTROLLER: bool = true; + // TODO: this is all made up, copied from tuned Gimlet values const PID_CONFIG: PidConfig = PidConfig { zero: 35.0, diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 35283e57c2..96031d7049 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -1227,65 +1227,136 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { } } +/// The platform/bsp specific interface contract for the thermal control loop +/// +/// ## Important terms: +/// +/// * Fans: The fans of a system, consisting of both the output controlled in +/// PWM duty cycle percentages, as well as a sensor monitoring the measured +/// RPM of the fans. +/// * Inputs: Temperature sensors that are actively polled by the thermal +/// control loop. Typically I2C based. This includes both permananently +/// attached sensors as well as removable sensors. Some Inputs may only be +/// active in a subset of power states. When present and active, the data from +/// these inputs are used as part of the PID control loop. The readings from +/// these sensors are also reported to the Sensors API. +/// * Misc Sensors: Temperature sensors that are actively polled by the thermal +/// control loop and reported to the Sensors API, but are not used as inputs +/// to the PID control loop. Misc sensors are active in all power states. +/// * Dynamic Inputs: Temperature sensors that are NOT actively polled by the +/// thermal control loop, from which readings are instead obtained by querying +/// the sensor API. These readings are used as inputs to the PID control loop. +/// By default, all Dynamic Inputs are not marked as present, and require an +/// external command (via IPC) to provide the necessary thermal model, and +/// inform the control loop that the sensors are active and should be queried. +/// * Watchdogs: Features of the external fan controllers that automatically +/// move the fans to their highest commanded speed when not communicated with +/// for a configured time duration. pub trait BspInterface { + /// Default [`PidConfig`] to use when in automatic control mode const PID_CONFIG: PidConfig; + /// Run the PID loop on startup + const USE_CONTROLLER: bool; + + /// Instruct the sequencer to power down the system fn power_down(&self) -> Result<(), crate::SeqError>; + /// The current power mode reported by the sequencer fn power_mode(&self) -> PowerBitmask; + /// An iterator representing the presence of each Fan of the system + /// + /// Typically reported by the sequencer, or always present in non-variable + /// configurations. fn read_fan_presence( &mut self, ) -> Result, crate::SeqError>; + /// Return an iterator of the current status of each fan. The iterator may + /// be lazy, meaning that failure to exhaust the iterator means that not all + /// fans will be actively read. fn read_fan_rpms(&mut self) -> impl Iterator; + /// Return an iterator of the current status of each misc sensor. The + /// iterator may be lazy, meaning that faulure to exhaust the iterator means + /// that not all sensors will be actively queried. fn read_misc_sensors( &self, ) -> impl Iterator)>; + /// Return an iterator of the current status of each input. The iterator + /// reports whether the input is powered, present, and whether the latest + /// query was successful. This updates the state of the sensor, which is + /// obtained by calling [`Self::all_present_inputs_status()`], which unlike + /// this API, retains the latest valid value, in case of transient read + /// failures. fn read_inputs( &mut self, mode: PowerBitmask, ) -> impl Iterator; - // TODO: This probably needs to exist, but for cosmo we have no dynamic - // inputs to read back. This should read from the api and store the state + /// Reads back all dynamic inputs that are currently marked as present. The + /// information from this querying can be obtained by calling + /// [`Self::all_present_inputs_status()`]. fn read_dynamic_inputs_back_from_sensor_api( &mut self, sensor_api: &task_sensor_api::Sensor, ); - // returns Ok(true) if this was a new input + /// Set the given dynamic input as present, and configured with the given + /// model. + /// + /// Returns `Ok(true)` when the input was not previously present. Returns + /// `Ok(false)` if the input was already present, and the new model was + /// ignored. Returns an error if the given index was invalid. fn update_dynamic_input( &mut self, index: usize, model: ThermalProperties, ) -> Result; - // sets last_reading to Some(Missing), returns sensor id + /// Set the given dynamic as not present. + /// + /// Returns Ok if the given index was valid, otherwise returns an error. + /// Does not indicate whether the input was previously present or not. fn remove_dynamic_input( &mut self, index: usize, ) -> Result; + /// Have all powered inputs (regular and dynamic) been queried? + /// + /// This is used to determine whether it is appropriate to leave the `Boot` + /// state. A sensor is considered to be queried if it is: + /// + /// * Unpowered + /// * Not present and marked as removable + /// * Has ever received a valid reply (even if not currently responding) fn all_inputs_queried(&self) -> bool; - // Visit all temperature sensors, first the inputs, then the dynamic_inputs. - // Inputs or Dynamic Inputs that are Invalid will be skipped. Dynamic Inputs - // that are not present will be skipped. + /// Visit all temperature sensors, first the inputs, then the dynamic + /// inputs. Only yields inputs that are powered, present (if removable), and + /// have ever received a valid reading. [`Self::all_inputs_queried()`] + /// should be used to determine if all inputs necessary to leave the Boot + /// state are present. fn all_present_inputs_status( &self, ) -> impl Iterator>; + /// For any input that has received a valid reading, mark it as not + /// received. fn reset_all_values(&mut self); + /// Set all fan controller watchdogs to the given duration fn set_all_watchdogs( &mut self, watchdog: I2cWatchdog, ) -> Result<(), ThermalError>; - // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, - // even if some fail. return the LAST error if any. + /// Attempt to set all fan outputs to the given duty cycle. If a fan is + /// removable and not present set the duty to 0. Attempts to set ALL duty + /// cycles, even if some setting operations fail. In case of any failures, + /// the most recent error is returned. fn set_all_fan_duty(&mut self, duty: PWMDuty) -> Result<(), ThermalError>; } diff --git a/task/thermal/src/main.rs b/task/thermal/src/main.rs index 8a0ce15126..faf9c2d56c 100644 --- a/task/thermal/src/main.rs +++ b/task/thermal/src/main.rs @@ -47,7 +47,7 @@ mod control; use crate::{ bsp::{Bsp, PowerBitmask, SeqError}, - control::ThermalControl, + control::{BspInterface, ThermalControl}, }; use drv_i2c_api::ResponseCode; use drv_i2c_devices::max31790::I2cWatchdog; @@ -405,7 +405,7 @@ fn main() -> ! { deadline, runtime: 0, }; - if bsp::USE_CONTROLLER { + if ::USE_CONTROLLER { server.set_mode_auto().unwrap_lite(); } else { server.set_mode_manual(PWMDuty(0)).unwrap_lite(); From a29774418c30de88913a00b6e42c5e5efa8309f8 Mon Sep 17 00:00:00 2001 From: James Munns Date: Wed, 29 Jul 2026 19:45:22 +0200 Subject: [PATCH 31/33] Address some review comments and correct some stale comments --- task/thermal/src/bsp/common/max31790.rs | 4 +--- task/thermal/src/bsp/cosmo_ab.rs | 15 +++++---------- task/thermal/src/bsp/gimlet_bcdef.rs | 15 ++++++--------- task/thermal/src/bsp/grapefruit.rs | 14 ++++++-------- task/thermal/src/bsp/minibar.rs | 5 +---- task/thermal/src/bsp/sidecar_bcd.rs | 1 + 6 files changed, 20 insertions(+), 34 deletions(-) diff --git a/task/thermal/src/bsp/common/max31790.rs b/task/thermal/src/bsp/common/max31790.rs index 1e756126ba..478c661398 100644 --- a/task/thermal/src/bsp/common/max31790.rs +++ b/task/thermal/src/bsp/common/max31790.rs @@ -11,9 +11,7 @@ use task_sensor_api::SensorId; use task_thermal_api::{SensorReadError, ThermalError}; use crate::{ - // control::{ControllerInitError, retry_init}, - __RINGBUF, - Trace, + __RINGBUF, Trace, control::{Fan, FanStatus}, }; diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 72ed3cb613..70db944f0d 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -154,8 +154,6 @@ impl crate::control::BspInterface for Bsp { .map(move |i| i.do_reading(mode, task)) } - // TODO: This probably needs to exist, but for cosmo we have no dynamic - // inputs to read back. This should read from the api and store the state fn read_dynamic_inputs_back_from_sensor_api( &mut self, _sensor_api: &Sensor, @@ -163,13 +161,12 @@ impl crate::control::BspInterface for Bsp { // No dynamic inputs here } - // returns Ok(true) if this was a new input fn update_dynamic_input( &mut self, _index: usize, _model: ThermalProperties, ) -> Result { - // No dynamic inputs here, todo: static assert this + // No dynamic inputs here Err(ThermalError::InvalidIndex) } @@ -178,26 +175,26 @@ impl crate::control::BspInterface for Bsp { &mut self, _index: usize, ) -> Result { - // No dynamic inputs here, todo: static assert this + // No dynamic inputs here Err(ThermalError::InvalidIndex) } fn all_inputs_queried(&self) -> bool { self.inputs.iter().all(InputChannel::has_been_queried) - // && self.dynamic_inputs... + // No dynamic inputs here } fn all_present_inputs_status( &self, ) -> impl Iterator> { self.inputs.iter().filter_map(|input| input.status()) - // .chain(self.dynamic_inputs...) + // No dynamic inputs here } fn reset_all_values(&mut self) { let power = self.power_mode(); self.inputs.iter_mut().for_each(|i| i.reset_value(power)); - // self.dynamic_inputs... + // No dynamic inputs here } fn set_all_watchdogs( @@ -211,8 +208,6 @@ impl crate::control::BspInterface for Bsp { .map_err(|_| ThermalError::DeviceError) } - // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, - // even if some fail. return the LAST error if any. fn set_all_fan_duty(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { let fctrl = self.fctrl.try_initialize()?; let mut any_err = false; diff --git a/task/thermal/src/bsp/gimlet_bcdef.rs b/task/thermal/src/bsp/gimlet_bcdef.rs index 009379f64e..fe60ef00f8 100644 --- a/task/thermal/src/bsp/gimlet_bcdef.rs +++ b/task/thermal/src/bsp/gimlet_bcdef.rs @@ -28,6 +28,7 @@ use i2c_temp_input::{ Device, InputChannel, InputChannelMetadata, TemperatureSensor, }; +// This BSP uses the max31790 for fan control/monitoring #[path = "./common/max31790.rs"] mod max31790; use max31790::Max31790State; @@ -199,8 +200,6 @@ impl crate::control::BspInterface for Bsp { .map(move |i| i.do_reading(mode, task)) } - // TODO: This probably needs to exist, but for gimlet we have no dynamic - // inputs to read back. This should read from the api and store the state fn read_dynamic_inputs_back_from_sensor_api( &mut self, _sensor_api: &Sensor, @@ -214,7 +213,7 @@ impl crate::control::BspInterface for Bsp { _index: usize, _model: ThermalProperties, ) -> Result { - // No dynamic inputs here, todo: static assert this + // No dynamic inputs here Err(ThermalError::InvalidIndex) } @@ -223,26 +222,26 @@ impl crate::control::BspInterface for Bsp { &mut self, _index: usize, ) -> Result { - // No dynamic inputs here, todo: static assert this + // No dynamic inputs here Err(ThermalError::InvalidIndex) } fn all_inputs_queried(&self) -> bool { self.inputs.iter().all(InputChannel::has_been_queried) - // && self.dynamic_inputs... + // No dynamic inputs here } fn all_present_inputs_status( &self, ) -> impl Iterator> { self.inputs.iter().filter_map(|input| input.status()) - // .chain(self.dynamic_inputs...) + // No dynamic inputs here } fn reset_all_values(&mut self) { let power = self.power_mode(); self.inputs.iter_mut().for_each(|i| i.reset_value(power)); - // self.dynamic_inputs... + // No dynamic inputs here } fn set_all_watchdogs( @@ -256,8 +255,6 @@ impl crate::control::BspInterface for Bsp { .map_err(|_| ThermalError::DeviceError) } - // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, - // even if some fail. return the LAST error if any. fn set_all_fan_duty(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { let fctrl = self.fctrl.try_initialize()?; let mut any_err = false; diff --git a/task/thermal/src/bsp/grapefruit.rs b/task/thermal/src/bsp/grapefruit.rs index 2b025995d1..89989b2831 100644 --- a/task/thermal/src/bsp/grapefruit.rs +++ b/task/thermal/src/bsp/grapefruit.rs @@ -23,6 +23,7 @@ use i2c_temp_input::{ Device, InputChannel, InputChannelMetadata, TemperatureSensor, }; +// This BSP uses the emc2305 for fan control/monitoring #[path = "./common/emc2305.rs"] mod emc2305; use emc2305::Emc2305State; @@ -127,13 +128,12 @@ impl crate::control::BspInterface for Bsp { // No dynamic inputs } - // returns Ok(true) if this was a new input fn update_dynamic_input( &mut self, _index: usize, _model: ThermalProperties, ) -> Result { - // No dynamic inputs here, todo: static assert this + // No dynamic inputs here Err(ThermalError::InvalidIndex) } @@ -142,26 +142,26 @@ impl crate::control::BspInterface for Bsp { &mut self, _index: usize, ) -> Result { - // No dynamic inputs here, todo: static assert this + // No dynamic inputs here Err(ThermalError::InvalidIndex) } fn all_inputs_queried(&self) -> bool { self.inputs.iter().all(InputChannel::has_been_queried) - // && self.dynamic_inputs... + // No dynamic inputs } fn all_present_inputs_status( &self, ) -> impl Iterator> { self.inputs.iter().filter_map(|input| input.status()) - // .chain(self.dynamic_inputs...) + // No dynamic inputs } fn reset_all_values(&mut self) { let power = self.power_mode(); self.inputs.iter_mut().for_each(|i| i.reset_value(power)); - // self.dynamic_inputs... + // No dynamic inputs } fn set_all_watchdogs( @@ -175,8 +175,6 @@ impl crate::control::BspInterface for Bsp { .map_err(|_| ThermalError::DeviceError) } - // If a fan is missing, set PWMDuty(0). Attempt to apply to ALL fans, - // even if some fail. return the LAST error if any. fn set_all_fan_duty(&mut self, duty: PWMDuty) -> Result<(), ThermalError> { let fctrl = self.fctrl.try_initialize()?; let mut any_err = false; diff --git a/task/thermal/src/bsp/minibar.rs b/task/thermal/src/bsp/minibar.rs index abea5e8529..3b3e8b4938 100644 --- a/task/thermal/src/bsp/minibar.rs +++ b/task/thermal/src/bsp/minibar.rs @@ -26,6 +26,7 @@ impl crate::control::BspInterface for Bsp { // Run the PID loop on startup const USE_CONTROLLER: bool = false; + // PID config doesn't matter since we have no fans. const PID_CONFIG: PidConfig = PidConfig { zero: 0., gain_p: 0., @@ -83,22 +84,18 @@ impl crate::control::BspInterface for Bsp { // no dynamic inputs } - // returns Ok(true) if this was a new input fn update_dynamic_input( &mut self, _index: usize, _model: ThermalProperties, ) -> Result { - // No dynamic inputs here, todo: static assert this Err(ThermalError::InvalidIndex) } - // sets last_reading to Some(Missing), returns sensor id fn remove_dynamic_input( &mut self, _index: usize, ) -> Result { - // No dynamic inputs here, todo: static assert this Err(ThermalError::InvalidIndex) } diff --git a/task/thermal/src/bsp/sidecar_bcd.rs b/task/thermal/src/bsp/sidecar_bcd.rs index 6e8d00676b..2d3c0c978c 100644 --- a/task/thermal/src/bsp/sidecar_bcd.rs +++ b/task/thermal/src/bsp/sidecar_bcd.rs @@ -30,6 +30,7 @@ use i2c_temp_input::{ Device, InputChannel, InputChannelMetadata, TemperatureSensor, }; +// This BSP uses the max31790 for fan control/monitoring #[path = "./common/max31790.rs"] mod max31790; use max31790::Max31790State; From 25470387b121ce8225fdfe133735b469ecf31612 Mon Sep 17 00:00:00 2001 From: James Munns Date: Wed, 29 Jul 2026 19:45:56 +0200 Subject: [PATCH 32/33] One more --- task/thermal/src/bsp/common/emc2305.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/task/thermal/src/bsp/common/emc2305.rs b/task/thermal/src/bsp/common/emc2305.rs index bd1860052f..0fc71fc5a5 100644 --- a/task/thermal/src/bsp/common/emc2305.rs +++ b/task/thermal/src/bsp/common/emc2305.rs @@ -17,8 +17,6 @@ use crate::{ /// Tracks whether a Emc2305 fan controller has been initialized, and /// initializes it on demand when accessed, if necessary. -/// -/// This is copy-pasted from [`Max31790`] pub(crate) struct Emc2305State { emc2305: Emc2305, fan_count: u8, From 6b78b8a6be4d58c691522d3872ae76881f2eba17 Mon Sep 17 00:00:00 2001 From: James Munns Date: Wed, 29 Jul 2026 19:53:23 +0200 Subject: [PATCH 33/33] Update docs --- task/thermal/src/bsp/cosmo_ab.rs | 1 + task/thermal/src/control.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/task/thermal/src/bsp/cosmo_ab.rs b/task/thermal/src/bsp/cosmo_ab.rs index 70db944f0d..1a58d91cee 100644 --- a/task/thermal/src/bsp/cosmo_ab.rs +++ b/task/thermal/src/bsp/cosmo_ab.rs @@ -28,6 +28,7 @@ use i2c_temp_input::{ Device, InputChannel, InputChannelMetadata, TemperatureSensor, }; +// This BSP uses the max31790 for fan control/monitoring #[path = "./common/max31790.rs"] mod max31790; use max31790::Max31790State; diff --git a/task/thermal/src/control.rs b/task/thermal/src/control.rs index 96031d7049..24bf359b6e 100644 --- a/task/thermal/src/control.rs +++ b/task/thermal/src/control.rs @@ -1243,12 +1243,17 @@ impl<'a, B: BspInterface> ThermalControl<'a, B> { /// * Misc Sensors: Temperature sensors that are actively polled by the thermal /// control loop and reported to the Sensors API, but are not used as inputs /// to the PID control loop. Misc sensors are active in all power states. +/// * Example: The six TMP117 air sensors on Cosmo, which monitor the +/// ambient air temperature within the sled. /// * Dynamic Inputs: Temperature sensors that are NOT actively polled by the /// thermal control loop, from which readings are instead obtained by querying /// the sensor API. These readings are used as inputs to the PID control loop. /// By default, all Dynamic Inputs are not marked as present, and require an /// external command (via IPC) to provide the necessary thermal model, and /// inform the control loop that the sensors are active and should be queried. +/// * Example: Transceivers (xcvrs) 0..32 on Sidecar, which are managed by +/// the `transceivers-server` task, which monitors when an xcvr has been +/// added and removed, and monitors the temperature of any present xcvr. /// * Watchdogs: Features of the external fan controllers that automatically /// move the fans to their highest commanded speed when not communicated with /// for a configured time duration.