/ src / tasks / upload.rs
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  }