1use umsh_ulcp::{Status, temperature::UNKNOWN};
4use umsh_ulcp_runtime::temperature::TemperatureInventory;
5
6#[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 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 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 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
83fn 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 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}