Add serial ports config file (#777)

* feat: Add serial ports config file

* fix: Remove unused comment

Co-authored-by: Juraj Sadel <jurajsadel@gmail.com>

* docs: Update readme instructions

* docs: Update changelog

* docs: Fix typo

Co-authored-by: Juraj Sadel <jurajsadel@gmail.com>

* 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 <jurajsadel@gmail.com>
This commit is contained in:
Sergio Gasquez Arcos 2025-05-27 10:39:59 +02:00 committed by GitHub
parent 628947d5c7
commit 818a730bca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 197 additions and 119 deletions

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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 theres 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

View File

@ -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(

View File

@ -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<u32>,
/// Bootloader path
#[serde(default)]
pub bootloader: Option<PathBuf>,
/// Preferred serial port connection information
#[serde(default)]
pub connection: Connection,
/// Partition table path
#[serde(default)]
pub partition_table: Option<PathBuf>,
/// Partition table offset
#[serde(default)]
pub partition_table_offset: Option<u32>,
/// Preferred USB devices
#[serde(default)]
pub usb_device: Vec<UsbDevice>,
/// 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<UsbDevice>,
/// 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<Self> {
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<F: Fn(&mut Self)>(&self, modify_fn: F) -> Result<()> {
let mut copy = self.clone();
modify_fn(&mut copy);
let serialized = toml::to_string(&copy)
let serialized = toml::to_string(&copy.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<PathBuf, Error> {
let local_config = std::env::current_dir()?.join("espflash.toml");
fn project_config_path() -> Result<PathBuf, Error> {
Self::find_config_path("espflash.toml")
}
fn port_config_path() -> Result<PathBuf, Error> {
Self::find_config_path("espflash_ports.toml")
}
fn find_config_path(filename: &str) -> Result<PathBuf, Error> {
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)
}
}

View File

@ -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<SerialPortInfo> = 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(())
}

View File

@ -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<Serial
if let Some(serial) = &matches.port {
let ports = detect_usb_serial_ports(true).unwrap_or_default();
find_serial_port(&ports, serial)
} else if let Some(serial) = &config.connection.serial {
} else if let Some(serial) = &config.port_config.connection.serial {
let ports = detect_usb_serial_ports(true).unwrap_or_default();
find_serial_port(&ports, serial)
} else {
let ports = detect_usb_serial_ports(matches.list_all_ports).unwrap_or_default();
let (port, matches) = select_serial_port(ports, config, matches.confirm_port)?;
let (port, matches) = select_serial_port(ports, &config.port_config, matches.confirm_port)?;
match &port.port_type {
SerialPortType::UsbPort(usb_info) if !matches => {
let remember = Confirm::with_theme(&ColorfulTheme::default())
@ -51,7 +53,7 @@ pub fn serial_port_info(matches: &ConnectArgs, config: &Config) -> Result<Serial
// Allow this operation to fail without terminating the
// application, but inform the user if something goes wrong.
if let Err(e) = config.save_with(|config| {
config.usb_device.push(UsbDevice {
config.port_config.usb_device.push(UsbDevice {
vid: usb_info.vid,
pid: usb_info.pid,
})
@ -139,7 +141,7 @@ const KNOWN_DEVICES: &[UsbDevice] = &[
}, // QinHeng Electronics CH340 serial converter
];
pub(super) fn known_ports_filter(port: &SerialPortInfo, config: &Config) -> 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<SerialPortInfo>,
config: &Config,
config: &PortConfig,
force_confirm_port: bool,
) -> Result<(SerialPortInfo, bool), Error> {
if let [port] = ports