Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,25 @@ pub struct ExportArgs {
pub output: Option<PathBuf>,
}

#[derive(Debug, Parser)]
pub struct FlashArgs {
/// Hardware version.
#[arg(long)]
pub revision: u8,

/// Path to the firmware source code or firmware binary archive.
#[arg(long)]
pub input: Option<PathBuf>,

/// Path to a file where to write a firmware dump.
#[arg(long)]
pub output: Option<PathBuf>,

/// Serial number to write on the device.
#[arg(long)]
pub serial: Option<u32>,
}

#[derive(Debug, Parser)]
pub struct BadgesArgs {
/// Full app ID.
Expand Down Expand Up @@ -371,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)]
Expand Down
1 change: 1 addition & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,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(),
}
Expand Down
191 changes: 191 additions & 0 deletions src/commands/flash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
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(root_args: &RuntimeArgs, args: &FlashArgs) -> Result<()> {
if let Some(port) = &root_args.port
&& !port.starts_with("/dev/tty")
{
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(root_args, serial).context("write serial number")?;
}

if is_source(args)? {
println!("⏳️ flashing firmware from source...");
flash_from_source(root_args, args)?;
} else {
// TODO: support installing from file.
// TODO: support downloading and installing a release.
bail!("firmware can only be built from source")
}

println!("✅ flashed");
Ok(())
}

/// Write serial number into flash memory of the device.
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![
"write-bin",
"--skip-update-check",
"--non-interactive",
"--chip",
"esp32s3",
"0x10000",
"/tmp/serial.txt",
];
if let Some(port) = &root_args.port {
cmd_args.push("--port");
cmd_args.push(port);
}
let root = std::env::current_dir()?;
exec_espflash(&root, &cmd_args)?;
Ok(())
}

/// Check if the --input (or the current dir) is the firefly-main or firefly-io source code.
fn is_source(args: &FlashArgs) -> Result<bool> {
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(root_args: &RuntimeArgs, args: &FlashArgs) -> Result<()> {
let root = if let Some(path) = &args.input {
path
} 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) = &root_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 {
// TODO: support saving as gz file
let revision = format!("v{}", args.revision);
let mut cmd_args = vec![
"save-image",
"--features",
&revision,
"--release",
path_to_utf8(output_path)?,
];
cmd_args.extend_from_slice(&shared_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!["erase-parts", "--partition-table", partitions, "otadata"];
cmd_args.extend_from_slice(&shared_args);
exec_espflash(root, &cmd_args).context("erase OTA partition")?;

// Flash the image to the device.
let revision = format!("v{}", args.revision);
let mut cmd_args = vec![
"flash",
"--features",
&revision,
"--release",
"--partition-table",
partitions,
"--target-app-partition",
"factory",
];
cmd_args.extend_from_slice(&shared_args);
exec_espflash(root, &cmd_args).context("flash firmware")?;

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()
}

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).context("read ~/export-esp.sh")?;
let parts: Vec<_> = dotenv_raw.split('"').collect();
if parts.len() == 5 {
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]);
}
}
}

let status = cmd.status().context("run espflash")?;
if !status.success() {
let code = status.code().unwrap_or_default();
bail!("unexpected status code: {code}")
}
Ok(())
}
2 changes: 2 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod cheat;
mod config;
mod emulator;
mod export;
mod flash;
mod import;
mod inspect;
mod logs;
Expand All @@ -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;
Expand Down
20 changes: 5 additions & 15 deletions src/commands/shots.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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);
}

Expand Down Expand Up @@ -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")?;
Expand All @@ -84,13 +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);
copy_file(&src_path, &dst_path).with_context(|| {
format!(
"copy screenshot from {} into {}",
path_to_utf8(&src_path),
path_to_utf8(&dst_path),
)
})?;
copy_file(&src_path, &dst_path).context("copy screenshot")?;
}
Ok(())
}
Expand All @@ -99,7 +94,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 {
Expand Down Expand Up @@ -214,8 +209,3 @@ fn write_chunk<W: Write>(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("???")
}
9 changes: 9 additions & 0 deletions src/fs.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::bail;
use crossterm::style::Stylize;
use std::collections::HashMap;
use std::ffi::OsString;
Expand Down Expand Up @@ -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::*;
Expand Down
11 changes: 2 additions & 9 deletions src/langs.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -621,14 +622,6 @@ fn find_wasm(from_dir: &Path) -> anyhow::Result<PathBuf> {
}
}

/// 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() {
Expand All @@ -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(());
};
Expand Down
Loading