From 2869852e5fdae162746c22fb31f531a68542661a Mon Sep 17 00:00:00 2001 From: Jesse Braham Date: Fri, 3 Sep 2021 11:52:43 -0700 Subject: [PATCH] Rewrite cargo-espflash to use clap instead of pico-args --- cargo-espflash/Cargo.toml | 31 +++-- cargo-espflash/README.md | 25 +++- cargo-espflash/src/main.rs | 273 +++++++++++++++++++++---------------- espflash/Cargo.toml | 2 + espflash/src/chip/mod.rs | 6 +- espflash/src/flasher.rs | 12 +- 6 files changed, 213 insertions(+), 136 deletions(-) diff --git a/cargo-espflash/Cargo.toml b/cargo-espflash/Cargo.toml index cb7185ca..3ac71878 100644 --- a/cargo-espflash/Cargo.toml +++ b/cargo-espflash/Cargo.toml @@ -1,21 +1,34 @@ [package] name = "cargo-espflash" version = "0.1.4" -description = "Cargo subcommand for flashing the ESP8266 and ESP32 over serial" -license = "GPL-2.0" authors = [ "Robin Appelman ", "Jesse Braham ", ] -repository = "https://github.com/esp-rs/espflash" edition = "2018" +description = "Cargo subcommand for flashing Espressif devices over serial" +repository = "https://github.com/esp-rs/espflash" +license = "GPL-2.0" +keywords = [ + "cargo", + "cli", + "embedded", + "esp", + "xtensa", +] +categories = [ + "command-line-utilities", + "development-tools", + "development-tools::cargo-plugins", + "embedded", +] [dependencies] -espflash = { version = "0.1.3", path = "../espflash" } -pico-args = "0.4.0" +anyhow = "1.0" +cargo_metadata = "0.14" +clap = "2.33" +espflash = { version = "*", path = "../espflash" } +guess_host_triple = "0.1" +serde = { version = "1.0", features = ["derive"] } serial = "0.4" -serde = { version = "1", features = ["derive"] } toml = "0.5" -cargo_metadata = "0.14.0" -guess_host_triple = "0.1.2" -anyhow = "1.0" diff --git a/cargo-espflash/README.md b/cargo-espflash/README.md index 334f571d..6952cda3 100644 --- a/cargo-espflash/README.md +++ b/cargo-espflash/README.md @@ -1,13 +1,32 @@ # `cargo-espflash` -_ESP8266_ and _ESP32_ cross-compiler and serial flasher cargo subcommand. +Cross-compiler and serial flasher cargo subcommand for Espressif devices. Currently supports __ESP32__, __ESP32-C3__, and __ESP8266__. Prior to flashing, the project is built using the `build-std` unstable cargo feature. Please refer to the [cargo documentation](https://doc.rust-lang.org/cargo/reference/unstable.html#build-std) for more information. ## Usage -```bash -$ cargo espflash [--board-info] [--ram] [--release] [--example EXAMPLE] [--chip {esp32,esp8266}] +```text +cargo-espflash 0.1.4 +Cargo subcommand for flashing Espressif devices over serial + +USAGE: + cargo espflash [FLAGS] [OPTIONS] [SERIAL] + +FLAGS: + --board-info Display the connected board's information + -h, --help Prints help information + --ram Load the application to RAM instead of Flash + --release Build the application using the release profile + -V, --version Prints version information + +OPTIONS: + --example Example to build and flash + --features Comma delimited list of build features + --speed Baud rate at which to flash target device + +ARGS: + Serial port connected to target device ``` When the `--ram` option is specified, the provided ELF image will be loaded into ram and executed without touching the flash. diff --git a/cargo-espflash/src/main.rs b/cargo-espflash/src/main.rs index d30ca35b..c4a610db 100644 --- a/cargo-espflash/src/main.rs +++ b/cargo-espflash/src/main.rs @@ -1,150 +1,180 @@ -mod cargo_config; - -use std::ffi::OsString; -use std::fs::read; -use std::path::PathBuf; -use std::process::{exit, Command, ExitStatus, Stdio}; - -use crate::cargo_config::has_build_std; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, bail, Context}; use cargo_metadata::Message; +use clap::{App, Arg, SubCommand}; use espflash::{Config, Flasher}; -use pico_args::Arguments; use serial::{BaudRate, SerialPort}; -fn main() -> Result<()> { - let args = parse_args().expect("Unable to parse command-line arguments"); +use std::{ + fs::read, + path::PathBuf, + process::{exit, Command, ExitStatus, Stdio}, + string::ToString, +}; + +mod cargo_config; +use cargo_config::has_build_std; + +fn main() -> anyhow::Result<()> { + let mut app = App::new(env!("CARGO_PKG_NAME")) + .bin_name("cargo") + .subcommand( + SubCommand::with_name("espflash") + .version(env!("CARGO_PKG_VERSION")) + .about(env!("CARGO_PKG_DESCRIPTION")) + .arg( + Arg::with_name("board_info") + .long("board-info") + .help("Display the connected board's information"), + ) + .arg( + Arg::with_name("ram") + .long("ram") + .help("Load the application to RAM instead of Flash"), + ) + .arg( + Arg::with_name("release") + .long("release") + .help("Build the application using the release profile"), + ) + .arg( + Arg::with_name("example") + .long("example") + .takes_value(true) + .value_name("EXAMPLE") + .help("Example to build and flash"), + ) + .arg( + Arg::with_name("features") + .long("features") + .use_delimiter(true) + .takes_value(true) + .value_name("FEATURES") + .help("Comma delimited list of build features"), + ) + .arg( + Arg::with_name("speed") + .long("speed") + .takes_value(true) + .value_name("SPEED") + .help("Baud rate at which to flash target device"), + ) + .arg( + Arg::with_name("serial") + .takes_value(true) + .value_name("SERIAL") + .help("Serial port connected to target device"), + ), + ); + + let matches = app.clone().get_matches(); + let matches = match matches.subcommand_matches("espflash") { + Some(matches) => matches, + None => { + app.print_help()?; + exit(0); + } + }; + let config = Config::load(); - if args.help || (args.serial.is_none() && config.connection.serial.is_none()) { - return usage(); - } + // The serial port must be specified, either as a command-line argument or in + // the cargo configuration file. In the case that both have been provided the + // command-line argument will take precedence. + let port = if let Some(serial) = matches.value_of("serial") { + serial.to_string() + } else if let Some(serial) = config.connection.serial { + serial + } else { + app.print_help()?; + exit(0); + }; - let port = args - .serial - .or(config.connection.serial) - .context("serial port missing")?; + // Only build the application if the '--board-info' flag has not been passed. + let show_board_info = matches.is_present("board_info"); + let path = if !show_board_info { + let release = matches.is_present("release"); + let example = matches.value_of("example"); + let features = matches.value_of("features"); - let speed = args.speed.map(|v| BaudRate::from_speed(v as usize)); + let path = build(release, example, features)?; - // Don't build if we are just querying board info - let path = if !args.board_info { - build(args.release, &args.example, &args.features)? + Some(path) } else { - PathBuf::new() + None }; + // Attempt to open the serial port and set its initial baud rate. + println!("Serial port: {}", port); + println!("Connecting...\n"); let mut serial = serial::open(&port).context(format!("Failed to open serial port {}", port))?; serial.reconfigure(&|settings| { settings.set_baud_rate(BaudRate::Baud115200)?; Ok(()) })?; + // Parse the baud rate if provided as as a command-line argument. + let speed = if let Some(speed) = matches.value_of("speed") { + let speed = speed.parse::()?; + Some(BaudRate::from_speed(speed)) + } else { + None + }; + + // Connect the Flasher to the target device. If the '--board-info' flag has been + // provided, display the board info and terminate the application. let mut flasher = Flasher::connect(serial, speed)?; - if args.board_info { - return board_info(&flasher); + if show_board_info { + board_info(&flasher); + return Ok(()); } - let elf_data = read(&path)?; - - if args.ram { + // Read the ELF data from the build path and load it to the target. + let elf_data = read(path.unwrap())?; + if matches.is_present("ram") { flasher.load_elf_to_ram(&elf_data)?; } else { flasher.load_elf_to_flash(&elf_data)?; } + // We're all done! Ok(()) } -#[derive(Debug)] -struct AppArgs { - help: bool, - board_info: bool, - ram: bool, - release: bool, - example: Option, - features: Option, - chip: Option, - speed: Option, - serial: Option, -} - -#[allow(clippy::unnecessary_wraps)] -fn usage() -> Result<()> { - let usage = "Usage: cargo espflash \ - [--board-info] \ - [--ram] \ - [--release] \ - [--example EXAMPLE] \ - [--chip {{esp32,esp32c3,esp8266}}] \ - [--speed BAUD] \ - "; - - println!("{}", usage); - - Ok(()) -} - -#[allow(clippy::unnecessary_wraps)] -fn board_info(flasher: &Flasher) -> Result<()> { - println!("Chip type: {:?}", flasher.chip()); - println!("Flash size: {:?}", flasher.flash_size()); - - Ok(()) +fn board_info(flasher: &Flasher) { + println!("Chip type: {}", flasher.chip()); + println!("Flash size: {}", flasher.flash_size()); } -fn parse_args() -> Result { - // Skip the command and subcommand (ie. 'cargo espflash') and convert the - // remaining arguments to the expected type. - let args = std::env::args().skip(2).map(OsString::from).collect(); - - let mut args = Arguments::from_vec(args); - - let app_args = AppArgs { - help: args.contains("--help"), - board_info: args.contains("--board-info"), - ram: args.contains("--ram"), - release: args.contains("--release"), - example: args.opt_value_from_str("--example")?, - features: args.opt_value_from_str("--features")?, - chip: args.opt_value_from_str("--chip")?, - speed: args.opt_value_from_str("--speed")?, - serial: args.opt_free_from_str()?, +fn build(release: bool, example: Option<&str>, features: Option<&str>) -> anyhow::Result { + // The 'build-std' unstable cargo feature is required to enable + // cross-compilation. If it has not been set then we cannot build the + // application. + if !has_build_std(".") { + bail!( + "cargo currently requires the unstable 'build-std' feature, ensure \ + that .cargo/config{.toml} has the appropriate options.\n \ + See: https://doc.rust-lang.org/cargo/reference/unstable.html#build-std" + ); }; - Ok(app_args) -} - -fn build(release: bool, example: &Option, features: &Option) -> Result { - let mut args: Vec = vec![]; + // Build the list of arguments to pass to 'cargo build'. + let mut args = vec![]; if release { - args.push("--release".to_string()); + args.push("--release"); } - match example { - Some(example) => { - args.push("--example".to_string()); - args.push(example.to_string()); - } - None => {} + if let Some(example) = example { + args.push("--example"); + args.push(example); } - match features { - Some(features) => { - args.push("--features".to_string()); - args.push(features.to_string()); - } - None => {} + if let Some(features) = features { + args.push("--features"); + args.push(features); } - if !has_build_std(".") { - bail!( - r#"cargo currently requires the unstable build-std, ensure .cargo/config{{.toml}} has the appropriate options. - See: https://doc.rust-lang.org/cargo/reference/unstable.html#build-std"# - ); - }; - + // Invoke the 'cargo build' command, passing our list of arguments. let output = Command::new("cargo") .arg("build") .args(args) @@ -165,8 +195,7 @@ fn build(release: bool, example: &Option, features: &Option) -> Message::CompilerArtifact(artifact) => { if artifact.executable.is_some() { if target_artifact.is_some() { - // We found multiple binary artifacts, - // so we don't know which one to use. + // We found multiple binary artifacts, so we don't know which one to use. bail!("Multiple artifacts found, please specify one with --bin"); } else { target_artifact = Some(artifact); @@ -178,29 +207,31 @@ fn build(release: bool, example: &Option, features: &Option) -> print!("{}", rendered); } } - // Ignore other messages. + // Ignore all other messages. _ => (), } } - // Check if the command succeeded, otherwise return an error. - // Any error messages occuring during the build are shown above, - // when the compiler messages are rendered. + // Check if the command succeeded, otherwise return an error. Any error messages + // occuring during the build are shown above, when the compiler messages are + // rendered. if !output.status.success() { exit_with_process_status(output.status); } - if let Some(artifact) = target_artifact { - let artifact_path = PathBuf::from( - artifact - .executable - .ok_or(anyhow!("artifact executable path is missing"))? - .as_path(), - ); - Ok(artifact_path) - } else { + // If no target artifact was found, we don't have a path to return. + if target_artifact.is_none() { bail!("Artifact not found"); } + + let artifact_path = PathBuf::from( + target_artifact + .unwrap() + .executable + .ok_or_else(|| anyhow!("artifact executable path is missing"))?, + ); + + Ok(artifact_path) } #[cfg(unix)] diff --git a/espflash/Cargo.toml b/espflash/Cargo.toml index 736e8351..69cc760a 100644 --- a/espflash/Cargo.toml +++ b/espflash/Cargo.toml @@ -28,6 +28,8 @@ serde = { version = "1.0", features = ["derive"] } toml = "0.5" directories-next = "2.0.0" color-eyre = "0.5" +strum = "0.21.0" +strum_macros = "0.21.1" [dev-dependencies] pretty_assertions = "0.7.1" diff --git a/espflash/src/chip/mod.rs b/espflash/src/chip/mod.rs index a11325be..9c9af0f1 100644 --- a/espflash/src/chip/mod.rs +++ b/espflash/src/chip/mod.rs @@ -1,4 +1,5 @@ use bytemuck::{bytes_of, Pod, Zeroable}; +use strum_macros::Display; use crate::{ elf::{update_checksum, CodeSegment, FirmwareImage, RomSegment}, @@ -86,10 +87,13 @@ struct ExtendedHeader { append_digest: u8, } -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Display)] pub enum Chip { + #[strum(serialize = "ESP32")] Esp32, + #[strum(serialize = "ESP32-C3")] Esp32c3, + #[strum(serialize = "ESP8266")] Esp8266, } diff --git a/espflash/src/flasher.rs b/espflash/src/flasher.rs index bca76a1f..4e6259cf 100644 --- a/espflash/src/flasher.rs +++ b/espflash/src/flasher.rs @@ -1,6 +1,7 @@ use bytemuck::{__core::time::Duration, bytes_of, Pod, Zeroable}; use indicatif::{ProgressBar, ProgressStyle}; use serial::{BaudRate, SerialPort}; +use strum_macros::Display; use std::{mem::size_of, thread::sleep}; @@ -27,8 +28,8 @@ const MEM_END_TIMEOUT: Duration = Duration::from_millis(50); const SYNC_TIMEOUT: Duration = Duration::from_millis(100); #[derive(Copy, Clone, Debug)] -#[repr(u8)] #[allow(dead_code)] +#[repr(u8)] enum Command { FlashBegin = 0x02, FlashData = 0x03, @@ -69,16 +70,23 @@ impl Command { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Display)] #[allow(dead_code)] #[repr(u8)] pub enum FlashSize { + #[strum(serialize = "256KB")] Flash256Kb = 0x12, + #[strum(serialize = "512KB")] Flash512Kb = 0x13, + #[strum(serialize = "1MB")] Flash1Mb = 0x14, + #[strum(serialize = "2MB")] Flash2Mb = 0x15, + #[strum(serialize = "4MB")] Flash4Mb = 0x16, + #[strum(serialize = "8MB")] Flash8Mb = 0x17, + #[strum(serialize = "16MB")] Flash16Mb = 0x18, FlashRetry = 0xFF, // used to hint that alternate detection should be tried }