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 }