Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 5 additions & 108 deletions espflash/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String, Error> {
// 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<Vec<SerialPortInfo>> {
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::<Vec<_>>();

Ok(ports)
}

fn select_serial_port(ports: Vec<SerialPortInfo>, devices: &[UsbDevice]) -> Result<String, Error> {
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::<Vec<_>>();
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<Flasher> {
let port = get_serial_port(matches, config)?;
Expand Down
131 changes: 131 additions & 0 deletions espflash/src/cli/serial.rs
Original file line number Diff line number Diff line change
@@ -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<String, Error> {
// 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<Vec<SerialPortInfo>> {
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::<Vec<_>>();

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<SerialPortInfo>,
configured_devices: &[UsbDevice],
) -> Result<String, Error> {
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::<Vec<_>>();
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)
}
}