use crossterm::style::Stylize;
use dialoguer::{theme::ColorfulTheme, Confirm, Select};
use miette::{IntoDiagnostic, Result};
use serialport::{available_ports, SerialPortInfo, SerialPortType, UsbPortInfo};
use super::{config::Config, ConnectOpts};
use crate::{cli::config::UsbDevice, error::Error};
pub fn get_serial_port_info(
matches: &ConnectOpts,
config: &Config,
) -> Result<SerialPortInfo, Error> {
let ports = detect_usb_serial_ports().unwrap_or_default();
if let Some(serial) = &matches.serial {
#[cfg(not(target_os = "windows"))]
let serial = std::fs::canonicalize(serial)?.to_string_lossy().to_string();
find_serial_port(&ports, &serial)
} else if let Some(serial) = &config.connection.serial {
#[cfg(not(target_os = "windows"))]
let serial = std::fs::canonicalize(serial)?.to_string_lossy().to_string();
find_serial_port(&ports, &serial)
} else {
let (port, matches) = select_serial_port(ports, config)?;
match &port.port_type {
SerialPortType::UsbPort(usb_info) if !matches => {
let remember = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Remember this serial port for future use?")
.interact_opt()?
.unwrap_or_default();
if remember {
if let Err(e) = config.save_with(|config| {
config.usb_device.push(UsbDevice {
vid: usb_info.vid,
pid: usb_info.pid,
})
}) {
eprintln!("Failed to save config {:#}", e);
}
}
}
_ => {}
}
Ok(port)
}
}
fn find_serial_port(ports: &[SerialPortInfo], name: &str) -> Result<SerialPortInfo, Error> {
let port_info = ports
.iter()
.find(|port| port.port_name.to_lowercase() == name.to_lowercase());
if let Some(port) = port_info {
Ok(port.to_owned())
} else {
Err(Error::SerialNotFound(name.to_owned()))
}
}
#[cfg(all(target_os = "linux", target_env = "musl"))]
fn detect_usb_serial_ports() -> Result<Vec<SerialPortInfo>> {
use std::{
fs::{read_link, read_to_string},
path::PathBuf,
};
let ports = available_ports().into_diagnostic()?;
let ports = ports
.into_iter()
.filter_map(|port_info| {
let path = PathBuf::from(&port_info.port_name);
let mut parent_dev = path.canonicalize().ok()?;
parent_dev.pop();
parent_dev.pop();
parent_dev.pop();
read_link(parent_dev.join("subsystem"))
.ok()
.filter(|subsystem| subsystem.ends_with("usb"))?;
let interface = read_to_string(parent_dev.join("interface"))
.ok()
.map(|s| s.trim().to_string());
parent_dev.pop();
let vid = read_to_string(parent_dev.join("idVendor")).ok()?;
let pid = read_to_string(parent_dev.join("idProduct")).ok()?;
Some(SerialPortInfo {
port_type: SerialPortType::UsbPort(UsbPortInfo {
vid: u16::from_str_radix(vid.trim(), 16).ok()?,
pid: u16::from_str_radix(pid.trim(), 16).ok()?,
product: interface,
serial_number: None,
manufacturer: None,
}),
port_name: format!("/dev/{}", path.file_name()?.to_str()?),
})
})
.collect::<Vec<_>>();
Ok(ports)
}
#[cfg(not(all(target_os = "linux", target_env = "musl")))]
fn detect_usb_serial_ports() -> Result<Vec<SerialPortInfo>> {
let ports = available_ports().into_diagnostic()?;
let ports = ports
.into_iter()
.filter(|port_info| {
matches!(
&port_info.port_type,
SerialPortType::UsbPort(..) | SerialPortType::Unknown
)
})
.collect::<Vec<_>>();
Ok(ports)
}
const KNOWN_DEVICES: &[UsbDevice] = &[
UsbDevice {
vid: 0x10c4,
pid: 0xea60,
}, UsbDevice {
vid: 0x1a86,
pid: 0x7523,
}, ];
fn select_serial_port(
ports: Vec<SerialPortInfo>,
config: &Config,
) -> Result<(SerialPortInfo, bool), Error> {
let device_matches = |info| {
config
.usb_device
.iter()
.chain(KNOWN_DEVICES.iter())
.any(|dev| dev.matches(info))
};
if ports.len() > 1 {
println!(
"Detected {} serial ports. Ports which 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 {
formatted.to_string()
}
}
_ => 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) => {
let matches = if let SerialPortType::UsbPort(usb_info) = &port_info.port_type {
device_matches(usb_info)
} else {
false
};
Ok((port_info.to_owned(), matches))
}
None => Err(Error::SerialNotFound(
port_names.get(index).unwrap().to_string(),
)),
}
} else if let [port] = ports.as_slice() {
let port_name = port.port_name.clone();
let port_info = match &port.port_type {
SerialPortType::UsbPort(info) => info,
SerialPortType::Unknown => &UsbPortInfo {
vid: 0,
pid: 0,
serial_number: None,
manufacturer: None,
product: None,
},
_ => unreachable!(),
};
if device_matches(port_info) {
Ok((port.to_owned(), true))
} else if confirm_port(&port_name, port_info)? {
Ok((port.to_owned(), false))
} else {
Err(Error::SerialNotFound(port_name))
}
} else {
Err(Error::NoSerial)
}
}
fn confirm_port(port_name: &str, port_info: &UsbPortInfo) -> Result<bool, Error> {
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)
}