/ src / tasks / common.rs
common.rs
  1  use loco_rs::prelude::*;
  2  use serde::Deserialize;
  3  use std::process::Command;
  4  
  5  const RESET: &str = "\x1b[0m";
  6  const BOLD: &str = "\x1b[1m";
  7  const CYAN: &str = "\x1b[36m";
  8  const GREEN: &str = "\x1b[32m";
  9  const YELLOW: &str = "\x1b[33m";
 10  const RED: &str = "\x1b[31m";
 11  
 12  // ─── Firmware settings from config/development.yaml ────────────────────────────
 13  
 14  #[derive(Debug, Deserialize)]
 15  pub struct Settings {
 16      pub firmware: FirmwareSettings,
 17  }
 18  
 19  #[derive(Debug, Deserialize)]
 20  pub struct FirmwareSettings {
 21      pub chip: String,
 22      #[serde(default)]
 23      pub ipv4_address: Option<String>,
 24      #[serde(default = "default_ota_port")]
 25      pub ota_port: u16,
 26      #[serde(default)]
 27      pub bin: Option<String>,
 28      #[serde(default)]
 29      pub package: Option<String>,
 30  }
 31  
 32  fn default_ota_port() -> u16 {
 33      3232
 34  }
 35  
 36  impl FirmwareSettings {
 37      /// Infer the Rust target triple from the chip name.
 38      pub fn target(&self) -> &str {
 39          match self.chip.as_str() {
 40              "esp32" => "xtensa-esp32-none-elf",
 41              "esp32s2" => "xtensa-esp32s2-none-elf",
 42              "esp32s3" => "xtensa-esp32s3-none-elf",
 43              "esp32c3" => "riscv32imc-unknown-none-elf",
 44              "esp32c6" => "riscv32imac-unknown-none-elf",
 45              "esp32h2" => "riscv32imac-unknown-none-elf",
 46              _ => "xtensa-esp32s3-none-elf",
 47          }
 48      }
 49  
 50      pub fn partition_table(&self) -> String {
 51          format!("boards/{}.partitions.csv", self.chip)
 52      }
 53  
 54      pub fn bin_name(&self) -> &str {
 55          self.bin.as_deref().unwrap_or("microvisor")
 56      }
 57  
 58      pub fn package_name(&self) -> &str {
 59          self.package.as_deref().unwrap_or("firmware")
 60      }
 61  
 62      pub fn elf_path(&self) -> String {
 63          format!("target/{}/release/{}", self.target(), self.bin_name())
 64      }
 65  
 66      pub fn device_ip(&self) -> &str {
 67          self.ipv4_address.as_deref().unwrap_or("10.0.0.68")
 68      }
 69  
 70      pub fn ota_endpoint(&self) -> String {
 71          format!("{}:{}", self.device_ip(), self.ota_port)
 72      }
 73  }
 74  
 75  pub fn load_settings(app_context: &AppContext) -> Result<Settings> {
 76      let settings = app_context
 77          .config
 78          .settings
 79          .as_ref()
 80          .ok_or_else(|| Error::Message("missing 'settings' in config YAML".into()))?;
 81  
 82      serde_json::from_value(settings.clone())
 83          .map_err(|e| Error::Message(format!("failed to parse firmware settings: {}", e)))
 84  }
 85  
 86  // ─── OTA protocol constants ────────────────────────────────────────────────────
 87  
 88  pub const OTA_STATUS_READY: u8 = 0xA5;
 89  pub const OTA_STATUS_BEGIN_FAILED: u8 = 0xE1;
 90  
 91  // ─── Output helpers ────────────────────────────────────────────────────────────
 92  
 93  pub fn section(title: &str) {
 94      println!("\n{BOLD}{CYAN}🚀 {title}{RESET}");
 95  }
 96  
 97  pub fn step(index: usize, total: usize, label: &str) {
 98      println!("{BOLD}{CYAN}[{index}/{total}]{RESET} {label}");
 99  }
100  
101  pub fn info(message: &str) {
102      println!("{CYAN}ℹ{RESET} {message}");
103  }
104  
105  pub fn success(message: &str) {
106      println!("{GREEN}✅{RESET} {message}");
107  }
108  
109  pub fn warn(message: &str) {
110      println!("{YELLOW}⚠{RESET} {message}");
111  }
112  
113  pub fn error(message: &str) {
114      println!("{RED}❌{RESET} {message}");
115  }
116  
117  pub fn run_command(program: &str, args: &[&str]) -> Result<()> {
118      let rendered = args.join(" ");
119      println!("{BOLD}$ {program} {rendered}{RESET}");
120  
121      let status = Command::new(program).args(args).status()?;
122      if status.success() {
123          Ok(())
124      } else {
125          Err(Error::Message(format!(
126              "command failed (exit {:?}): {} {}",
127              status.code(),
128              program,
129              rendered
130          )))
131      }
132  }
133  
134  // ─── Shared firmware operations ────────────────────────────────────────────────
135  
136  pub fn build_firmware(firmware: &FirmwareSettings) -> Result<()> {
137      run_command(
138          "cargo",
139          &[
140              "+esp",
141              "build",
142              "--release",
143              "-p",
144              firmware.package_name(),
145              "--bin",
146              firmware.bin_name(),
147              "--config",
148              r#"unstable.build-std=["core","alloc"]"#,
149              "--target",
150              firmware.target(),
151          ],
152      )
153  }