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 }