diff --git a/espflash/src/cli/mod.rs b/espflash/src/cli/mod.rs index 1f022f1e..47023636 100644 --- a/espflash/src/cli/mod.rs +++ b/espflash/src/cli/mod.rs @@ -1,122 +1,19 @@ +use self::clap::ConnectArgs; /// 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 miette::{Result, WrapErr}; +use serialport::FlowControl; -use self::clap::ConnectArgs; -use crate::cli::config::UsbDevice; +use crate::cli::serial::get_serial_port; use crate::{error::Error, Flasher}; pub mod clap; pub mod config; mod line_endings; pub mod monitor; - -fn get_serial_port(matches: &ConnectArgs, config: &Config) -> Result { - // 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. - // - // 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.to_owned()) - } else if let Some(serial) = &config.connection.serial { - Ok(serial.to_owned()) - } else if let Ok(ports) = detect_usb_serial_ports() { - select_serial_port(ports, &config.usb_device) - } else { - Err(Error::NoSerial) - } -} - -fn detect_usb_serial_ports() -> Result> { - let ports = available_ports().into_diagnostic()?; - let ports = ports - .iter() - .filter_map(|port_info| match port_info.port_type { - SerialPortType::UsbPort(..) => Some(port_info.to_owned()), - _ => None, - }) - .collect::>(); - - Ok(ports) -} - -fn select_serial_port(ports: Vec, devices: &[UsbDevice]) -> Result { - let device_matches = |info| devices.iter().any(|dev| dev.matches(info)); - - 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) => { - let formatted = if device_matches(info) { - port_info.port_name.as_str().bold() - } else { - port_info.port_name.as_str().reset() - }; - if let Some(product) = &info.product { - format!("{} - {}", formatted, product) - } else { - format!("{}", formatted) - } - } - _ => port_info.port_name.clone(), - }) - .collect::>(); - let index = Select::with_theme(&ColorfulTheme::default()) - .items(&port_names) - .default(0) - .interact_opt()? - .ok_or(Error::Canceled)?; - - 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 device_matches(port_info) - || Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt({ - if let Some(product) = &port_info.product { - format!("Use serial port '{}' - {}?", port_name, product) - } else { - format!("Use serial port '{}'?", port_name) - } - }) - .interact_opt()? - .ok_or(Error::Canceled)? - { - Ok(port_name) - } else { - Err(Error::NoSerial) - } - } else { - // No serial ports detected - Err(Error::NoSerial) - } -} +mod serial; pub fn connect(matches: &ConnectArgs, config: &Config) -> Result { let port = get_serial_port(matches, config)?; diff --git a/espflash/src/cli/serial.rs b/espflash/src/cli/serial.rs new file mode 100644 index 00000000..3c3071f3 --- /dev/null +++ b/espflash/src/cli/serial.rs @@ -0,0 +1,131 @@ +use super::config::Config; +use crossterm::style::Stylize; +use dialoguer::{theme::ColorfulTheme, Confirm, Select}; +use miette::{IntoDiagnostic, Result}; +use serialport::{available_ports, SerialPortInfo, SerialPortType}; + +use super::clap::ConnectArgs; +use crate::cli::config::UsbDevice; +use crate::error::Error; + +pub fn get_serial_port(matches: &ConnectArgs, config: &Config) -> Result { + // 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. + // + // 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.to_owned()) + } else if let Some(serial) = &config.connection.serial { + Ok(serial.to_owned()) + } else if let Ok(ports) = detect_usb_serial_ports() { + select_serial_port(ports, &config.usb_device) + } else { + Err(Error::NoSerial) + } +} + +fn detect_usb_serial_ports() -> Result> { + let ports = available_ports().into_diagnostic()?; + let ports = ports + .iter() + .filter_map(|port_info| match port_info.port_type { + SerialPortType::UsbPort(..) => Some(port_info.to_owned()), + _ => None, + }) + .collect::>(); + + Ok(ports) +} + +/// USB UART adapters which are known to be on common dev boards +const KNOWN_DEVICES: &[UsbDevice] = &[ + UsbDevice { + vid: 0x10c4, + pid: 0xea60, + }, // Silicon Labs CP210x UART Bridge + UsbDevice { + vid: 0x1a86, + pid: 0x7523, + }, // QinHeng Electronics CH340 serial converter +]; + +fn select_serial_port( + ports: Vec, + configured_devices: &[UsbDevice], +) -> Result { + let device_matches = |info| { + configured_devices + .iter() + .chain(KNOWN_DEVICES.iter()) + .any(|dev| dev.matches(info)) + }; + + if ports.len() > 1 { + // Multiple serial ports detected + println!( + "Detected {} serial ports. Ports with match a known common dev board are highlighted.\n", + ports.len() + ); + + let port_names = ports + .iter() + .map(|port_info| match &port_info.port_type { + SerialPortType::UsbPort(info) => { + let formatted = if device_matches(info) { + port_info.port_name.as_str().bold() + } else { + port_info.port_name.as_str().reset() + }; + if let Some(product) = &info.product { + format!("{} - {}", formatted, product) + } else { + format!("{}", formatted) + } + } + _ => port_info.port_name.clone(), + }) + .collect::>(); + let index = Select::with_theme(&ColorfulTheme::default()) + .items(&port_names) + .default(0) + .interact_opt()? + .ok_or(Error::Canceled)?; + + 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 device_matches(port_info) + || Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt({ + if let Some(product) = &port_info.product { + format!("Use serial port '{}' - {}?", port_name, product) + } else { + format!("Use serial port '{}'?", port_name) + } + }) + .interact_opt()? + .ok_or(Error::Canceled)? + { + Ok(port_name) + } else { + Err(Error::NoSerial) + } + } else { + // No serial ports detected + Err(Error::NoSerial) + } +}