From 8ed29f6d75afa9472493d266cf78b3502762387a Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 11:26:11 +0200 Subject: [PATCH 1/9] DRY path_to_utf8 --- src/commands/shots.rs | 17 +++++++---------- src/fs.rs | 9 +++++++++ src/langs.rs | 11 ++--------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/commands/shots.rs b/src/commands/shots.rs index 13e3af5..0bd5bbb 100644 --- a/src/commands/shots.rs +++ b/src/commands/shots.rs @@ -1,4 +1,5 @@ use crate::args::ShotsDownloadArgs; +use crate::fs::path_to_utf8; use anyhow::{Context, Result, bail}; use std::io::Write; use std::path::{Path, PathBuf}; @@ -20,7 +21,7 @@ pub fn cmd_shots_download(vfs: &Path, args: &ShotsDownloadArgs) -> Result<()> { return download_file(&src_path, &dst_dir); } if src_path.is_dir() { - println!("downloading a dir from {}", path_to_utf8(&src_path)); + println!("downloading a dir from {}", path_to_utf8(&src_path)?); return download_dir(&src_path, &dst_dir); } @@ -67,7 +68,7 @@ fn download_dir(src_dir: &Path, dst_dir: &Path) -> Result<()> { } println!( "⏳️ downloading all files from from {}...", - path_to_utf8(src_dir) + path_to_utf8(src_dir)? ); if !dst_dir.exists() { std::fs::create_dir_all(dst_dir).context("create output dir")?; @@ -84,11 +85,12 @@ fn download_dir(src_dir: &Path, dst_dir: &Path) -> Result<()> { } let dst_file_name = get_output_file_name(&src_path)?; let dst_path = dst_dir.join(dst_file_name); + let src_path_str = path_to_utf8(&src_path)?; + let dst_path_str = path_to_utf8(&dst_path)?; copy_file(&src_path, &dst_path).with_context(|| { format!( "copy screenshot from {} into {}", - path_to_utf8(&src_path), - path_to_utf8(&dst_path), + src_path_str, dst_path_str, ) })?; } @@ -99,7 +101,7 @@ fn download_dir(src_dir: &Path, dst_dir: &Path) -> Result<()> { fn download_file(src_path: &Path, dst_path: &Path) -> Result<()> { println!( "⏳️ downloading a single file from {}...", - path_to_utf8(src_path) + path_to_utf8(src_path)? ); let is_file = has_ext(dst_path, "png"); if dst_path.is_file() || is_file { @@ -214,8 +216,3 @@ fn write_chunk(mut w: W, name: &[u8; 4], data: &[u8]) -> Result<()> { w.write_all(&crc.finalize().to_be_bytes())?; Ok(()) } - -/// Convert a file system path to UTF-8 if possible. -pub fn path_to_utf8(path: &Path) -> &str { - path.to_str().unwrap_or("???") -} diff --git a/src/fs.rs b/src/fs.rs index 8a0a522..80feeb2 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,3 +1,4 @@ +use anyhow::bail; use crossterm::style::Stylize; use std::collections::HashMap; use std::ffi::OsString; @@ -31,6 +32,14 @@ pub fn format_size(size: u64) -> String { } } +/// Convert a file system path to UTF-8 if possible. +pub fn path_to_utf8(path: &Path) -> anyhow::Result<&str> { + match path.to_str() { + Some(path) => Ok(path), + None => bail!("path cannot be converted to UTF-8"), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/langs.rs b/src/langs.rs index f48de50..941a187 100644 --- a/src/langs.rs +++ b/src/langs.rs @@ -1,6 +1,7 @@ use crate::args::BuildArgs; use crate::config::{Config, Lang}; use crate::file_names::BIN; +use crate::fs::path_to_utf8; use crate::wasm::{optimize, strip_custom}; use anyhow::{Context, bail}; use std::env::temp_dir; @@ -621,14 +622,6 @@ fn find_wasm(from_dir: &Path) -> anyhow::Result { } } -/// Convert a file system path to UTF-8 if possible. -pub fn path_to_utf8(path: &Path) -> anyhow::Result<&str> { - match path.to_str() { - Some(path) => Ok(path), - None => bail!("project root path cannot be converted to UTF-8"), - } -} - pub fn run_cmd(cmd: &mut Command) -> anyhow::Result<()> { let status = cmd.status()?; if !status.success() { @@ -642,7 +635,7 @@ pub fn run_cmd(cmd: &mut Command) -> anyhow::Result<()> { pub fn check_installed(lang: &str, bin: &str, arg: &str) -> anyhow::Result<()> { use std::fmt::Write; - let output = Command::new(bin).args([arg]).output(); + let output = Command::new(bin).arg(arg).output(); let Ok(output) = output else { return Ok(()); }; From f51098070bcc8ee448b0af07f507e846cbdba46f Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 15:09:32 +0200 Subject: [PATCH 2/9] flash firmware --- src/args.rs | 26 +++++++ src/cli.rs | 1 + src/commands/flash.rs | 165 ++++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 2 + src/commands/shots.rs | 9 +-- 5 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 src/commands/flash.rs diff --git a/src/args.rs b/src/args.rs index 03d3d0f..ba8a50b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -39,6 +39,9 @@ pub enum Commands { /// Launch firefly-emulator. Emulator(EmulatorArgs), + /// Write firmware on the device. + Flash(FlashArgs), + /// Run tests. #[clap(alias("tests"), alias("pytest"))] Test(TestArgs), @@ -170,6 +173,29 @@ pub struct ExportArgs { pub output: Option, } +#[derive(Debug, Parser)] +pub struct FlashArgs { + /// Hardware version. + #[arg(long)] + pub version: u8, + + /// Path to the firmware source code or archive. + #[arg(long)] + pub input: Option, + + /// Path to serial port for connecting to a running device. + #[arg(long)] + pub port: Option, + + /// Path to a file where to write a firmware dump. + #[arg(long)] + pub output: Option, + + /// Serial number to write on the device. + #[arg(long)] + pub serial: Option, +} + #[derive(Debug, Parser)] pub struct BadgesArgs { /// Full app ID. diff --git a/src/cli.rs b/src/cli.rs index 1be089d..35dbbac 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,6 +13,7 @@ pub fn run_command(vfs: PathBuf, command: &Commands) -> anyhow::Result<()> { New(args) => cmd_new(args), Test(args) => cmd_test(args), Emulator(args) => cmd_emulator(&vfs, args), + Flash(args) => cmd_flash(args), Badges(args) => cmd_badges(&vfs, args), Boards(args) => cmd_boards(&vfs, args), Inspect(args) => cmd_inspect(&vfs, args), diff --git a/src/commands/flash.rs b/src/commands/flash.rs new file mode 100644 index 0000000..bc8a8bf --- /dev/null +++ b/src/commands/flash.rs @@ -0,0 +1,165 @@ +use std::{env::temp_dir, fs, process::Command}; + +use crate::{args::FlashArgs, fs::path_to_utf8}; +use anyhow::{Context, Result, bail}; + +/// `ff flash`: Flash firmware into device or file. +pub fn cmd_flash(args: &FlashArgs) -> Result<()> { + if let Some(port) = &args.port + && !port.starts_with("/dev/") + { + bail!("invalid --port"); + } + if let Some(output) = &args.output + && output.starts_with("/dev/") + { + bail!("invalid --output"); + } + + // Install cargo-espflash. + if !espflash_installed() { + if !cargo_installed() { + bail!("cargo is not installed"); + } + println!("⏳️ installing cargo-espflash..."); + Command::new("cargo") + .args(["install", "cargo-espflash"]) + .output()?; + } + + // If serial number is provided, write it to the device. + if let Some(serial) = args.serial { + println!("⏳️ writing serial number..."); + write_serial(args, serial).context("write serial number")?; + } + + if is_source(args)? { + println!("⏳️ flashing firmware from source..."); + flash_from_source(args)?; + } else { + // TODO: support installing from file. + // TODO: support downloading and installing a release. + bail!("firmware can only be built from source") + } + + // TODO: monitor + println!("✅ flashed"); + Ok(()) +} + +/// Write serial number into flash memory of the device. +fn write_serial(args: &FlashArgs, serial: u32) -> Result<()> { + let serial_path = temp_dir().join("firefly-serial.bin"); + fs::write(serial_path, serial.to_le_bytes()).context("write serial number into temp file")?; + let mut cmd_args: Vec<&str> = vec![ + "espflash", + "write-bin", + "--skip-update-check", + "--chip", + "esp32s3", + "0x10000", + "/tmp/serial.txt", + ]; + if let Some(port) = &args.port { + cmd_args.push("--port"); + cmd_args.push(port); + } + Command::new("cargo").args(&cmd_args).output()?; + Ok(()) +} + +/// Check if the --input (or the current dir) is the firefly-main or firefly-io source code. +fn is_source(args: &FlashArgs) -> Result { + let root = if let Some(path) = &args.input { + path + } else { + &std::env::current_dir().context("detect current dir")? + }; + if root.is_file() { + return Ok(false); + } + let config_path = root.join("Cargo.toml"); + if !config_path.is_file() { + return Ok(false); + } + let config = fs::read_to_string(config_path).context("read Cargo.toml")?; + Ok(config.contains(r#"name = "firefly-main""#) || config.contains(r#"name = "firefly-io""#)) +} + +/// Build firmware from source and flash it to the device. +fn flash_from_source(args: &FlashArgs) -> Result<()> { + // If output path is provided, save the image into the file. + if let Some(output_path) = &args.output { + // TODO: support saving as gz file + let version = format!("v{}", args.version); + let mut cmd_args = vec![ + "espflash", + "save-image", + "--skip-update-check", + "--features", + &version, + "--chip", + "esp32s3", + "--release", + path_to_utf8(output_path)?, + ]; + if let Some(port) = &args.port { + cmd_args.push("--port"); + cmd_args.push(port); + } + Command::new("cargo").args(cmd_args).output()?; + return Ok(()); + } + + // If no output path provided, flash the image to the device. + + Ok(()) +} + +fn cargo_installed() -> bool { + let output = Command::new("cargo").arg("version").output(); + let Ok(output) = output else { + return false; + }; + output.status.success() +} + +fn espflash_installed() -> bool { + let output = Command::new("cargo-espflash").args(["--version"]).output(); + let Ok(output) = output else { + return false; + }; + output.status.success() +} + +// # https://taskfile.dev +// version: "3" +// dotenv: +// - ~/export-esp.sh + +// vars: +// IMAGE: ../../apps/firefly-updates/firefly-main + +// flash: +// desc: Build the firmware and write it to the device connected via USB. +// cmds: +// - task: install-espflash +// - task: write-serial +// # Switch OTA to the factory slot +// - > +// cargo espflash erase-parts +// --partition-table partitions.csv +// otadata +// - > +// cargo espflash flash +// --skip-update-check +// --chip esp32s3 +// --partition-table partitions.csv +// --target-app-partition factory +// --release +// {{.CLI_ARGS}} + +// monitor: +// cmds: +// - task: install-espflash +// - cargo espflash monitor {{.CLI_ARGS}} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 82f04be..a778c1c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,6 +6,7 @@ mod cheat; mod config; mod emulator; mod export; +mod flash; mod import; mod inspect; mod logs; @@ -27,6 +28,7 @@ pub use cheat::cmd_cheat; pub use config::cmd_config_get; pub use emulator::cmd_emulator; pub use export::cmd_export; +pub use flash::cmd_flash; pub use import::cmd_import; pub use inspect::cmd_inspect; pub use logs::cmd_logs; diff --git a/src/commands/shots.rs b/src/commands/shots.rs index 0bd5bbb..2f0ed0d 100644 --- a/src/commands/shots.rs +++ b/src/commands/shots.rs @@ -85,14 +85,7 @@ fn download_dir(src_dir: &Path, dst_dir: &Path) -> Result<()> { } let dst_file_name = get_output_file_name(&src_path)?; let dst_path = dst_dir.join(dst_file_name); - let src_path_str = path_to_utf8(&src_path)?; - let dst_path_str = path_to_utf8(&dst_path)?; - copy_file(&src_path, &dst_path).with_context(|| { - format!( - "copy screenshot from {} into {}", - src_path_str, dst_path_str, - ) - })?; + copy_file(&src_path, &dst_path).context("copy screenshot")?; } Ok(()) } From 41946d6850d8f2690bc9ba7d601d643a87dcffdf Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 15:29:02 +0200 Subject: [PATCH 3/9] flash to the device --- src/args.rs | 2 +- src/commands/flash.rs | 60 ++++++++++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/args.rs b/src/args.rs index ba8a50b..597750f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -177,7 +177,7 @@ pub struct ExportArgs { pub struct FlashArgs { /// Hardware version. #[arg(long)] - pub version: u8, + pub revision: u8, /// Path to the firmware source code or archive. #[arg(long)] diff --git a/src/commands/flash.rs b/src/commands/flash.rs index bc8a8bf..14f8e15 100644 --- a/src/commands/flash.rs +++ b/src/commands/flash.rs @@ -6,7 +6,7 @@ use anyhow::{Context, Result, bail}; /// `ff flash`: Flash firmware into device or file. pub fn cmd_flash(args: &FlashArgs) -> Result<()> { if let Some(port) = &args.port - && !port.starts_with("/dev/") + && !port.starts_with("/dev/tty") { bail!("invalid --port"); } @@ -55,6 +55,7 @@ fn write_serial(args: &FlashArgs, serial: u32) -> Result<()> { "espflash", "write-bin", "--skip-update-check", + "--non-interactive", "--chip", "esp32s3", "0x10000", @@ -91,13 +92,14 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { // If output path is provided, save the image into the file. if let Some(output_path) = &args.output { // TODO: support saving as gz file - let version = format!("v{}", args.version); + let revision = format!("v{}", args.revision); let mut cmd_args = vec![ "espflash", "save-image", "--skip-update-check", + "--non-interactive", "--features", - &version, + &revision, "--chip", "esp32s3", "--release", @@ -111,7 +113,38 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { return Ok(()); } - // If no output path provided, flash the image to the device. + // Switch OTA to the factory slot. + let cmd_args = [ + "espflash", + "erase-parts", + "--partition-table", + "partitions.csv", + "otadata", + ]; + Command::new("cargo").args(cmd_args).output()?; + + // Flash the image to the device. + let revision = format!("v{}", args.revision); + let mut cmd_args = vec![ + "espflash", + "flash", + "--skip-update-check", + "--non-interactive", + "--features", + &revision, + "--chip", + "esp32s3", + "--release", + "--partition-table", + "partitions.csv", + "--target-app-partition", + "factory", + ]; + if let Some(port) = &args.port { + cmd_args.push("--port"); + cmd_args.push(port); + } + Command::new("cargo").args(cmd_args).output()?; Ok(()) } @@ -140,25 +173,6 @@ fn espflash_installed() -> bool { // vars: // IMAGE: ../../apps/firefly-updates/firefly-main -// flash: -// desc: Build the firmware and write it to the device connected via USB. -// cmds: -// - task: install-espflash -// - task: write-serial -// # Switch OTA to the factory slot -// - > -// cargo espflash erase-parts -// --partition-table partitions.csv -// otadata -// - > -// cargo espflash flash -// --skip-update-check -// --chip esp32s3 -// --partition-table partitions.csv -// --target-app-partition factory -// --release -// {{.CLI_ARGS}} - // monitor: // cmds: // - task: install-espflash From 6fe07330434a6457af73a54b4dfdd16b24df61b6 Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 15:38:51 +0200 Subject: [PATCH 4/9] respect --input when building from source --- src/args.rs | 2 +- src/commands/flash.rs | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/args.rs b/src/args.rs index 597750f..8df8969 100644 --- a/src/args.rs +++ b/src/args.rs @@ -179,7 +179,7 @@ pub struct FlashArgs { #[arg(long)] pub revision: u8, - /// Path to the firmware source code or archive. + /// Path to the firmware source code or firmware binary archive. #[arg(long)] pub input: Option, diff --git a/src/commands/flash.rs b/src/commands/flash.rs index 14f8e15..716e33d 100644 --- a/src/commands/flash.rs +++ b/src/commands/flash.rs @@ -89,6 +89,12 @@ fn is_source(args: &FlashArgs) -> Result { /// Build firmware from source and flash it to the device. fn flash_from_source(args: &FlashArgs) -> Result<()> { + let root = if let Some(path) = &args.input { + path + } else { + &std::env::current_dir().context("detect current dir")? + }; + // If output path is provided, save the image into the file. if let Some(output_path) = &args.output { // TODO: support saving as gz file @@ -109,19 +115,27 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { cmd_args.push("--port"); cmd_args.push(port); } - Command::new("cargo").args(cmd_args).output()?; + Command::new("cargo") + .args(cmd_args) + .current_dir(root) + .output()?; return Ok(()); } // Switch OTA to the factory slot. + let partitions_path = root.join("partitions.csv"); + let partitions = path_to_utf8(&partitions_path)?; let cmd_args = [ "espflash", "erase-parts", "--partition-table", - "partitions.csv", + partitions, "otadata", ]; - Command::new("cargo").args(cmd_args).output()?; + Command::new("cargo") + .args(cmd_args) + .current_dir(root) + .output()?; // Flash the image to the device. let revision = format!("v{}", args.revision); @@ -136,7 +150,7 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { "esp32s3", "--release", "--partition-table", - "partitions.csv", + partitions, "--target-app-partition", "factory", ]; @@ -144,7 +158,10 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { cmd_args.push("--port"); cmd_args.push(port); } - Command::new("cargo").args(cmd_args).output()?; + Command::new("cargo") + .args(cmd_args) + .current_dir(root) + .output()?; Ok(()) } From 227eb8c8465ded5813d1f9b0c0917a064b04740c Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 15:44:50 +0200 Subject: [PATCH 5/9] simplify --- src/commands/flash.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/commands/flash.rs b/src/commands/flash.rs index 716e33d..c693b09 100644 --- a/src/commands/flash.rs +++ b/src/commands/flash.rs @@ -94,6 +94,16 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { } else { &std::env::current_dir().context("detect current dir")? }; + let mut shared_args = vec![ + "--skip-update-check", + "--non-interactive", + "--chip", + "esp32s3", + ]; + if let Some(port) = &args.port { + shared_args.push("--port"); + shared_args.push(port); + } // If output path is provided, save the image into the file. if let Some(output_path) = &args.output { @@ -102,19 +112,12 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { let mut cmd_args = vec![ "espflash", "save-image", - "--skip-update-check", - "--non-interactive", "--features", &revision, - "--chip", - "esp32s3", "--release", path_to_utf8(output_path)?, ]; - if let Some(port) = &args.port { - cmd_args.push("--port"); - cmd_args.push(port); - } + cmd_args.extend_from_slice(&shared_args); Command::new("cargo") .args(cmd_args) .current_dir(root) @@ -125,13 +128,14 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { // Switch OTA to the factory slot. let partitions_path = root.join("partitions.csv"); let partitions = path_to_utf8(&partitions_path)?; - let cmd_args = [ + let mut cmd_args = vec![ "espflash", "erase-parts", "--partition-table", partitions, "otadata", ]; + cmd_args.extend_from_slice(&shared_args); Command::new("cargo") .args(cmd_args) .current_dir(root) @@ -142,18 +146,15 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { let mut cmd_args = vec![ "espflash", "flash", - "--skip-update-check", - "--non-interactive", "--features", &revision, - "--chip", - "esp32s3", "--release", "--partition-table", partitions, "--target-app-partition", "factory", ]; + cmd_args.extend_from_slice(&shared_args); if let Some(port) = &args.port { cmd_args.push("--port"); cmd_args.push(port); From 560cc309932f1d130d7d93bac00fa96c7b27e69d Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 15:57:36 +0200 Subject: [PATCH 6/9] use dotenv file --- src/commands/flash.rs | 48 ++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/commands/flash.rs b/src/commands/flash.rs index c693b09..3437d4d 100644 --- a/src/commands/flash.rs +++ b/src/commands/flash.rs @@ -1,4 +1,4 @@ -use std::{env::temp_dir, fs, process::Command}; +use std::{env::temp_dir, fs, path::Path, process::Command}; use crate::{args::FlashArgs, fs::path_to_utf8}; use anyhow::{Context, Result, bail}; @@ -52,7 +52,6 @@ fn write_serial(args: &FlashArgs, serial: u32) -> Result<()> { let serial_path = temp_dir().join("firefly-serial.bin"); fs::write(serial_path, serial.to_le_bytes()).context("write serial number into temp file")?; let mut cmd_args: Vec<&str> = vec![ - "espflash", "write-bin", "--skip-update-check", "--non-interactive", @@ -65,7 +64,8 @@ fn write_serial(args: &FlashArgs, serial: u32) -> Result<()> { cmd_args.push("--port"); cmd_args.push(port); } - Command::new("cargo").args(&cmd_args).output()?; + let root = std::env::current_dir()?; + exec_espflash(&root, &cmd_args)?; Ok(()) } @@ -110,7 +110,6 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { // TODO: support saving as gz file let revision = format!("v{}", args.revision); let mut cmd_args = vec![ - "espflash", "save-image", "--features", &revision, @@ -118,10 +117,7 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { path_to_utf8(output_path)?, ]; cmd_args.extend_from_slice(&shared_args); - Command::new("cargo") - .args(cmd_args) - .current_dir(root) - .output()?; + exec_espflash(root, &cmd_args)?; return Ok(()); } @@ -136,15 +132,11 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { "otadata", ]; cmd_args.extend_from_slice(&shared_args); - Command::new("cargo") - .args(cmd_args) - .current_dir(root) - .output()?; + exec_espflash(root, &cmd_args)?; // Flash the image to the device. let revision = format!("v{}", args.revision); let mut cmd_args = vec![ - "espflash", "flash", "--features", &revision, @@ -155,14 +147,7 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { "factory", ]; cmd_args.extend_from_slice(&shared_args); - if let Some(port) = &args.port { - cmd_args.push("--port"); - cmd_args.push(port); - } - Command::new("cargo") - .args(cmd_args) - .current_dir(root) - .output()?; + exec_espflash(root, &cmd_args)?; Ok(()) } @@ -183,6 +168,27 @@ fn espflash_installed() -> bool { output.status.success() } +fn exec_espflash(root: &Path, cmd_args: &[&str]) -> Result<()> { + let mut cmd = Command::new("cargo"); + let mut cmd = cmd.arg("espflash").args(cmd_args).current_dir(root); + + // Set env vars from ~/export-esp.sh. + if let Some(home) = std::env::home_dir() { + let dotenv_path = home.join("export-esp.sh"); + if dotenv_path.is_file() { + let dotenv_raw = fs::read_to_string(dotenv_path)?; + let parts: Vec<_> = dotenv_raw.split('"').collect(); + if parts.len() == 5 { + cmd = cmd.env("PATH", parts[1]); + cmd = cmd.env("PATH", parts[3]); + } + } + } + + cmd.output()?; + Ok(()) +} + // # https://taskfile.dev // version: "3" // dotenv: From 437a7e502a6af4fecc9d42ea59b4a792e4935db3 Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 17:16:20 +0200 Subject: [PATCH 7/9] make flash a subcmd of runtime, wrap more errors --- src/args.rs | 10 +++------- src/cli.rs | 2 +- src/commands/flash.rs | 44 ++++++++++++++++++++----------------------- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/args.rs b/src/args.rs index 8df8969..cd2ac8b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -39,9 +39,6 @@ pub enum Commands { /// Launch firefly-emulator. Emulator(EmulatorArgs), - /// Write firmware on the device. - Flash(FlashArgs), - /// Run tests. #[clap(alias("tests"), alias("pytest"))] Test(TestArgs), @@ -183,10 +180,6 @@ pub struct FlashArgs { #[arg(long)] pub input: Option, - /// Path to serial port for connecting to a running device. - #[arg(long)] - pub port: Option, - /// Path to a file where to write a firmware dump. #[arg(long)] pub output: Option, @@ -397,6 +390,9 @@ pub enum RuntimeCommands { /// Fetch and print the ID of the running app. Id, + + /// Write firmware on the device. + Flash(FlashArgs), } #[derive(Debug, Parser)] diff --git a/src/cli.rs b/src/cli.rs index 35dbbac..8e23f3f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,7 +13,6 @@ pub fn run_command(vfs: PathBuf, command: &Commands) -> anyhow::Result<()> { New(args) => cmd_new(args), Test(args) => cmd_test(args), Emulator(args) => cmd_emulator(&vfs, args), - Flash(args) => cmd_flash(args), Badges(args) => cmd_badges(&vfs, args), Boards(args) => cmd_boards(&vfs, args), Inspect(args) => cmd_inspect(&vfs, args), @@ -36,6 +35,7 @@ pub fn run_command(vfs: PathBuf, command: &Commands) -> anyhow::Result<()> { RuntimeCommands::Cheat(args) => cmd_cheat(root_args, args), RuntimeCommands::Monitor => cmd_monitor(root_args), RuntimeCommands::Logs => cmd_logs(root_args), + RuntimeCommands::Flash(args) => cmd_flash(root_args, args), }, Vfs => cmd_vfs(), } diff --git a/src/commands/flash.rs b/src/commands/flash.rs index 3437d4d..61b2432 100644 --- a/src/commands/flash.rs +++ b/src/commands/flash.rs @@ -1,11 +1,13 @@ -use std::{env::temp_dir, fs, path::Path, process::Command}; - -use crate::{args::FlashArgs, fs::path_to_utf8}; +use crate::{ + args::{FlashArgs, RuntimeArgs}, + fs::path_to_utf8, +}; use anyhow::{Context, Result, bail}; +use std::{env::temp_dir, fs, path::Path, process::Command}; /// `ff flash`: Flash firmware into device or file. -pub fn cmd_flash(args: &FlashArgs) -> Result<()> { - if let Some(port) = &args.port +pub fn cmd_flash(root_args: &RuntimeArgs, args: &FlashArgs) -> Result<()> { + if let Some(port) = &root_args.port && !port.starts_with("/dev/tty") { bail!("invalid --port"); @@ -30,12 +32,12 @@ pub fn cmd_flash(args: &FlashArgs) -> Result<()> { // If serial number is provided, write it to the device. if let Some(serial) = args.serial { println!("⏳️ writing serial number..."); - write_serial(args, serial).context("write serial number")?; + write_serial(root_args, serial).context("write serial number")?; } if is_source(args)? { println!("⏳️ flashing firmware from source..."); - flash_from_source(args)?; + flash_from_source(root_args, args)?; } else { // TODO: support installing from file. // TODO: support downloading and installing a release. @@ -48,7 +50,7 @@ pub fn cmd_flash(args: &FlashArgs) -> Result<()> { } /// Write serial number into flash memory of the device. -fn write_serial(args: &FlashArgs, serial: u32) -> Result<()> { +fn write_serial(root_args: &RuntimeArgs, serial: u32) -> Result<()> { let serial_path = temp_dir().join("firefly-serial.bin"); fs::write(serial_path, serial.to_le_bytes()).context("write serial number into temp file")?; let mut cmd_args: Vec<&str> = vec![ @@ -60,7 +62,7 @@ fn write_serial(args: &FlashArgs, serial: u32) -> Result<()> { "0x10000", "/tmp/serial.txt", ]; - if let Some(port) = &args.port { + if let Some(port) = &root_args.port { cmd_args.push("--port"); cmd_args.push(port); } @@ -88,7 +90,7 @@ fn is_source(args: &FlashArgs) -> Result { } /// Build firmware from source and flash it to the device. -fn flash_from_source(args: &FlashArgs) -> Result<()> { +fn flash_from_source(root_args: &RuntimeArgs, args: &FlashArgs) -> Result<()> { let root = if let Some(path) = &args.input { path } else { @@ -100,7 +102,7 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { "--chip", "esp32s3", ]; - if let Some(port) = &args.port { + if let Some(port) = &root_args.port { shared_args.push("--port"); shared_args.push(port); } @@ -117,22 +119,16 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { path_to_utf8(output_path)?, ]; cmd_args.extend_from_slice(&shared_args); - exec_espflash(root, &cmd_args)?; + exec_espflash(root, &cmd_args).context("save image")?; return Ok(()); } // Switch OTA to the factory slot. let partitions_path = root.join("partitions.csv"); let partitions = path_to_utf8(&partitions_path)?; - let mut cmd_args = vec![ - "espflash", - "erase-parts", - "--partition-table", - partitions, - "otadata", - ]; + let mut cmd_args = vec!["erase-parts", "--partition-table", partitions, "otadata"]; cmd_args.extend_from_slice(&shared_args); - exec_espflash(root, &cmd_args)?; + exec_espflash(root, &cmd_args).context("erase OTA partition")?; // Flash the image to the device. let revision = format!("v{}", args.revision); @@ -147,7 +143,7 @@ fn flash_from_source(args: &FlashArgs) -> Result<()> { "factory", ]; cmd_args.extend_from_slice(&shared_args); - exec_espflash(root, &cmd_args)?; + exec_espflash(root, &cmd_args).context("flash firmware")?; Ok(()) } @@ -176,16 +172,16 @@ fn exec_espflash(root: &Path, cmd_args: &[&str]) -> Result<()> { if let Some(home) = std::env::home_dir() { let dotenv_path = home.join("export-esp.sh"); if dotenv_path.is_file() { - let dotenv_raw = fs::read_to_string(dotenv_path)?; + let dotenv_raw = fs::read_to_string(dotenv_path).context("read ~/export-esp.sh")?; let parts: Vec<_> = dotenv_raw.split('"').collect(); if parts.len() == 5 { cmd = cmd.env("PATH", parts[1]); - cmd = cmd.env("PATH", parts[3]); + cmd = cmd.env("LIBCLANG_PATH", parts[3]); } } } - cmd.output()?; + cmd.status().context("run espflash")?; Ok(()) } From 0b87ed6ac842cb994baacddedd6eddc9dec2f668 Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 18:12:30 +0200 Subject: [PATCH 8/9] expand path --- src/commands/flash.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/flash.rs b/src/commands/flash.rs index 61b2432..232dbfa 100644 --- a/src/commands/flash.rs +++ b/src/commands/flash.rs @@ -175,13 +175,19 @@ fn exec_espflash(root: &Path, cmd_args: &[&str]) -> Result<()> { let dotenv_raw = fs::read_to_string(dotenv_path).context("read ~/export-esp.sh")?; let parts: Vec<_> = dotenv_raw.split('"').collect(); if parts.len() == 5 { - cmd = cmd.env("PATH", parts[1]); + let (path, _) = parts[1].split_once(':').unwrap(); + let path = format!("{path}:{}", std::env::var("PATH").unwrap()); + cmd = cmd.env("PATH", path); cmd = cmd.env("LIBCLANG_PATH", parts[3]); } } } - cmd.status().context("run espflash")?; + let status = cmd.status().context("run espflash")?; + if !status.success() { + let code = status.code().unwrap_or_default(); + bail!("unexpected status code: {code}") + } Ok(()) } From d2b35008f8e7a1f577be9a59be9327e47225061d Mon Sep 17 00:00:00 2001 From: gram Date: Sat, 26 Sep 2026 18:13:25 +0200 Subject: [PATCH 9/9] cleanup --- src/commands/flash.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/commands/flash.rs b/src/commands/flash.rs index 232dbfa..a25915c 100644 --- a/src/commands/flash.rs +++ b/src/commands/flash.rs @@ -44,7 +44,6 @@ pub fn cmd_flash(root_args: &RuntimeArgs, args: &FlashArgs) -> Result<()> { bail!("firmware can only be built from source") } - // TODO: monitor println!("✅ flashed"); Ok(()) } @@ -190,16 +189,3 @@ fn exec_espflash(root: &Path, cmd_args: &[&str]) -> Result<()> { } Ok(()) } - -// # https://taskfile.dev -// version: "3" -// dotenv: -// - ~/export-esp.sh - -// vars: -// IMAGE: ../../apps/firefly-updates/firefly-main - -// monitor: -// cmds: -// - task: install-espflash -// - cargo espflash monitor {{.CLI_ARGS}}