From 8d5311eefacf5b96985496d0fe7f404087103151 Mon Sep 17 00:00:00 2001 From: Jesse Braham Date: Mon, 15 Nov 2021 17:53:55 -0800 Subject: [PATCH] Improve serial port detection/selection, allow for VID/PID to be configured --- espflash/src/cli/config.rs | 12 +++- espflash/src/cli/mod.rs | 130 +++++++++++++++++++++++-------------- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/espflash/src/cli/config.rs b/espflash/src/cli/config.rs index e95bfe5..c2c01ad 100644 --- a/espflash/src/cli/config.rs +++ b/espflash/src/cli/config.rs @@ -1,11 +1,14 @@ +use std::fs::read; + use directories_next::ProjectDirs; use serde::Deserialize; -use std::fs::read; #[derive(Debug, Deserialize, Default)] pub struct Config { #[serde(default)] pub connection: Connection, + #[serde(default)] + pub usb_device: UsbDevice, } #[derive(Debug, Deserialize, Default)] @@ -13,11 +16,18 @@ pub struct Connection { pub serial: Option, } +#[derive(Debug, Deserialize, Default)] +pub struct UsbDevice { + pub vid: Option, + pub pid: Option, +} + impl Config { /// Load the config from config file pub fn load() -> Self { let dirs = ProjectDirs::from("rs", "esp", "espflash").unwrap(); let file = dirs.config_dir().join("espflash.toml"); + if let Ok(data) = read(&file) { toml::from_slice(&data).unwrap() } else { diff --git a/espflash/src/cli/mod.rs b/espflash/src/cli/mod.rs index a6bd795..5ef6dee 100644 --- a/espflash/src/cli/mod.rs +++ b/espflash/src/cli/mod.rs @@ -1,75 +1,105 @@ /// CLI utilities shared between espflash and cargo-espflash /// /// No stability guaranties applies +use config::Config; +use crossterm::style::Stylize; +use dialoguer::{theme::ColorfulTheme, Confirm, Select}; +use miette::{IntoDiagnostic, Result, WrapErr}; +use serialport::{available_ports, FlowControl, SerialPortInfo, SerialPortType}; + +use self::clap::ConnectArgs; +use crate::{error::Error, Flasher}; + pub mod clap; pub mod config; mod line_endings; pub mod monitor; -use self::clap::ConnectArgs; -use crate::error::Error; -use crate::Flasher; -use config::Config; -use dialoguer::{theme::ColorfulTheme, Select}; -use miette::{IntoDiagnostic, Result, WrapErr}; -use serialport::{available_ports, FlowControl, SerialPortType}; fn get_serial_port(matches: &ConnectArgs, config: &Config) -> Result { - // The serial port should 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. + // A serial port should be specified either as a command-line argument or in a + // configuration file. In the case that both have been provided the command-line + // argument takes precedence. // - // If neither have been provided: - // a) if there is only one serial port detected, it will be used - // b) if there is more than one serial port detected, the user will be - // prompted to select one or exit + // Users may optionally specify the device's VID and PID in the configuration + // file. If no VID/PID have been provided, the user will always be prompted to + // select a serial device. If some VID/PID have been provided the user will be + // prompted to select a serial device, unless there is only one found and its + // VID/PID matches the configured values. if let Some(serial) = &matches.serial { - Ok(serial.into()) + Ok(serial.to_owned()) } else if let Some(serial) = &config.connection.serial { - Ok(serial.into()) - } else if let Ok(ports) = detect_serial_ports() { - let maybe_serial = if ports.len() > 1 { - println!( - "{} serial ports detected, please select one or press Ctrl+c to exit\n", - ports.len() - ); - let index = Select::with_theme(&ColorfulTheme::default()) - .items(&ports) - .default(0) - .interact() - .unwrap(); - - ports.get(index) - } else { - ports.get(0) - }; - - match maybe_serial { - Some(serial) => Ok(serial.into()), - None => Err(Error::NoSerial), - } + Ok(serial.to_owned()) + } else if let Ok(ports) = detect_usb_serial_ports() { + select_serial_port(ports, config.usb_device.vid, config.usb_device.pid) } else { Err(Error::NoSerial) } } -fn detect_serial_ports() -> Result> { - // Find all available serial ports on the host and filter them down to only - // those which are likely candidates for ESP devices. At this time we are only - // interested in USB devices and no further filtering is being done, however - // this may change. +fn detect_usb_serial_ports() -> Result> { let ports = available_ports().into_diagnostic()?; let ports = ports .iter() - .filter(|&port| matches!(&port.port_type, SerialPortType::UsbPort(..))); - - // Now that we have a vector of candidate serial ports, the only information we - // need from them are the ports' names. - let port_names = ports - .cloned() - .map(|port| port.port_name) + .filter_map(|port_info| match port_info.port_type { + SerialPortType::UsbPort(..) => Some(port_info.to_owned()), + _ => None, + }) .collect::>(); - Ok(port_names) + Ok(ports) +} + +fn select_serial_port( + ports: Vec, + vid: Option, + pid: Option, +) -> Result { + if ports.len() > 1 { + // Multiple serial ports detected + println!( + "Detected {} serial ports. Ports with VID/PID matching configured values are bolded.\n", + ports.len() + ); + + let port_names = ports + .iter() + .map(|port_info| match &port_info.port_type { + SerialPortType::UsbPort(info) if Some(info.vid) == vid && Some(info.pid) == pid => { + format!("{}", port_info.port_name.clone().bold()) + } + _ => port_info.port_name.clone(), + }) + .collect::>(); + let index = Select::with_theme(&ColorfulTheme::default()) + .items(&port_names) + .default(0) + .interact()?; + + match ports.get(index) { + Some(port_info) => Ok(port_info.port_name.to_owned()), + None => Err(Error::NoSerial), + } + } else if let [port] = ports.as_slice() { + // Single serial port detected + let port_name = port.port_name.clone(); + let port_info = match &port.port_type { + SerialPortType::UsbPort(info) => info, + _ => unreachable!(), + }; + + if (Some(port_info.vid) == vid && Some(port_info.pid) == pid) + || Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!("Use serial port '{}'?", port_name)) + .interact()? + { + Ok(port_name) + } else { + Err(Error::NoSerial) + } + } else { + // No serial ports detected + Err(Error::NoSerial) + } } pub fn connect(matches: &ConnectArgs, config: &Config) -> Result {