1use embedded_hal::delay::DelayNs;
4use embedded_hal::i2c::I2c;
5use pamoja_core::Sensor;
6
7use super::{
8 command, command_frame, data_ready, max_duration_ms, serial_number, temperature_offset_word,
9 word, write_frame, Measurement, I2C_ADDRESS,
10};
11use crate::error::DriverError;
12
13pub const DATA_READY_POLL_MILLIS: u32 = 100;
15
16pub const DATA_READY_POLLS: u8 = 60;
19
20#[derive(Debug)]
52pub struct Scd4x<I2C, D> {
53 bus: I2C,
54 delay: D,
55 serial: Option<u64>,
56}
57
58impl<I2C, D> Scd4x<I2C, D> {
59 pub fn new(bus: I2C, delay: D) -> Self {
70 Scd4x {
71 bus,
72 delay,
73 serial: None,
74 }
75 }
76
77 pub fn serial(&self) -> Option<u64> {
79 self.serial
80 }
81
82 pub fn release(self) -> (I2C, D) {
88 (self.bus, self.delay)
89 }
90}
91
92impl<I2C: I2c, D: DelayNs> Scd4x<I2C, D> {
93 pub fn init(&mut self) -> Result<(), DriverError<I2C::Error>> {
102 self.send(command::STOP_PERIODIC_MEASUREMENT)?;
103 self.send(command::GET_SERIAL_NUMBER)?;
104 let mut frame = [0u8; 9];
105 self.bus
106 .read(I2C_ADDRESS, &mut frame)
107 .map_err(DriverError::Bus)?;
108 let serial = serial_number(&frame)?;
109 self.send(command::START_PERIODIC_MEASUREMENT)?;
110 self.serial = Some(serial);
111 Ok(())
112 }
113
114 pub fn measure(&mut self) -> Result<Measurement, DriverError<I2C::Error>> {
131 if self.serial.is_none() {
132 self.init()?;
133 }
134 let mut ready = false;
135 for _ in 0..DATA_READY_POLLS {
136 if self.data_ready()? {
137 ready = true;
138 break;
139 }
140 self.delay.delay_ms(DATA_READY_POLL_MILLIS);
141 }
142 if !ready {
143 return Err(DriverError::Timeout);
144 }
145 self.read_measurement()
146 }
147
148 pub fn measure_single_shot(&mut self) -> Result<Measurement, DriverError<I2C::Error>> {
164 self.send(command::MEASURE_SINGLE_SHOT)?;
165 self.read_measurement()
166 }
167
168 pub fn data_ready(&mut self) -> Result<bool, DriverError<I2C::Error>> {
180 self.send(command::GET_DATA_READY_STATUS)?;
181 let mut frame = [0u8; 3];
182 self.bus
183 .read(I2C_ADDRESS, &mut frame)
184 .map_err(DriverError::Bus)?;
185 Ok(data_ready(word(&frame)?))
186 }
187
188 pub fn stop(&mut self) -> Result<(), DriverError<I2C::Error>> {
194 self.send(command::STOP_PERIODIC_MEASUREMENT)
195 }
196
197 pub fn start(&mut self) -> Result<(), DriverError<I2C::Error>> {
203 self.send(command::START_PERIODIC_MEASUREMENT)
204 }
205
206 pub fn set_temperature_offset(
220 &mut self,
221 milli_celsius: u32,
222 ) -> Result<(), DriverError<I2C::Error>> {
223 self.stop()?;
224 self.write(
225 command::SET_TEMPERATURE_OFFSET,
226 temperature_offset_word(milli_celsius),
227 )?;
228 self.start()
229 }
230
231 pub fn set_sensor_altitude(&mut self, meters: u16) -> Result<(), DriverError<I2C::Error>> {
244 self.stop()?;
245 self.write(command::SET_SENSOR_ALTITUDE, meters)?;
246 self.start()
247 }
248
249 fn read_measurement(&mut self) -> Result<Measurement, DriverError<I2C::Error>> {
250 self.send(command::READ_MEASUREMENT)?;
251 let mut frame = [0u8; 9];
252 self.bus
253 .read(I2C_ADDRESS, &mut frame)
254 .map_err(DriverError::Bus)?;
255 Ok(Measurement::parse(&frame)?)
256 }
257
258 fn send(&mut self, command: u16) -> Result<(), DriverError<I2C::Error>> {
259 self.bus
260 .write(I2C_ADDRESS, &command_frame(command))
261 .map_err(DriverError::Bus)?;
262 self.wait(command);
263 Ok(())
264 }
265
266 fn write(&mut self, command: u16, value: u16) -> Result<(), DriverError<I2C::Error>> {
267 self.bus
268 .write(I2C_ADDRESS, &write_frame(command, value))
269 .map_err(DriverError::Bus)?;
270 self.wait(command);
271 Ok(())
272 }
273
274 fn wait(&mut self, command: u16) {
275 if let Some(millis) = max_duration_ms(command) {
276 self.delay.delay_ms(u32::from(millis));
277 }
278 }
279}
280
281impl<I2C, D> Sensor for Scd4x<I2C, D>
282where
283 I2C: I2c + Send,
284 I2C::Error: core::fmt::Debug,
285 D: DelayNs + Send,
286{
287 type Reading = Measurement;
288
289 async fn read(&mut self) -> pamoja_core::Result<Measurement> {
290 self.measure().map_err(pamoja_core::Error::from)
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::scd4x::{serial_number_frame, word_frame, PERIODIC_MEASUREMENT_INTERVAL_MS};
298 use crate::SensorError;
299 use alloc::vec;
300 use alloc::vec::Vec;
301 use embedded_hal::i2c::{ErrorKind, NoAcknowledgeSource};
302 use pamoja_hal::script::{block_on, DelayLog, I2cScript, I2cStep};
303
304 const SERIAL: u64 = 0x1234_5678_9ABC;
305
306 fn send(command: u16) -> I2cStep {
307 I2cStep::write(I2C_ADDRESS, command_frame(command))
308 }
309
310 fn init_steps() -> Vec<I2cStep> {
311 vec![
312 send(command::STOP_PERIODIC_MEASUREMENT),
313 send(command::GET_SERIAL_NUMBER),
314 I2cStep::read(I2C_ADDRESS, serial_number_frame(SERIAL)),
315 send(command::START_PERIODIC_MEASUREMENT),
316 ]
317 }
318
319 fn sample() -> Measurement {
320 Measurement::from_physical(500, 25_000, 37_000)
321 }
322
323 #[test]
324 fn init_stops_identifies_and_starts_then_measure_polls_and_reads() {
325 let mut steps = init_steps();
326 steps.extend([
327 send(command::GET_DATA_READY_STATUS),
328 I2cStep::read(I2C_ADDRESS, word_frame(0x8000)),
329 send(command::GET_DATA_READY_STATUS),
330 I2cStep::read(I2C_ADDRESS, word_frame(0x8006)),
331 send(command::READ_MEASUREMENT),
332 I2cStep::read(I2C_ADDRESS, sample().to_bytes()),
333 ]);
334 let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
335 sensor.init().unwrap();
336 assert_eq!(sensor.serial(), Some(SERIAL));
337 let measurement = sensor.measure().unwrap();
338 assert_eq!(measurement, sample());
339 assert_eq!(measurement.co2_ppm, 500);
340 let (bus, delay) = sensor.release();
341 assert!(bus.done());
342 assert_eq!(
343 delay.waits_ns(),
344 [500_000_000, 1_000_000, 1_000_000, 100_000_000, 1_000_000, 1_000_000],
345 "stop waits 500 ms, each read waits its 1 ms execution time, a not-ready poll waits 100 ms"
346 );
347 }
348
349 #[test]
350 fn a_result_that_never_comes_is_a_timeout() {
351 let mut steps = init_steps();
352 for _ in 0..DATA_READY_POLLS {
353 steps.push(send(command::GET_DATA_READY_STATUS));
354 steps.push(I2cStep::read(I2C_ADDRESS, word_frame(0x8000)));
355 }
356 let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
357 assert_eq!(sensor.measure(), Err(DriverError::Timeout));
358 let (bus, delay) = sensor.release();
359 assert!(bus.done());
360 let polls = u64::from(DATA_READY_POLLS);
361 assert_eq!(delay.total_millis(), 500 + 1 + polls * (1 + 100));
362 assert!(delay.total_millis() > u64::from(PERIODIC_MEASUREMENT_INTERVAL_MS));
363 }
364
365 #[test]
366 fn a_corrupted_word_is_a_crc_failure_and_a_bus_fault_a_bus_error() {
367 let mut corrupted = serial_number_frame(SERIAL);
368 corrupted[2] ^= 0x01;
369 let steps = [
370 send(command::STOP_PERIODIC_MEASUREMENT),
371 send(command::GET_SERIAL_NUMBER),
372 I2cStep::read(I2C_ADDRESS, corrupted),
373 ];
374 let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
375 assert_eq!(sensor.init(), Err(DriverError::Sensor(SensorError::Crc)));
376 assert_eq!(sensor.serial(), None);
377
378 let kind = ErrorKind::NoAcknowledge(NoAcknowledgeSource::Address);
379 let mut sensor = Scd4x::new(
380 I2cScript::new([I2cStep::fault(I2C_ADDRESS, kind)]),
381 DelayLog::new(),
382 );
383 assert!(matches!(sensor.init(), Err(DriverError::Bus(_))));
384 }
385
386 #[test]
387 fn a_single_shot_waits_five_seconds_then_reads() {
388 let steps = [
389 send(command::MEASURE_SINGLE_SHOT),
390 send(command::READ_MEASUREMENT),
391 I2cStep::read(I2C_ADDRESS, sample().to_bytes()),
392 ];
393 let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
394 assert_eq!(sensor.measure_single_shot().unwrap(), sample());
395 let (bus, delay) = sensor.release();
396 assert!(bus.done());
397 assert_eq!(delay.total_millis(), 5_001);
398 }
399
400 #[test]
401 fn settings_stop_write_and_restart_the_periodic_measurement() {
402 let mut steps = init_steps();
403 steps.extend([
404 send(command::STOP_PERIODIC_MEASUREMENT),
405 I2cStep::write(
406 I2C_ADDRESS,
407 write_frame(
408 command::SET_TEMPERATURE_OFFSET,
409 temperature_offset_word(4_000),
410 ),
411 ),
412 send(command::START_PERIODIC_MEASUREMENT),
413 send(command::STOP_PERIODIC_MEASUREMENT),
414 I2cStep::write(
415 I2C_ADDRESS,
416 write_frame(command::SET_SENSOR_ALTITUDE, 1_500),
417 ),
418 send(command::START_PERIODIC_MEASUREMENT),
419 ]);
420 let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
421 sensor.init().unwrap();
422 sensor.set_temperature_offset(4_000).unwrap();
423 sensor.set_sensor_altitude(1_500).unwrap();
424 assert!(sensor.release().0.done());
425 }
426
427 #[test]
428 fn as_a_sensor_it_reads_and_maps_to_co2() {
429 let mut steps = init_steps();
430 steps.extend([
431 send(command::GET_DATA_READY_STATUS),
432 I2cStep::read(I2C_ADDRESS, word_frame(0x8006)),
433 send(command::READ_MEASUREMENT),
434 I2cStep::read(I2C_ADDRESS, sample().to_bytes()),
435 ]);
436 let sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
437 let mut co2 = sensor.map(|measurement| measurement.co2_ppm);
438 assert_eq!(block_on(co2.read()).unwrap(), 500);
439 }
440
441 #[test]
442 fn start_up_and_one_reading_make_the_transfers_the_datasheet_gives() {
443 use crate::scd4x::{Measurement, I2C_ADDRESS};
444
445 const PART: u8 = I2C_ADDRESS;
446 let bus = I2cScript::new([
447 I2cStep::write(PART, [0x3F, 0x86]),
448 I2cStep::write(PART, [0x36, 0x82]),
449 I2cStep::read(PART, serial_number_frame(0x1234_5678_9ABC)),
450 I2cStep::write(PART, [0x21, 0xB1]),
451 I2cStep::write(PART, [0xE4, 0xB8]),
452 I2cStep::read(PART, word_frame(0x8006)),
453 I2cStep::write(PART, [0xEC, 0x05]),
454 I2cStep::read(
455 PART,
456 Measurement::from_physical(500, 25_000, 37_000).to_bytes(),
457 ),
458 ]);
459
460 let mut sensor = Scd4x::new(bus, DelayLog::new());
461 let measurement = block_on(Sensor::read(&mut sensor)).expect("the scripted part answers");
462 assert_eq!(measurement.co2_ppm, 500);
463 assert_eq!(sensor.serial(), Some(0x1234_5678_9ABC));
464 }
465}