fix: OTA protocol with proper handshake

Previous OTA sent chunks blindly without waiting for firmware ACK.
Now properly implements the protocol:
1. Send "OTA <size>"
2. Wait for "OTA_READY" with 5s timeout
3. Send 4096-byte chunks, wait for "OTA_OK" after each
4. Stop on "OTA_DONE" or abort on "OTA_FAIL"
5. Proper port timeout management

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mae PUGIN 2026-04-07 15:41:21 +02:00
parent 0448efebc9
commit d6e5a7f681

View file

@ -1376,27 +1376,21 @@ fn main() {
let serial = serial.clone(); let serial = serial.clone();
let tx = tx.clone(); let tx = tx.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
use std::io::{Read, Write, BufRead, BufReader};
let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner());
let total = firmware.len(); let total = firmware.len();
let chunk_size = 4096usize; let chunk_size = 4096usize;
// Step 1: Send OTA command // Step 1: Send OTA <size> command
let cmd = format!("OTA {}", total); let cmd = format!("OTA {}", total);
if let Err(e) = ser.send_command(&cmd) { if let Err(e) = ser.send_command(&cmd) {
let _ = tx.send(BgMsg::OtaDone(Err(format!("Send OTA cmd failed: {}", e)))); let _ = tx.send(BgMsg::OtaDone(Err(format!("Send OTA cmd failed: {}", e))));
return; return;
} }
// Step 2: Wait for OTA_READY // Step 2: Wait for OTA_READY (read lines with timeout)
let _ = tx.send(BgMsg::OtaProgress(0.0, "Waiting for OTA_READY...".into())); let _ = tx.send(BgMsg::OtaProgress(0.0, "Waiting for OTA_READY...".into()));
let ready = ser.query_command("").unwrap_or_default();
let got_ready = ready.iter().any(|l| l.contains("OTA_READY"));
if !got_ready {
// Try reading more
std::thread::sleep(std::time::Duration::from_secs(2));
}
// Step 3: Send chunks
let port = match ser.port_mut() { let port = match ser.port_mut() {
Some(p) => p, Some(p) => p,
None => { None => {
@ -1405,28 +1399,77 @@ fn main() {
} }
}; };
use std::io::Write; // Read until we get OTA_READY or timeout
let old_timeout = port.timeout();
let _ = port.set_timeout(std::time::Duration::from_secs(5));
let mut got_ready = false;
let port_clone = port.try_clone().unwrap();
let mut reader = BufReader::new(port_clone);
for _ in 0..20 {
let mut line = String::new();
if reader.read_line(&mut line).is_ok() {
if line.contains("OTA_READY") {
got_ready = true;
break;
}
}
}
drop(reader);
if !got_ready {
let _ = port.set_timeout(old_timeout);
let _ = tx.send(BgMsg::OtaDone(Err("Firmware did not respond OTA_READY".into())));
return;
}
// Step 3: Send chunks and wait for ACK after each
let num_chunks = (total + chunk_size - 1) / chunk_size; let num_chunks = (total + chunk_size - 1) / chunk_size;
let _ = port.set_timeout(std::time::Duration::from_secs(5));
for (i, chunk) in firmware.chunks(chunk_size).enumerate() { for (i, chunk) in firmware.chunks(chunk_size).enumerate() {
// Send chunk
if let Err(e) = port.write_all(chunk) { if let Err(e) = port.write_all(chunk) {
let _ = port.set_timeout(old_timeout);
let _ = tx.send(BgMsg::OtaDone(Err(format!("Write chunk {} failed: {}", i, e)))); let _ = tx.send(BgMsg::OtaDone(Err(format!("Write chunk {} failed: {}", i, e))));
return; return;
} }
let _ = port.flush(); let _ = port.flush();
let progress = (i + 1) as f32 / num_chunks as f32; let progress = (i + 1) as f32 / num_chunks as f32;
let msg = format!("Chunk {}/{} ({} KB / {} KB)", let _ = tx.send(BgMsg::OtaProgress(progress * 0.95, format!(
"Chunk {}/{} ({} KB / {} KB)",
i + 1, num_chunks, i + 1, num_chunks,
((i + 1) * chunk_size).min(total) / 1024, ((i + 1) * chunk_size).min(total) / 1024,
total / 1024); total / 1024
let _ = tx.send(BgMsg::OtaProgress(progress * 0.95, msg)); )));
// Wait for ACK (read with timeout) // Wait for ACK line
std::thread::sleep(std::time::Duration::from_millis(50)); let mut ack_buf = [0u8; 256];
let mut buf = [0u8; 256]; let mut ack = String::new();
let _ = port.read(&mut buf); let start = std::time::Instant::now();
while start.elapsed() < std::time::Duration::from_secs(5) {
match port.read(&mut ack_buf) {
Ok(n) if n > 0 => {
ack.push_str(&String::from_utf8_lossy(&ack_buf[..n]));
if ack.contains("OTA_OK") || ack.contains("OTA_DONE") || ack.contains("OTA_FAIL") {
break;
}
}
_ => std::thread::sleep(std::time::Duration::from_millis(10)),
}
} }
if ack.contains("OTA_FAIL") {
let _ = port.set_timeout(old_timeout);
let _ = tx.send(BgMsg::OtaDone(Err(format!("Firmware error: {}", ack.trim()))));
return;
}
if ack.contains("OTA_DONE") {
break; // Firmware signals all received
}
}
let _ = port.set_timeout(old_timeout);
let _ = tx.send(BgMsg::OtaProgress(1.0, "OTA complete, rebooting...".into())); let _ = tx.send(BgMsg::OtaProgress(1.0, "OTA complete, rebooting...".into()));
let _ = tx.send(BgMsg::OtaDone(Ok(()))); let _ = tx.send(BgMsg::OtaDone(Ok(())));
}); });