/ firmware / src / programs / carbon_dioxide.rs
carbon_dioxide.rs
  1  use defmt::info;
  2  use embassy_time::{Duration, Timer};
  3  use esp_hal::i2c::master::I2c;
  4  use scd30_interface::{asynch::Scd30, data::{AmbientPressureCompensation, DataStatus}};
  5  use scd4x::Scd4xAsync;
  6  
  7  use crate::config::app;
  8  use crate::sensors::manager::{self, Co2Reading};
  9  
 10  pub type AsyncI2cBus = I2c<'static, esp_hal::Async>;
 11  pub type Scd30Sensor = Scd30<AsyncI2cBus>;
 12  pub type Scd4xSensor = Scd4xAsync<AsyncI2cBus, embassy_time::Delay>;
 13  
 14  pub enum Backend {
 15      Scd30(Scd30Sensor),
 16      Scd4x(Scd4xSensor),
 17  }
 18  
 19  #[derive(Clone, Copy)]
 20  pub enum BackendKind {
 21      Scd30,
 22      Scd4x,
 23  }
 24  
 25  fn scd30_address() -> u8 {
 26      manager::carbon_dioxide_address_scd30()
 27  }
 28  
 29  fn scd4x_address() -> u8 {
 30      manager::carbon_dioxide_address_scd4x()
 31  }
 32  
 33  fn sensor_name() -> &'static str {
 34      manager::carbon_dioxide_name()
 35  }
 36  
 37  fn model_label(backend_kind: BackendKind) -> &'static str {
 38      match backend_kind {
 39          BackendKind::Scd30 => manager::carbon_dioxide_model_scd30(),
 40          BackendKind::Scd4x => manager::carbon_dioxide_model_scd4x(),
 41      }
 42  }
 43  
 44  pub async fn probe_scd30(i2c_bus: AsyncI2cBus) -> Result<Scd30Sensor, AsyncI2cBus> {
 45      let mut sensor = Scd30::new(i2c_bus);
 46  
 47      if let Err(error) = sensor.read_firmware_version().await {
 48          info!("SCD30 probe failed at {=u8:#x}: {:?}", scd30_address(), error);
 49          return Err(sensor.shutdown());
 50      }
 51  
 52      let _ = sensor.stop_continuous_measurements().await;
 53      Timer::after(Duration::from_millis(100)).await;
 54  
 55      if let Err(error) = sensor
 56          .trigger_continuous_measurements(Some(AmbientPressureCompensation::DefaultPressure))
 57          .await
 58      {
 59          info!("SCD30 start measurement failed: {:?}", error);
 60          return Err(sensor.shutdown());
 61      }
 62  
 63      info!("SCD30 initialized at {=u8:#x}", scd30_address());
 64      Ok(sensor)
 65  }
 66  
 67  pub async fn probe_scd4x(i2c_bus: AsyncI2cBus) -> Result<Scd4xSensor, AsyncI2cBus> {
 68      let mut sensor = Scd4xAsync::new(i2c_bus, embassy_time::Delay);
 69  
 70      if sensor.serial_number().await.is_err() {
 71          info!("SCD4x probe failed at {=u8:#x}", scd4x_address());
 72          return Err(sensor.destroy());
 73      }
 74  
 75      let _ = sensor.stop_periodic_measurement().await;
 76      Timer::after(Duration::from_millis(500)).await;
 77  
 78      if sensor.reinit().await.is_err() {
 79          info!("SCD4x reinit failed");
 80          return Err(sensor.destroy());
 81      }
 82  
 83      if sensor.start_periodic_measurement().await.is_err() {
 84          info!("SCD4x start measurement failed");
 85          return Err(sensor.destroy());
 86      }
 87  
 88      info!("SCD4x initialized at {=u8:#x}", scd4x_address());
 89      Ok(sensor)
 90  }
 91  
 92  pub async fn read_scd30(sensor: &mut Scd30Sensor) -> Result<Co2Reading, ()> {
 93      let mut data_ready = false;
 94  
 95      for _attempt in 0..app::data_logger::POLL_RETRIES {
 96          match sensor.is_data_ready().await {
 97              Ok(status) if status == DataStatus::Ready => {
 98                  data_ready = true;
 99                  break;
100              }
101              Ok(_) => {}
102              Err(error) => {
103                  info!("SCD30 data_ready error: {:?}", error);
104                  return Err(());
105              }
106          }
107          Timer::after(Duration::from_millis(app::data_logger::POLL_INTERVAL_MS)).await;
108      }
109  
110      if !data_ready {
111          info!("SCD30 poll timed out");
112          return Err(());
113      }
114  
115      match sensor.read_measurement().await {
116          Ok(measurement) => Ok(Co2Reading {
117              ok: true,
118              co2_ppm: measurement.co2_concentration,
119              temperature: measurement.temperature,
120              humidity: measurement.humidity,
121              model: model_label(BackendKind::Scd30),
122              name: sensor_name(),
123          }),
124          Err(error) => {
125              info!("SCD30 read error: {:?}", error);
126              Err(())
127          }
128      }
129  }
130  
131  pub async fn read_scd4x(sensor: &mut Scd4xSensor) -> Result<Co2Reading, ()> {
132      let mut data_ready = false;
133  
134      for _attempt in 0..app::carbon_dioxide::SCD4X_POLL_RETRIES {
135          match sensor.data_ready_status().await {
136              Ok(true) => {
137                  data_ready = true;
138                  break;
139              }
140              Ok(false) => {}
141              Err(_) => {
142                  info!("SCD4x data_ready error");
143                  return Err(());
144              }
145          }
146          Timer::after(Duration::from_millis(app::carbon_dioxide::SCD4X_POLL_INTERVAL_MS)).await;
147      }
148  
149      if !data_ready {
150          info!("SCD4x poll timed out");
151          return Err(());
152      }
153  
154      match sensor.measurement().await {
155          Ok(measurement) => Ok(Co2Reading {
156              ok: true,
157              co2_ppm: measurement.co2 as f32,
158              temperature: measurement.temperature,
159              humidity: measurement.humidity,
160              model: model_label(BackendKind::Scd4x),
161              name: sensor_name(),
162          }),
163          Err(_) => {
164              info!("SCD4x read error");
165              Err(())
166          }
167      }
168  }
169  
170  fn failed_reading(model: &'static str) -> Co2Reading {
171      Co2Reading {
172          model,
173          name: sensor_name(),
174          ..Co2Reading::default()
175      }
176  }
177  
178  #[embassy_executor::task]
179  pub async fn task(i2c_bus: AsyncI2cBus) {
180      sensor_loop(i2c_bus).await
181  }
182  
183  pub async fn sensor_loop(i2c_bus: AsyncI2cBus) -> ! {
184      let mut current_backend: Option<Backend> = None;
185      let mut bus_for_probing = Some(i2c_bus);
186      let mut consecutive_failures = 0usize;
187  
188      loop {
189          if current_backend.is_none() {
190              let Some(bus) = bus_for_probing.take() else {
191                  info!("co2 probing skipped: I2C bus unavailable");
192                  Timer::after(Duration::from_secs(app::carbon_dioxide::PROBE_RETRY_SECS)).await;
193                  continue;
194              };
195  
196              match probe_scd30(bus).await {
197                  Ok(scd30) => {
198                      current_backend = Some(Backend::Scd30(scd30));
199                      info!("co2 backend: {=str}", model_label(BackendKind::Scd30));
200                      consecutive_failures = 0;
201                      continue;
202                  }
203                  Err(bus) => {
204                      match probe_scd4x(bus).await {
205                          Ok(scd4x) => {
206                              current_backend = Some(Backend::Scd4x(scd4x));
207                              info!("co2 backend: {=str}", model_label(BackendKind::Scd4x));
208                              consecutive_failures = 0;
209                              continue;
210                          }
211                          Err(bus) => {
212                              bus_for_probing = Some(bus);
213                              info!(
214                                  "co2 probe failed; retry in {=u64}s",
215                                  app::carbon_dioxide::PROBE_RETRY_SECS
216                              );
217                              Timer::after(Duration::from_secs(
218                                  app::carbon_dioxide::PROBE_RETRY_SECS,
219                              ))
220                              .await;
221                              continue;
222                          }
223                      }
224                  }
225              }
226          }
227  
228          let result = match current_backend.as_mut().unwrap() {
229              Backend::Scd30(sensor) => read_scd30(sensor).await,
230              Backend::Scd4x(sensor) => read_scd4x(sensor).await,
231          };
232  
233          match result {
234              Ok(reading) => {
235                  manager::publish_carbon_dioxide_reading(reading);
236                  consecutive_failures = 0;
237  
238                  info!(
239                      "{=str}: co2={=f32} temp={=f32} rh={=f32}",
240                      reading.model, reading.co2_ppm, reading.temperature, reading.humidity
241                  );
242              }
243              Err(()) => {
244                  let model = match current_backend.as_ref().unwrap() {
245                      Backend::Scd30(_) => model_label(BackendKind::Scd30),
246                      Backend::Scd4x(_) => model_label(BackendKind::Scd4x),
247                  };
248  
249                  manager::publish_carbon_dioxide_reading(failed_reading(model));
250                  consecutive_failures += 1;
251  
252                  if consecutive_failures >= app::carbon_dioxide::MAX_CONSECUTIVE_FAILURES {
253                      info!(
254                          "co2: {=usize} consecutive failures; resetting",
255                          consecutive_failures
256                      );
257  
258                      let backend = current_backend.take().unwrap();
259                      bus_for_probing = Some(match backend {
260                          Backend::Scd30(sensor) => sensor.shutdown(),
261                          Backend::Scd4x(sensor) => sensor.destroy(),
262                      });
263                      consecutive_failures = 0;
264  
265                      Timer::after(Duration::from_secs(1)).await;
266                      continue;
267                  }
268              }
269          }
270  
271          Timer::after(Duration::from_secs(
272              app::data_logger::SAMPLING_INTERVAL_SECS,
273          ))
274          .await;
275      }
276  }