From 818a730bca8e3b540cfd3785b1fdb368e684dbd4 Mon Sep 17 00:00:00 2001 From: Sergio Gasquez Arcos Date: Tue, 27 May 2025 10:39:59 +0200 Subject: [PATCH] Add serial ports config file (#777) * feat: Add serial ports config file * fix: Remove unused comment Co-authored-by: Juraj Sadel * docs: Update readme instructions * docs: Update changelog * docs: Fix typo Co-authored-by: Juraj Sadel * feat: Use a single config struct * feat: Simplify find_config_path methods * docs: Update config documentation * fix: Use a single config struct in cargo-espflash * feat: Simplify save_with method * docs: Improve docstrings * fix: Clippy lint * docs: Add a note about why there are 2 config files --------- Co-authored-by: Juraj Sadel --- CHANGELOG.md | 1 + cargo-espflash/README.md | 83 +++++++++++++++++++-------------- cargo-espflash/src/main.rs | 10 ++-- espflash/README.md | 83 +++++++++++++++++++-------------- espflash/src/bin/espflash.rs | 8 ++-- espflash/src/cli/config.rs | 90 ++++++++++++++++++++++++++---------- espflash/src/cli/mod.rs | 25 ++++++---- espflash/src/cli/serial.rs | 16 ++++--- 8 files changed, 197 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e6701..4c685d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Run some arguments checks for monitoring flags. (#842) - Add support for the ESP32-C5 (#863) - `--after` options now work with `espflash board-info`, `espflash read-flash` and `espflash checksum-md5` (#867) +- Add support for serial port configuration files. (#777) ### Changed diff --git a/cargo-espflash/README.md b/cargo-espflash/README.md index aef2b31..bca43d6 100644 --- a/cargo-espflash/README.md +++ b/cargo-espflash/README.md @@ -17,7 +17,10 @@ Supports the **ESP32**, **ESP32-C2/C3/C5/C6**, **ESP32-H2**, **ESP32-P4**, and * - [Permissions on Linux](#permissions-on-linux) - [Windows Subsystem for Linux](#windows-subsystem-for-linux) - [Bootloader and Partition Table](#bootloader-and-partition-table) -- [Configuration File](#configuration-file) +- [Configuration Files](#configuration-files) + - [`espflash_ports.toml`](#espflash_portstoml) + - [`espflash.toml`](#espflashtoml) + - [Configuration Files Location](#configuration-files-location) - [Configuration Precedence](#configuration-precedence) - [Logging Format](#logging-format) - [Development Kit Support Policy](#development-kit-support-policy) @@ -111,49 +114,61 @@ If the `--bootloader` and/or `--partition-table` options are provided then these [esp-idf-sys]: https://github.com/esp-rs/esp-idf-sys -## Configuration File +## Configuration Files -The configuration file allows you to define various parameters for your application: +There are two configuration files allowing you to define various parameters for your application: -- Serial port: - - By name: - ```toml - [connection] - serial = "/dev/ttyUSB0" - ``` - - By USB VID/PID values: - ```toml - [[usb_device]] - vid = "303a" - pid = "1001" - ``` +- `espflash.toml`: Project configuration +- `espflash_ports.toml`: Port configuration + +The reason to split configuration into two different files is to allow Git ignoring the Serial Port configuration, which is specific to the user (see [#727](https://github.com/esp-rs/espflash/issues/727)). + +### `espflash_ports.toml` + +This file allows you to define the serial port connection parameters: +- By name: + ```toml + [connection] + serial = "/dev/ttyUSB0" + ``` +- By USB VID/PID values: + ```toml + [[usb_device]] + vid = "303a" + pid = "1001" + ``` + +### `espflash.toml` + +This file allows you to define different flash parameters: - Baudrate: - ```toml - baudrate = 460800 - ``` +```toml +baudrate = 460800 +``` - Bootloader: - ```toml - bootloader = "path/to/custom/bootloader.bin" - ``` +```toml +bootloader = "path/to/custom/bootloader.bin" +``` - Partition table - ```toml - partition_table = "path/to/custom/partition-table.bin" - ``` +```toml +partition_table = "path/to/custom/partition-table.bin" +``` - Flash settings - ```toml - [flash] - mode = "qio" - size = "8MB" - frequency = "80MHz" - ``` +```toml +[flash] +mode = "qio" +size = "8MB" +frequency = "80MHz" +``` -You can have a local and/or a global configuration file: +### Configuration Files Location +You can have a local and/or a global configuration file(s): - For local configurations, store the file under the current working directory or in the parent directory (to support Cargo workspaces) with the name `espflash.toml` - Global file location differs based on your operating system: - - Linux: `$HOME/.config/espflash/espflash.toml` - - macOS: `$HOME/Library/Application Support/rs.esp.espflash/espflash.toml` - - Windows: `%APPDATA%\esp\espflash\espflash.toml` + - Linux: `$HOME/.config/espflash/espflash.toml` or `$HOME/.config/espflash/espflash_ports.toml` + - macOS: `$HOME/Library/Application Support/rs.esp.espflash/espflash.toml` or `$HOME/Library/Application Support/rs.esp.espflash/espflash_ports.toml` + - Windows: `%APPDATA%\esp\espflash\espflash.toml` or `%APPDATA%\esp\espflash\espflash_ports.toml` ### Configuration Precedence diff --git a/cargo-espflash/src/main.rs b/cargo-espflash/src/main.rs index 6dc814d..3718fa7 100644 --- a/cargo-espflash/src/main.rs +++ b/cargo-espflash/src/main.rs @@ -242,7 +242,7 @@ fn main() -> Result<()> { Commands::EraseRegion(args) => erase_region(args, &config), Commands::Flash(args) => flash(args, &config), Commands::HoldInReset(args) => hold_in_reset(args, &config), - Commands::ListPorts(args) => list_ports(&args, &config), + Commands::ListPorts(args) => list_ports(&args, &config.port_config), Commands::Monitor(args) => serial_monitor(args, &config), Commands::PartitionTable(args) => partition_table(args), Commands::ReadFlash(args) => read_flash(args, &config), @@ -267,7 +267,7 @@ pub fn erase_parts(args: ErasePartsArgs, config: &Config) -> Result<()> { let partition_table = args .partition_table .as_deref() - .or(config.partition_table.as_deref()); + .or(config.project_config.partition_table.as_deref()); let mut flasher = connect(&args.connect_args, config, false, false)?; let chip = flasher.chip(); @@ -302,7 +302,7 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> { // we'll override the detected (or default) value with this. if let Some(flash_size) = args.build_args.flash_config_args.flash_size { flasher.set_flash_size(flash_size); - } else if let Some(flash_size) = config.flash.size { + } else if let Some(flash_size) = config.project_config.flash.size { flasher.set_flash_size(flash_size); } @@ -329,7 +329,7 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> { let mut flash_config = args.build_args.flash_config_args; flash_config.flash_size = flash_config .flash_size // Use CLI argument if provided - .or(config.flash.size) // If no CLI argument, try the config file + .or(config.project_config.flash.size) // If no CLI argument, try the config file .or_else(|| flasher.flash_detect().ok().flatten()) // Try detecting flash size next .or_else(|| Some(FlashSize::default())); // Otherwise, use a reasonable default value @@ -576,7 +576,7 @@ fn save_image(args: SaveImageArgs, config: &Config) -> Result<()> { let mut flash_config = args.build_args.flash_config_args; flash_config.flash_size = flash_config .flash_size // Use CLI argument if provided - .or(config.flash.size) // If no CLI argument, try the config file + .or(config.project_config.flash.size) // If no CLI argument, try the config file .or_else(|| Some(FlashSize::default())); // Otherwise, use a reasonable default value let flash_data = make_flash_data( diff --git a/espflash/README.md b/espflash/README.md index b93762e..8ce9e7c 100644 --- a/espflash/README.md +++ b/espflash/README.md @@ -19,7 +19,10 @@ Supports the **ESP32**, **ESP32-C2/C3/C5/C6**, **ESP32-H2**, **ESP32-P4**, and * - [Windows Subsystem for Linux](#windows-subsystem-for-linux) - [Cargo Runner](#cargo-runner) - [Using `espflash` as a Library](#using-espflash-as-a-library) -- [Configuration File](#configuration-file) +- [Configuration Files](#configuration-files) + - [`espflash_ports.toml`](#espflash_portstoml) + - [`espflash.toml`](#espflashtoml) + - [Configuration Files Location](#configuration-files-location) - [Configuration Precedence](#configuration-precedence) - [Logging Format](#logging-format) - [Development Kit Support Policy](#development-kit-support-policy) @@ -131,49 +134,61 @@ or `cargo add espflash --no-default-features` We disable the `default-features` to opt-out the `cli` feature, which is enabled by default; you likely will not need any of these types or functions in your application so there’s no use pulling in the extra dependencies. -## Configuration File +## Configuration Files -The configuration file allows you to define various parameters for your application: +There are two configuration files allowing you to define various parameters for your application: -- Serial port: - - By name: - ```toml - [connection] - serial = "/dev/ttyUSB0" - ``` - - By USB VID/PID values: - ```toml - [[usb_device]] - vid = "303a" - pid = "1001" - ``` +- `espflash.toml`: Project configuration +- `espflash_ports.toml`: Port configuration + +The reason to split configuration into two different files is to allow Git ignoring the Serial Port configuration, which is specific to the user (see [#727](https://github.com/esp-rs/espflash/issues/727)). + +### `espflash_ports.toml` + +This file allows you to define the serial port connection parameters: +- By name: + ```toml + [connection] + serial = "/dev/ttyUSB0" + ``` +- By USB VID/PID values: + ```toml + [[usb_device]] + vid = "303a" + pid = "1001" + ``` + +### `espflash.toml` + +This file allows you to define different flash parameters: - Baudrate: - ```toml - baudrate = 460800 - ``` +```toml +baudrate = 460800 +``` - Bootloader: - ```toml - bootloader = "path/to/custom/bootloader.bin" - ``` +```toml +bootloader = "path/to/custom/bootloader.bin" +``` - Partition table - ```toml - partition_table = "path/to/custom/partition-table.bin" - ``` +```toml +partition_table = "path/to/custom/partition-table.bin" +``` - Flash settings - ```toml - [flash] - mode = "qio" - size = "8MB" - frequency = "80MHz" - ``` +```toml +[flash] +mode = "qio" +size = "8MB" +frequency = "80MHz" +``` -You can have a local and/or a global configuration file: +### Configuration Files Location +You can have a local and/or a global configuration file(s): - For local configurations, store the file under the current working directory or in the parent directory (to support Cargo workspaces) with the name `espflash.toml` - Global file location differs based on your operating system: - - Linux: `$HOME/.config/espflash/espflash.toml` - - macOS: `$HOME/Library/Application Support/rs.esp.espflash/espflash.toml` - - Windows: `%APPDATA%\esp\espflash\espflash.toml` + - Linux: `$HOME/.config/espflash/espflash.toml` or `$HOME/.config/espflash/espflash_ports.toml` + - macOS: `$HOME/Library/Application Support/rs.esp.espflash/espflash.toml` or `$HOME/Library/Application Support/rs.esp.espflash/espflash_ports.toml` + - Windows: `%APPDATA%\esp\espflash\espflash.toml` or `%APPDATA%\esp\espflash\espflash_ports.toml` ### Configuration Precedence diff --git a/espflash/src/bin/espflash.rs b/espflash/src/bin/espflash.rs index 2653c50..f0d42e0 100644 --- a/espflash/src/bin/espflash.rs +++ b/espflash/src/bin/espflash.rs @@ -173,7 +173,7 @@ fn main() -> Result<()> { Commands::EraseRegion(args) => erase_region(args, &config), Commands::Flash(args) => flash(args, &config), Commands::HoldInReset(args) => hold_in_reset(args, &config), - Commands::ListPorts(args) => list_ports(&args, &config), + Commands::ListPorts(args) => list_ports(&args, &config.port_config), Commands::Monitor(args) => serial_monitor(args, &config), Commands::PartitionTable(args) => partition_table(args), Commands::ReadFlash(args) => read_flash(args, &config), @@ -224,7 +224,7 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> { // override the detected (or default) value with this. if let Some(flash_size) = args.flash_config_args.flash_size { flasher.set_flash_size(flash_size); - } else if let Some(flash_size) = config.flash.size { + } else if let Some(flash_size) = config.project_config.flash.size { flasher.set_flash_size(flash_size); } @@ -241,7 +241,7 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> { let mut flash_config = args.flash_config_args; flash_config.flash_size = flash_config .flash_size // Use CLI argument if provided - .or(config.flash.size) // If no CLI argument, try the config file + .or(config.project_config.flash.size) // If no CLI argument, try the config file .or_else(|| flasher.flash_detect().ok().flatten()) // Try detecting flash size next .or_else(|| Some(FlashSize::default())); // Otherwise, use a reasonable default value @@ -296,7 +296,7 @@ fn save_image(args: SaveImageArgs, config: &Config) -> Result<()> { let mut flash_config = args.flash_config_args; flash_config.flash_size = flash_config .flash_size // Use CLI argument if provided - .or(config.flash.size) // If no CLI argument, try the config file + .or(config.project_config.flash.size) // If no CLI argument, try the config file .or_else(|| Some(FlashSize::default())); // Otherwise, use a reasonable default value let flash_data = make_flash_data( diff --git a/espflash/src/cli/config.rs b/espflash/src/cli/config.rs index d7a1fcd..20ddd84 100644 --- a/espflash/src/cli/config.rs +++ b/espflash/src/cli/config.rs @@ -72,94 +72,134 @@ impl UsbDevice { } } -/// Deserialized contents of a configuration file +/// Configuration for the project and the port #[derive(Debug, Deserialize, Serialize, Default, Clone)] pub struct Config { + /// Project configuration + pub project_config: ProjectConfig, + /// Port configuration + pub port_config: PortConfig, +} + +/// Project configuration +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct ProjectConfig { /// Baudrate #[serde(default)] pub baudrate: Option, /// Bootloader path #[serde(default)] pub bootloader: Option, - /// Preferred serial port connection information - #[serde(default)] - pub connection: Connection, /// Partition table path #[serde(default)] pub partition_table: Option, /// Partition table offset #[serde(default)] pub partition_table_offset: Option, - /// Preferred USB devices - #[serde(default)] - pub usb_device: Vec, /// Flash settings #[serde(default)] pub flash: FlashSettings, +} + +/// Serial port configuration +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct PortConfig { + /// Preferred serial port connection information + #[serde(default)] + pub connection: Connection, + /// Preferred USB devices + #[serde(default)] + pub usb_device: Vec, /// Path of the file to save the configuration to #[serde(skip)] save_path: PathBuf, } impl Config { - /// Load configuration from the configuration file + /// Load configuration from the configuration files pub fn load() -> Result { - let file = Self::config_path()?; + let project_config_file = Self::project_config_path()?; + let port_config_file = Self::port_config_path()?; - let mut config = if let Ok(data) = read_to_string(&file) { + let project_config = if let Ok(data) = read_to_string(&project_config_file) { toml::from_str(&data).into_diagnostic()? } else { - Self::default() + ProjectConfig::default() }; - if let Some(table) = &config.partition_table { + if let Some(table) = &project_config.partition_table { match table.extension() { Some(ext) if ext == "bin" || ext == "csv" => {} _ => return Err(Error::InvalidPartitionTablePath.into()), } } - if let Some(bootloader) = &config.bootloader { + if let Some(bootloader) = &project_config.bootloader { if bootloader.extension() != Some(OsStr::new("bin")) { return Err(Error::InvalidBootloaderPath.into()); } } - config.save_path = file; - debug!("Config: {:#?}", &config); - Ok(config) + debug!("Config: {:#?}", &project_config); + + let mut port_config = if let Ok(data) = read_to_string(&port_config_file) { + toml::from_str(&data).into_diagnostic()? + } else { + PortConfig::default() + }; + port_config.save_path = port_config_file; + debug!("Port Config: {:#?}", &port_config); + + Ok(Config { + project_config, + port_config, + }) } - /// Save configuration to the configuration file + /// Save port configuration to the configuration file pub fn save_with(&self, modify_fn: F) -> Result<()> { let mut copy = self.clone(); modify_fn(&mut copy); - let serialized = toml::to_string(©) + let serialized = toml::to_string(©.port_config) .into_diagnostic() .wrap_err("Failed to serialize config")?; - create_dir_all(self.save_path.parent().unwrap()) + + create_dir_all(self.port_config.save_path.parent().unwrap()) .into_diagnostic() .wrap_err("Failed to create config directory")?; - write(&self.save_path, serialized) + write(&self.port_config.save_path, serialized) .into_diagnostic() - .wrap_err_with(|| format!("Failed to write config to {}", self.save_path.display())) + .wrap_err_with(|| { + format!( + "Failed to write config to {}", + self.port_config.save_path.display() + ) + }) } - fn config_path() -> Result { - let local_config = std::env::current_dir()?.join("espflash.toml"); + fn project_config_path() -> Result { + Self::find_config_path("espflash.toml") + } + + fn port_config_path() -> Result { + Self::find_config_path("espflash_ports.toml") + } + + fn find_config_path(filename: &str) -> Result { + let local_config = std::env::current_dir()?.join(filename); if local_config.exists() { return Ok(local_config); } if let Some(parent_folder) = std::env::current_dir()?.parent() { - let workspace_config = parent_folder.join("espflash.toml"); + let workspace_config = parent_folder.join(filename); if workspace_config.exists() { return Ok(workspace_config); } } let project_dirs = ProjectDirs::from("rs", "esp", "espflash").unwrap(); - let global_config = project_dirs.config_dir().join("espflash.toml"); + let global_config = project_dirs.config_dir().join(filename); Ok(global_config) } } diff --git a/espflash/src/cli/mod.rs b/espflash/src/cli/mod.rs index d68ef6a..65b5992 100644 --- a/espflash/src/cli/mod.rs +++ b/espflash/src/cli/mod.rs @@ -21,6 +21,7 @@ use std::{ use clap::{Args, ValueEnum}; use clap_complete::Shell; use comfy_table::{Attribute, Cell, Color, Table, modifiers, presets::UTF8_FULL}; +use config::PortConfig; use esp_idf_part::{DataType, Partition, PartitionTable}; use indicatif::{HumanBytes, HumanCount, ProgressBar, style::ProgressStyle}; use log::{debug, info, warn}; @@ -425,7 +426,7 @@ pub fn connect( Ok(Flasher::connect( *Box::new(serial_port), port_info, - args.baud.or(config.baudrate), + args.baud.or(config.project_config.baudrate), !args.no_stub, !no_verify, !no_skip, @@ -468,7 +469,7 @@ pub fn checksum_md5(args: &ChecksumMd5Args, config: &Config) -> Result<()> { Ok(()) } -pub fn list_ports(args: &ListPortsArgs, config: &Config) -> Result<()> { +pub fn list_ports(args: &ListPortsArgs, config: &PortConfig) -> Result<()> { let mut ports: Vec = serial::detect_usb_serial_ports(true)? .into_iter() .filter(|p| args.list_all_ports || serial::known_ports_filter(p, config)) @@ -1022,17 +1023,17 @@ pub fn make_flash_data( let bootloader = image_args .bootloader .as_deref() - .or(config.bootloader.as_deref()) + .or(config.project_config.bootloader.as_deref()) .or(default_bootloader); let partition_table = image_args .partition_table .as_deref() - .or(config.partition_table.as_deref()) + .or(config.project_config.partition_table.as_deref()) .or(default_partition_table); let partition_table_offset = image_args .partition_table_offset - .or(config.partition_table_offset); + .or(config.project_config.partition_table_offset); if let Some(path) = &bootloader { println!("Bootloader: {}", path.display()); @@ -1042,9 +1043,13 @@ pub fn make_flash_data( } let flash_settings = FlashSettings::new( - flash_config_args.flash_mode.or(config.flash.mode), + flash_config_args + .flash_mode + .or(config.project_config.flash.mode), flash_config_args.flash_size, - flash_config_args.flash_freq.or(config.flash.freq), + flash_config_args + .flash_freq + .or(config.project_config.flash.freq), ); FlashData::new( @@ -1083,7 +1088,7 @@ pub fn write_bin(args: WriteBinArgs, config: &Config) -> Result<()> { let Some(partition_table) = args .partition_table .as_deref() - .or(config.partition_table.as_deref()) + .or(config.project_config.partition_table.as_deref()) else { miette::bail!("A partition table is required to resolve partition label"); }; @@ -1148,9 +1153,9 @@ pub fn write_bin(args: WriteBinArgs, config: &Config) -> Result<()> { pub fn reset(args: ConnectArgs, config: &Config) -> Result<()> { let mut args = args.clone(); args.no_stub = true; - let mut flash = connect(&args, config, true, true)?; + let mut flasher = connect(&args, config, true, true)?; info!("Resetting target device"); - flash.connection().reset()?; + flasher.connection().reset()?; Ok(()) } diff --git a/espflash/src/cli/serial.rs b/espflash/src/cli/serial.rs index 2c62b88..171b58b 100644 --- a/espflash/src/cli/serial.rs +++ b/espflash/src/cli/serial.rs @@ -9,7 +9,10 @@ use serialport::{SerialPortInfo, SerialPortType, available_ports}; use crate::{ Error, - cli::{Config, ConnectArgs, config::UsbDevice}, + cli::{ + ConnectArgs, + config::{Config, PortConfig, UsbDevice}, + }, }; /// Return the information of a serial port taking into account the different @@ -33,13 +36,12 @@ pub fn serial_port_info(matches: &ConnectArgs, config: &Config) -> Result { let remember = Confirm::with_theme(&ColorfulTheme::default()) @@ -51,7 +53,7 @@ pub fn serial_port_info(matches: &ConnectArgs, config: &Config) -> Result bool { +pub(super) fn known_ports_filter(port: &SerialPortInfo, config: &PortConfig) -> bool { // Does this port match a known one? match &port.port_type { SerialPortType::UsbPort(info) => config @@ -154,7 +156,7 @@ pub(super) fn known_ports_filter(port: &SerialPortInfo, config: &Config) -> bool /// Ask the user to select a serial port from a list of detected serial ports. fn select_serial_port( mut ports: Vec, - config: &Config, + config: &PortConfig, force_confirm_port: bool, ) -> Result<(SerialPortInfo, bool), Error> { if let [port] = ports