upload.rs
1 use loco_rs::prelude::*; 2 use std::{ 3 io::{Read, Write}, 4 net::TcpStream, 5 path::Path, 6 thread, 7 time::Duration, 8 }; 9 10 use super::common::*; 11 12 pub struct Upload; 13 #[async_trait] 14 impl Task for Upload { 15 fn task(&self) -> TaskInfo { 16 TaskInfo { 17 name: "upload".to_string(), 18 detail: "Build firmware and push OTA update to ESP32-S3".to_string(), 19 } 20 } 21 async fn run(&self, app_context: &AppContext, _vars: &task::Vars) -> Result<()> { 22 let settings = load_settings(app_context)?; 23 let firmware = &settings.firmware; 24 25 section("ESP32-S3 OTA Upload"); 26 27 let endpoint = firmware.ota_endpoint(); 28 info(&format!("target device: {}", endpoint)); 29 30 step(1, 4, "build firmware"); 31 build_firmware(firmware)?; 32 33 step(2, 4, "convert ELF to OTA image"); 34 let elf_path = firmware.elf_path(); 35 let ota_image_path = format!("{}-ota.bin", elf_path); 36 run_command( 37 "espflash", 38 &[ 39 "save-image", 40 "--chip", 41 &firmware.chip, 42 &elf_path, 43 &ota_image_path, 44 ], 45 )?; 46 47 let ota_image = Path::new(&ota_image_path); 48 if !ota_image.exists() { 49 error(&format!("OTA image not found at {}", ota_image_path)); 50 return Err(Error::Message(format!( 51 "OTA image not found at {}", 52 ota_image_path 53 ))); 54 } 55 56 let binary = std::fs::read(ota_image).expect("Failed to read firmware file"); 57 let binary_crc = crc32fast::hash(&binary); 58 info(&format!( 59 "OTA image: {} bytes ({:.2} MiB), CRC32: {:#010x}", 60 binary.len(), 61 binary.len() as f64 / (1024.0 * 1024.0), 62 binary_crc 63 )); 64 65 step(3, 4, &format!("connect to OTA receiver at {}", endpoint)); 66 let mut stream = loop { 67 let mut connected = None; 68 for attempt in 1..=10 { 69 match TcpStream::connect(&endpoint) { 70 Ok(s) => { 71 connected = Some(s); 72 break; 73 } 74 Err(e) => { 75 info(&format!("attempt {}/10: {} (retrying in 2s)", attempt, e)); 76 thread::sleep(Duration::from_secs(2)); 77 } 78 } 79 } 80 if let Some(stream) = connected { 81 break stream; 82 } 83 info(&format!("waiting for OTA receiver at {}...", endpoint)); 84 thread::sleep(Duration::from_secs(2)); 85 }; 86 87 step(4, 4, "send OTA image"); 88 stream 89 .set_read_timeout(Some(Duration::from_secs(10))) 90 .expect("set timeout"); 91 92 stream 93 .write_all(&(binary.len() as u32).to_le_bytes()) 94 .expect("send size"); 95 stream 96 .write_all(&binary_crc.to_le_bytes()) 97 .expect("send CRC"); 98 99 let mut preflight = [0u8; 1]; 100 stream.read_exact(&mut preflight).expect("read preflight"); 101 102 match preflight[0] { 103 OTA_STATUS_READY => info("device preflight OK, streaming payload..."), 104 OTA_STATUS_BEGIN_FAILED => { 105 return Err(Error::Message( 106 "device rejected OTA (ota_begin failed)".into(), 107 )); 108 } 109 code => { 110 return Err(Error::Message(format!( 111 "unexpected preflight status: 0x{code:02x}" 112 ))); 113 } 114 } 115 116 let mut ack = [0u8; 1]; 117 let mut sent_bytes: usize = 0; 118 let mut last_percent: usize = 0; 119 120 for (index, chunk) in binary.chunks(8192).enumerate() { 121 stream.write_all(chunk).expect("send chunk"); 122 stream.read_exact(&mut ack).expect("read ACK"); 123 sent_bytes += chunk.len(); 124 let percent = (sent_bytes * 100) / binary.len(); 125 if percent >= last_percent + 5 || percent == 100 { 126 print!( 127 "\r {:>3}% ({}/{} bytes) chunk {}", 128 percent, 129 sent_bytes, 130 binary.len(), 131 index + 1 132 ); 133 std::io::stdout().flush().unwrap(); 134 last_percent = percent; 135 } 136 } 137 138 println!(); 139 success("OTA complete — device is rebooting into new firmware"); 140 Ok(()) 141 } 142 }