firmware_t1000e/
temperature.rs

1//! Temperature sources owned by the device environment for one physical boot.
2
3use umsh_ulcp::{Status, temperature::UNKNOWN};
4use umsh_ulcp_runtime::temperature::TemperatureInventory;
5
6/// A driver instance, independent of its display label. Future battery and
7/// external drivers add source variants and their acquisition branches here.
8#[derive(Clone, Copy)]
9pub(crate) enum Source {
10    McuDie,
11    #[cfg(feature = "board-techo")]
12    Bme280,
13    #[cfg(feature = "t1000e")]
14    BoardNtc,
15    #[cfg(feature = "t1000e")]
16    RadioDie,
17}
18
19pub(crate) trait Reader {
20    /// Tenths of a kelvin; None means acquisition failed or is unavailable.
21    async fn sample(&mut self, source: Source) -> Option<u16>;
22}
23
24pub(crate) struct Sensors<R> {
25    inventory: TemperatureInventory<Source>,
26    reader: R,
27}
28
29impl<R: Reader> Sensors<R> {
30    pub(crate) fn new(reader: R) -> Self {
31        let mut sensors = Self {
32            inventory: TemperatureInventory::new(),
33            reader,
34        };
35        sensors
36            .register(Source::McuDie, "MCU die")
37            .expect("initial die sensor fits");
38        sensors
39    }
40
41    /// Add the board's fixed inventory once at boot, after the MCU slot.
42    pub(crate) fn with_board_sensors(reader: R) -> Self {
43        #[allow(unused_mut)]
44        let mut sensors = Self::new(reader);
45        #[cfg(all(target_os = "none", feature = "board-techo"))]
46        if super::bme280::SENSOR.address().is_some() {
47            sensors
48                .register(Source::Bme280, "BME280")
49                .expect("BME280 fits");
50        }
51        #[cfg(feature = "t1000e")]
52        {
53            sensors
54                .register(Source::BoardNtc, "Board NTC")
55                .expect("NTC fits");
56            sensors
57                .register(Source::RadioDie, "LoRa die")
58                .expect("radio fits");
59        }
60        sensors
61    }
62
63    /// Future discovery registers through this owner, retaining existing slots
64    /// when a driver becomes unavailable. Never rebuild on a protocol reset.
65    pub(crate) fn register(&mut self, source: Source, name: &str) -> Result<usize, Status> {
66        self.inventory.register(source, name)
67    }
68
69    pub(crate) fn read_names(&self, out: &mut [u8]) -> Result<usize, Status> {
70        self.inventory.read_names(out)
71    }
72
73    pub(crate) async fn sample(&mut self, out: &mut [u16]) -> Result<usize, Status> {
74        let sources = self.inventory.snapshot();
75        let slots = out.get_mut(..sources.len()).ok_or(Status::NOMEM)?;
76        for (slot, source) in slots.iter_mut().zip(sources) {
77            *slot = self.reader.sample(source).await.unwrap_or(UNKNOWN);
78        }
79        Ok(slots.len())
80    }
81}
82
83/// Signed quarter-degrees Celsius to tenths of a kelvin, nearest with ties up.
84/// Twice the output is exactly `raw * 5 + 5463`; widen before arithmetic.
85/// Quantizing the wire value does not increase the sensor's resolution/accuracy.
86fn quarter_celsius_to_tenths_kelvin(raw: i32) -> Option<u16> {
87    let twice_tenths = i64::from(raw) * 5 + 5463;
88    if twice_tenths < 0 {
89        return None;
90    }
91    let rounded = (twice_tenths + 1) / 2;
92    u16::try_from(rounded)
93        .ok()
94        .filter(|&value| value != UNKNOWN)
95}
96
97#[cfg(target_os = "none")]
98impl Reader for &'static nrf_mpsl::MultiprotocolServiceLayer<'static> {
99    async fn sample(&mut self, source: Source) -> Option<u16> {
100        match source {
101            #[cfg(feature = "board-techo")]
102            Source::Bme280 => super::bme280::sample().await,
103            Source::McuDie => {
104                // MPSL owns TEMP. Its approximately 50 us synchronous acquisition
105                // runs at the same thread-mode priority as mpsl_task's run loop.
106                quarter_celsius_to_tenths_kelvin(self.get_temperature().raw())
107            }
108            #[cfg(feature = "t1000e")]
109            Source::BoardNtc => umsh_bsp_t1000e::temperature::sample().await,
110            #[cfg(feature = "t1000e")]
111            Source::RadioDie => super::firmware::sample_radio_temperature().await,
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use embassy_futures::block_on;
120
121    #[test]
122    fn temperature_conversion_is_signed_and_in_tenths_kelvin() {
123        for (raw, expected) in [
124            (100, 2982),
125            (0, 2732),
126            (-40, 2632),
127            (-1, 2729),
128            (1, 2734),
129            (2, 2737),
130            (-1092, 2),
131            (25121, 65534),
132        ] {
133            assert_eq!(quarter_celsius_to_tenths_kelvin(raw), Some(expected));
134        }
135        for raw in [-1093, 25122, i32::MIN, i32::MAX] {
136            assert_eq!(quarter_celsius_to_tenths_kelvin(raw), None);
137        }
138    }
139
140    struct FakeReader {
141        calls: usize,
142        values: [Option<u16>; 3],
143    }
144    impl Reader for FakeReader {
145        async fn sample(&mut self, _: Source) -> Option<u16> {
146            let value = self.values[self.calls % self.values.len()];
147            self.calls += 1;
148            value
149        }
150    }
151
152    #[test]
153    fn temperature_names_and_short_buffers_do_not_sample() {
154        let mut sensors = Sensors::new(FakeReader {
155            calls: 0,
156            values: [Some(2982); 3],
157        });
158        let mut names = [0; 32];
159        let len = sensors.read_names(&mut names).unwrap();
160        assert_eq!(&names[..len], b"\x07MCU die");
161        assert_eq!(block_on(sensors.sample(&mut [])), Err(Status::NOMEM));
162        assert_eq!(sensors.reader.calls, 0);
163        let mut out = [0; 1];
164        for expected_calls in 1..=2 {
165            assert_eq!(block_on(sensors.sample(&mut out)), Ok(1));
166            assert_eq!(out, [2982]);
167            assert_eq!(sensors.reader.calls, expected_calls);
168        }
169    }
170
171    #[test]
172    fn temperature_failures_preserve_slots_and_names() {
173        let mut sensors = Sensors::new(FakeReader {
174            calls: 0,
175            values: [Some(2982), None, Some(2800)],
176        });
177        assert_eq!(sensors.register(Source::McuDie, "Battery"), Ok(1));
178        assert_eq!(sensors.register(Source::McuDie, "External"), Ok(2));
179        let mut names = [0; 64];
180        let len = sensors.read_names(&mut names).unwrap();
181        let mut out = [0; 3];
182        assert_eq!(block_on(sensors.sample(&mut out)), Ok(3));
183        assert_eq!(out, [2982, UNKNOWN, 2800]);
184        sensors.reader.values = [None; 3];
185        assert_eq!(block_on(sensors.sample(&mut out)), Ok(3));
186        assert_eq!(out, [UNKNOWN; 3]);
187        let mut after = [0; 64];
188        assert_eq!(sensors.read_names(&mut after), Ok(len));
189        assert_eq!(names, after);
190    }
191
192    #[cfg(feature = "t1000e")]
193    #[test]
194    fn temperature_board_inventory_retains_unavailable_radio_slot() {
195        let mut sensors = Sensors::with_board_sensors(FakeReader {
196            calls: 0,
197            values: [Some(3000), Some(2982), None],
198        });
199        let mut names = [0; 64];
200        let len = sensors.read_names(&mut names).unwrap();
201        assert_eq!(&names[..len], b"\x07MCU die\x09Board NTC\x08LoRa die");
202        assert_eq!(sensors.reader.calls, 0);
203        let mut out = [0; 3];
204        assert_eq!(block_on(sensors.sample(&mut out)), Ok(3));
205        assert_eq!(out, [3000, 2982, UNKNOWN]);
206        sensors.reader.values[2] = Some(2990);
207        assert_eq!(block_on(sensors.sample(&mut out)), Ok(3));
208        assert_eq!(out, [3000, 2982, 2990]);
209        assert_eq!(sensors.reader.calls, 6);
210    }
211}