/ firmware / src / programs / microfetch.rs
microfetch.rs
  1  use alloc::string::String as AllocString;
  2  use core::fmt::Write;
  3  use embassy_time::Instant;
  4  use esp_hal::{clock, efuse, system, system::Cpu};
  5  
  6  use crate::{
  7      config::{app, board},
  8      console::icons,
  9      hardware,
 10      services::{identity, system as system_service},
 11  };
 12  
 13  unsafe extern "C" {
 14      #[link_name = "esp_app_desc"]
 15      static ESP_APP_DESC: esp_bootloader_esp_idf::EspAppDesc;
 16  }
 17  
 18  // TODO(perf): The 30+ row!() invocations below are repetitive but each
 19  // has different format arguments, so a data-driven array of tuples would
 20  // require pre-formatting each value into an AllocString (one heap
 21  // allocation per row). On PSRAM that's tolerable but wasteful. The
 22  // current approach writes directly to a single output buffer with zero
 23  // intermediate allocations. Refactor only if readability becomes a
 24  // maintenance burden — the format arguments are type-checked at compile
 25  // time, which a data-driven approach would lose.
 26  macro_rules! row {
 27      ($out:expr, $color:expr, $icon:expr, $label:expr, $($val:tt)*) => {{
 28          let _ = write!($out, "  \x1b[1;{}m{} {:<14}\x1b[0m ", $color, $icon, $label);
 29          let _ = write!($out, $($val)*);
 30          let _ = write!($out, "\r\n");
 31      }};
 32  }
 33  
 34  pub fn run() -> AllocString {
 35      let mut out = AllocString::new();
 36      let secs = Instant::now().as_secs();
 37      let system_snapshot = system_service::snapshot();
 38      let sensor_inventory = &system_snapshot.sensors.inventory;
 39      let carbon_dioxide = system_snapshot.sensors.carbon_dioxide;
 40      let i2c_status = hardware::i2c::snapshot();
 41      let chip_rev = efuse::chip_revision();
 42      let mac = efuse::base_mac_address();
 43      let cpu_freq_mhz = clock::cpu_clock().as_mhz();
 44  
 45      let heap_used = esp_alloc::HEAP.used();
 46      let heap_free = esp_alloc::HEAP.free();
 47      let heap_total = heap_used + heap_free;
 48      let heap_pct = (heap_used * 100) / heap_total;
 49  
 50      let _ = write!(out, "\r\n");
 51  
 52      let _ = write!(
 53          out,
 54          "  \x1b[1;32m{}\x1b[0m\x1b[2m@\x1b[0m\x1b[1;36m{}\x1b[0m\r\n",
 55          identity::ssh_user(),
 56          identity::hostname()
 57      );
 58      let sep_len = identity::ssh_user().len() + 1 + identity::hostname().len();
 59      let _ = write!(out, "  \x1b[2m");
 60      for _ in 0..sep_len {
 61          out.push(icons::BOX_HORIZONTAL);
 62      }
 63      let _ = write!(out, "\x1b[0m\r\n");
 64  
 65      let app_desc = unsafe { &ESP_APP_DESC };
 66      row!(
 67          out,
 68          "33",
 69          "",
 70          "OS",
 71          "\x1b[1m{}\x1b[0m {} ({})",
 72          app_desc.project_name(),
 73          app_desc.version(),
 74          board::PLATFORM
 75      );
 76      row!(
 77          out,
 78          "35",
 79          "",
 80          "Host",
 81          "\x1b[1mESP32-S3\x1b[0m (rev {}.{})",
 82          chip_rev.major,
 83          chip_rev.minor
 84      );
 85      row!(
 86          out,
 87          "35",
 88          "",
 89          "Chassis",
 90          "\x1b[1mesp32s3-devkitc1-N8R8\x1b[0m"
 91      );
 92      row!(
 93          out,
 94          "36",
 95          "",
 96          "Kernel",
 97          "\x1b[1membassy 0.7\x1b[0m / esp-hal 1.0"
 98      );
 99      row!(
100          out,
101          "33",
102          icons::NF_FA_DATABASE,
103          "Built",
104          "\x1b[1m{}\x1b[0m {}",
105          app_desc.date(),
106          app_desc.time()
107      );
108      // Partition info removed — FlashStorage can only be created once and is
109      // owned by the boot code. Display booted partition via a different mechanism
110      // when available.
111  
112      let (d, h, m, s) = (
113          secs / 86400,
114          (secs % 86400) / 3600,
115          (secs % 3600) / 60,
116          secs % 60,
117      );
118      match (d, h) {
119          (1.., _) => row!(
120              out,
121              "34",
122              "",
123              "Uptime",
124              "\x1b[1m{}\x1b[0m days, \x1b[1m{}\x1b[0m hours, \x1b[1m{}\x1b[0m mins, \x1b[1m{}\x1b[0m secs",
125              d,
126              h,
127              m,
128              s
129          ),
130          (_, 1..) => row!(
131              out,
132              "34",
133              "",
134              "Uptime",
135              "\x1b[1m{}\x1b[0m hours, \x1b[1m{}\x1b[0m mins, \x1b[1m{}\x1b[0m secs",
136              h,
137              m,
138              s
139          ),
140          _ => row!(
141              out,
142              "34",
143              "",
144              "Uptime",
145              "\x1b[1m{}\x1b[0m mins, \x1b[1m{}\x1b[0m secs",
146              m,
147              s
148          ),
149      }
150  
151      if let Some(reason) = system::reset_reason() {
152          row!(
153              out,
154              "31",
155              icons::NF_FA_BOLT,
156              "Reset",
157              "\x1b[1m{:?}\x1b[0m",
158              reason
159          );
160      }
161  
162      row!(out, "32", "", "Shell", "\x1b[1mMicroshell\x1b[0m (SSH)");
163      row!(
164          out,
165          "31",
166          "",
167          "CPU",
168          "\x1b[1mXtensa LX7\x1b[0m ({}) @ \x1b[1m{} MHz\x1b[0m",
169          Cpu::COUNT,
170          cpu_freq_mhz
171      );
172      row!(
173          out,
174          "36",
175          "",
176          "RAM",
177          "\x1b[1m{:.2} KiB\x1b[0m / \x1b[1m{:.2} KiB\x1b[0m (\x1b[1;32m{}%\x1b[0m)",
178          heap_used as f32 / 1024.0,
179          heap_total as f32 / 1024.0,
180          heap_pct
181      );
182  
183      if system_snapshot.storage.sd_card_size_mb > 0 {
184          row!(
185              out,
186              "32",
187              "",
188              "Disk",
189              "\x1b[1m{} MiB\x1b[0m / \x1b[1m{} MiB\x1b[0m - {} [\x1b[1m{}\x1b[0m]",
190              0,
191              system_snapshot.storage.sd_card_size_mb,
192              app::sd_card::FS_TYPE,
193              app::sd_card::DEVICE
194          );
195      } else {
196          row!(
197              out,
198              "32",
199              icons::NF_FA_HDD,
200              "Disk",
201              "\x1b[2mnot detected\x1b[0m"
202          );
203      }
204  
205      row!(
206          out,
207          "33",
208          "",
209          "Local IP",
210          "\x1b[1m{}.{}.{}.{}\x1b[0m/24",
211          system_snapshot.network.station.ipv4_address[0],
212          system_snapshot.network.station.ipv4_address[1],
213          system_snapshot.network.station.ipv4_address[2],
214          system_snapshot.network.station.ipv4_address[3]
215      );
216      row!(
217          out,
218          "32",
219          icons::NF_FA_WIFI,
220          "WiFi STA",
221          "{}",
222          if system_snapshot.network.station.is_connected {
223              "\x1b[32mconnected\x1b[0m"
224          } else {
225              "\x1b[31mdisconnected\x1b[0m"
226          }
227      );
228      row!(
229          out,
230          "35",
231          "",
232          "MAC",
233          "\x1b[1m{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}\x1b[0m",
234          mac.as_bytes()[0],
235          mac.as_bytes()[1],
236          mac.as_bytes()[2],
237          mac.as_bytes()[3],
238          mac.as_bytes()[4],
239          mac.as_bytes()[5]
240      );
241  
242      let _ = write!(out, "\r\n");
243  
244      row!(
245          out,
246          "36",
247          icons::NF_FA_SERVER,
248          "Hostname",
249          "\x1b[1m{}\x1b[0m",
250          identity::hostname()
251      );
252      row!(
253          out,
254          "34",
255          icons::NF_FA_GLOBE,
256          "NTP",
257          "\x1b[1m{}\x1b[0m",
258          app::NTP_SERVER
259      );
260      row!(
261          out,
262          "33",
263          icons::NF_FA_PLUG,
264          "Ports",
265          "SSH:\x1b[1m{}\x1b[0m  HTTP:\x1b[1m{}\x1b[0m  OTA:\x1b[1m{}\x1b[0m  Log:\x1b[1m{}\x1b[0m",
266          app::ssh::PORT,
267          app::http::PORT,
268          app::ota::PORT,
269          app::tcp_log::PORT
270      );
271      row!(
272          out,
273          "35",
274          icons::NF_FA_WIFI,
275          "WiFi AP",
276          "\x1b[1m{}\x1b[0m (ch\x1b[1m{}\x1b[0m, {})",
277          system_snapshot.network.access_point.ssid,
278          system_snapshot.network.access_point.channel,
279          system_snapshot.network.access_point.auth_mode
280      );
281  
282      let _ = write!(out, "\r\n");
283  
284      row!(
285          out,
286          "36",
287          icons::NF_FA_COG,
288          "I2C Freq",
289          "\x1b[1m{}\x1b[0m kHz",
290          i2c_status.frequency_khz
291      );
292      row!(
293          out,
294          "31",
295          icons::NF_FA_BOLT,
296          "Power GPIO",
297          "\x1b[1mGPIO{}\x1b[0m",
298          i2c_status.power_gpio
299      );
300      row!(
301          out,
302          "34",
303          icons::NF_FA_COG,
304          "SPI (SD)",
305          "CS:\x1b[1mGPIO{}\x1b[0m  MOSI:\x1b[1mGPIO{}\x1b[0m  SCK:\x1b[1mGPIO{}\x1b[0m  MISO:\x1b[1mGPIO{}\x1b[0m",
306          board::sd_card::CS_GPIO,
307          board::sd_card::MOSI_GPIO,
308          board::sd_card::SCK_GPIO,
309          board::sd_card::MISO_GPIO
310      );
311  
312      for bus in i2c_status.buses.iter() {
313          row!(
314              out,
315              "36",
316              icons::NF_FA_SITEMAP,
317              bus.name,
318              "SDA:\x1b[1mGPIO{}\x1b[0m  SCL:\x1b[1mGPIO{}\x1b[0m",
319              bus.sda_gpio,
320              bus.scl_gpio
321          );
322      }
323  
324      let _ = write!(out, "\r\n");
325  
326      for sensor in sensor_inventory.iter() {
327          let mut val = AllocString::new();
328          let transport = sensor.transport_summary();
329          if let Some(address) = transport.address {
330              let _ = write!(
331                  val,
332                  "\x1b[1m{}\x1b[0m @ {} (\x1b[1m0x{:02X}\x1b[0m)",
333                  sensor.model, transport.bus_name, address
334              );
335          } else {
336              let _ = write!(
337                  val,
338                  "\x1b[1m{}\x1b[0m @ {} (slave \x1b[1m{}\x1b[0m reg \x1b[1m{}\x1b[0m)",
339                  sensor.model,
340                  transport.bus_name,
341                  transport.slave_id.unwrap_or_default(),
342                  transport.register_address.unwrap_or_default()
343              );
344          }
345          row!(out, "35", icons::NF_FA_SIGNAL, sensor.name, "{}", val);
346      }
347  
348      if carbon_dioxide.ok {
349          let _ = write!(out, "\r\n");
350          row!(
351              out,
352              "32",
353              icons::NF_FA_LEAF,
354              "CO2",
355              "\x1b[1;32m{:.1}\x1b[0m ppm",
356              carbon_dioxide.co2_ppm
357          );
358          row!(
359              out,
360              "31",
361              icons::NF_FA_THERMOMETER,
362              "Temperature",
363              "\x1b[1;33m{:.1}\x1b[0m\u{00b0}C",
364              carbon_dioxide.temperature
365          );
366          row!(
367              out,
368              "34",
369              icons::NF_FA_TINT,
370              "Humidity",
371              "\x1b[1;36m{:.1}\x1b[0m%%",
372              carbon_dioxide.humidity
373          );
374      }
375  
376      let _ = write!(out, "\r\n");
377      out
378  }