/ firmware / src / services / http / api.rs
api.rs
  1  use heapless::String as HeaplessString;
  2  use picoserve::response::{IntoResponse, Json};
  3  use serde::Serialize;
  4  
  5  use crate::sensors::manager;
  6  use crate::services::system;
  7  
  8  #[derive(Serialize)]
  9  struct ApiResponse<'a> {
 10      co2: Co2Data,
 11      status: StatusData<'a>,
 12      sensors: heapless::Vec<SensorData<'a>, { manager::MAX_SENSOR_COUNT }>,
 13      files: heapless::Vec<FileData, 64>,
 14  }
 15  
 16  #[derive(Serialize)]
 17  struct Co2Data {
 18      co2_ppm: f32,
 19      temperature: f32,
 20      humidity: f32,
 21      model: &'static str,
 22      ok: bool,
 23  }
 24  
 25  #[derive(Serialize)]
 26  struct StatusData<'a> {
 27      hostname: &'a str,
 28      platform: &'a str,
 29      uptime_seconds: u64,
 30      heap_free: usize,
 31      heap_used: usize,
 32      sd_card_mb: u32,
 33      sleep_pending: bool,
 34      wake_cause: &'a str,
 35      data_log_path: &'a str,
 36      data_log_interval_seconds: u64,
 37  }
 38  
 39  #[derive(Serialize)]
 40  struct SensorData<'a> {
 41      name: &'a str,
 42      model: &'a str,
 43      transport: SensorTransportData,
 44      live: SensorLiveData,
 45  }
 46  
 47  #[derive(Serialize)]
 48  #[serde(tag = "kind", rename_all = "snake_case")]
 49  enum SensorTransportData {
 50      I2c {
 51          bus_index: u8,
 52          address: u8,
 53          mux_channel: i8,
 54      },
 55      Modbus {
 56          channel: u8,
 57          slave_id: u8,
 58          register_address: u16,
 59      },
 60  }
 61  
 62  #[derive(Serialize)]
 63  #[serde(tag = "kind", rename_all = "snake_case")]
 64  enum SensorLiveData {
 65      None,
 66      Co2 {
 67          ok: bool,
 68          co2_ppm: f32,
 69          temperature: f32,
 70          humidity: f32,
 71      },
 72      TemperatureHumidity {
 73          ok: bool,
 74          temperature_celsius: f32,
 75          relative_humidity_percent: f32,
 76      },
 77  }
 78  
 79  #[derive(Serialize)]
 80  struct FileData {
 81      name: HeaplessString<32>,
 82      size: u32,
 83  }
 84  
 85  pub async fn api_handler() -> impl IntoResponse {
 86      let snapshot = system::snapshot();
 87  
 88      let mut sensor_list = heapless::Vec::<SensorData<'_>, { manager::MAX_SENSOR_COUNT }>::new();
 89      for sensor in snapshot.sensors.inventory.iter() {
 90          let transport_summary = sensor.transport_summary();
 91          let transport = if let Some(address) = transport_summary.address {
 92              SensorTransportData::I2c {
 93                  bus_index: transport_summary.bus_index.unwrap_or_default(),
 94                  address,
 95                  mux_channel: transport_summary.mux_channel.unwrap_or(-1),
 96              }
 97          } else {
 98              SensorTransportData::Modbus {
 99                  channel: transport_summary.channel.unwrap_or_default(),
100                  slave_id: transport_summary.slave_id.unwrap_or_default(),
101                  register_address: transport_summary.register_address.unwrap_or_default(),
102              }
103          };
104  
105          let live = if let Some(reading) = sensor.carbon_dioxide_reading() {
106              SensorLiveData::Co2 {
107                  ok: reading.ok,
108                  co2_ppm: reading.co2_ppm,
109                  temperature: reading.temperature,
110                  humidity: reading.humidity,
111              }
112          } else if let Some(reading) = sensor.temperature_humidity_reading() {
113              SensorLiveData::TemperatureHumidity {
114                  ok: reading.ok,
115                  temperature_celsius: reading.temperature_celsius,
116                  relative_humidity_percent: reading.relative_humidity_percent,
117              }
118          } else {
119              SensorLiveData::None
120          };
121  
122          let _ = sensor_list.push(SensorData {
123              name: sensor.name,
124              model: sensor.model,
125              transport,
126              live,
127          });
128      }
129  
130      let mut file_list = heapless::Vec::<FileData, 64>::new();
131      if let Ok(entries) = crate::filesystems::sd::list_filesystem_entries() {
132          for entry in &entries {
133              let _ = file_list.push(FileData {
134                  name: entry.name.clone(),
135                  size: entry.size,
136              });
137          }
138      }
139  
140      Json(ApiResponse {
141          co2: Co2Data {
142              co2_ppm: snapshot.sensors.carbon_dioxide.co2_ppm,
143              temperature: snapshot.sensors.carbon_dioxide.temperature,
144              humidity: snapshot.sensors.carbon_dioxide.humidity,
145              model: snapshot.sensors.carbon_dioxide.model,
146              ok: snapshot.sensors.carbon_dioxide.ok,
147          },
148          status: StatusData {
149              hostname: snapshot.hostname,
150              platform: snapshot.platform,
151              uptime_seconds: snapshot.uptime_seconds,
152              heap_free: snapshot.heap_free,
153              heap_used: snapshot.heap_used,
154              sd_card_mb: snapshot.storage.sd_card_size_mb,
155              sleep_pending: snapshot.sleep.pending,
156              wake_cause: snapshot.sleep.wake_cause,
157              data_log_path: snapshot.data_logger.path,
158              data_log_interval_seconds: snapshot.data_logger.interval_seconds,
159          },
160          sensors: sensor_list,
161          files: file_list,
162      })
163      .into_response()
164      .with_header("Access-Control-Allow-Origin", "*")
165  }