/ firmware / src / programs / temperature_and_humidity.rs
temperature_and_humidity.rs
  1  //! Temperature and humidity sensor program.
  2  //!
  3  //! Reads from an SHT31-compatible I2C sensor and logs to the SD card CSV.
  4  
  5  use defmt::info;
  6  use embassy_time::{Duration, Instant, Ticker, Timer};
  7  use esp_hal::i2c::master::I2c;
  8  
  9  use crate::hardware::i2c::{SENSOR_MEASUREMENT_COMMAND, calculate_crc8};
 10  use crate::services::data_logger;
 11  use crate::sensors::manager::{self, TemperatureHumidityReading};
 12  
 13  // ─── Sensor reading ────────────────────────────────────────────────────────────
 14  
 15  fn convert_temperature(raw: u16) -> f32 {
 16      -45.0 + 175.0 * (raw as f32 / 65535.0)
 17  }
 18  
 19  fn convert_humidity(raw: u16) -> f32 {
 20      100.0 * (raw as f32 / 65535.0)
 21  }
 22  
 23  async fn read_once(
 24      i2c_bus: &mut I2c<'static, esp_hal::Async>,
 25      sensor_address: u8,
 26  ) -> Result<(f32, f32), &'static str> {
 27      i2c_bus
 28          .write_async(sensor_address, &SENSOR_MEASUREMENT_COMMAND)
 29          .await
 30          .map_err(|_| "failed to send measurement command")?;
 31  
 32      Timer::after(Duration::from_millis(60)).await;
 33  
 34      let mut buf = [0_u8; 6];
 35      i2c_bus
 36          .read_async(sensor_address, &mut buf)
 37          .await
 38          .map_err(|_| "failed to read measurement bytes")?;
 39  
 40      let temp_bytes = [buf[0], buf[1]];
 41      let hum_bytes = [buf[3], buf[4]];
 42  
 43      if buf[2] != calculate_crc8(&temp_bytes) {
 44          return Err("temperature CRC mismatch");
 45      }
 46      if buf[5] != calculate_crc8(&hum_bytes) {
 47          return Err("humidity CRC mismatch");
 48      }
 49  
 50      Ok((
 51          convert_temperature(u16::from_be_bytes(temp_bytes)),
 52          convert_humidity(u16::from_be_bytes(hum_bytes)),
 53      ))
 54  }
 55  
 56  // ─── Data logging task ─────────────────────────────────────────────────────────
 57  
 58  #[embassy_executor::task]
 59  pub async fn task(
 60      mut i2c_bus: I2c<'static, esp_hal::Async>,
 61      sensor_address: u8,
 62      sensor_index: usize,
 63      sensor_name: &'static str,
 64  ) {
 65      let mut sampling_interval = Ticker::every(Duration::from_secs(
 66          crate::config::app::data_logger::SAMPLING_INTERVAL_SECS,
 67      ));
 68  
 69      loop {
 70          sampling_interval.next().await;
 71  
 72          match read_once(&mut i2c_bus, sensor_address).await {
 73              Ok((temperature_celsius, relative_humidity_percent)) => {
 74                  manager::publish_temperature_humidity_reading(
 75                      sensor_index,
 76                      TemperatureHumidityReading {
 77                          ok: true,
 78                          temperature_celsius,
 79                          relative_humidity_percent,
 80                          model: "SHT31",
 81                          name: sensor_name,
 82                      },
 83                  );
 84                  let timestamp_millis = Instant::now().as_millis();
 85                  if let Err(msg) = data_logger::append_temperature_humidity_sample(
 86                      timestamp_millis,
 87                      temperature_celsius,
 88                      relative_humidity_percent,
 89                  ) {
 90                      info!("failed to append data.csv row: {}", msg);
 91                  } else {
 92                      info!(
 93                          "logged {} sample: temperature={}C humidity={}%%",
 94                          sensor_name, temperature_celsius, relative_humidity_percent
 95                      );
 96                  }
 97              }
 98              Err(msg) => {
 99                  manager::mark_temperature_humidity_unavailable(sensor_index);
100                  info!("failed to read {} sensor: {}", sensor_name, msg);
101              }
102          }
103      }
104  }