#[cfg(feature = "native")]
use log::{debug, info, trace};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransportKind {
Serial,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceKind {
Ch340,
Cp210x,
Ftdi,
Prolific,
HiSilicon,
Unknown,
}
pub type UsbDevice = DeviceKind;
const KNOWN_USB_DEVICES: &[(u16, &[u16], DeviceKind)] = &[
(
0x1A86,
&[0x7523, 0x7522, 0x5523, 0x5512, 0x55D4],
DeviceKind::Ch340,
),
(
0x10C4,
&[0xEA60, 0xEA70, 0xEA71, 0xEA63],
DeviceKind::Cp210x,
),
(
0x0403,
&[0x6001, 0x6010, 0x6011, 0x6014, 0x6015],
DeviceKind::Ftdi,
),
(
0x067B,
&[0x2303, 0x23A3, 0x23C3, 0x23D3],
DeviceKind::Prolific,
),
(0x12D1, &[], DeviceKind::HiSilicon),
];
impl DeviceKind {
#[must_use]
pub fn from_vid_pid(vid: u16, pid: u16) -> Self {
for (known_vid, pids, device) in KNOWN_USB_DEVICES {
if vid == *known_vid && (pids.is_empty() || pids.contains(&pid)) {
return *device;
}
}
Self::Unknown
}
pub fn name(&self) -> &'static str {
match self {
Self::Ch340 => "CH340/CH341",
Self::Cp210x => "CP210x",
Self::Ftdi => "FTDI",
Self::Prolific => "PL2303",
Self::HiSilicon => "HiSilicon",
Self::Unknown => "Unknown",
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown)
}
pub fn is_high_priority(&self) -> bool {
matches!(self, Self::HiSilicon | Self::Ch340 | Self::Cp210x)
}
}
#[derive(Debug, Clone)]
pub struct DetectedPort {
pub name: String,
pub transport: TransportKind,
pub device: DeviceKind,
pub vid: Option<u16>,
pub pid: Option<u16>,
pub manufacturer: Option<String>,
pub product: Option<String>,
pub serial: Option<String>,
}
impl DetectedPort {
pub fn is_likely_hisilicon(&self) -> bool {
self.device
.is_known()
}
}
#[cfg(feature = "native")]
pub fn detect_ports() -> Vec<DetectedPort> {
let mut result = Vec::new();
match serialport::available_ports() {
Ok(ports) => {
for port_info in ports {
let mut detected = DetectedPort {
name: port_info
.port_name
.clone(),
transport: TransportKind::Serial,
device: DeviceKind::Unknown,
vid: None,
pid: None,
manufacturer: None,
product: None,
serial: None,
};
if let serialport::SerialPortType::UsbPort(usb_info) = port_info.port_type {
detected.vid = Some(usb_info.vid);
detected.pid = Some(usb_info.pid);
detected.manufacturer = usb_info.manufacturer;
detected.product = usb_info.product;
detected.serial = usb_info.serial_number;
detected.device = DeviceKind::from_vid_pid(usb_info.vid, usb_info.pid);
trace!(
"Found USB port: {} (VID: {:04X}, PID: {:04X}, Device: {:?})",
port_info.port_name, usb_info.vid, usb_info.pid, detected.device
);
}
result.push(detected);
}
},
Err(e) => {
debug!("Failed to enumerate serial ports: {e}");
},
}
result
}
#[cfg(not(feature = "native"))]
pub fn detect_ports() -> Vec<DetectedPort> {
Vec::new()
}
pub fn detect_hisilicon_ports() -> Vec<DetectedPort> {
detect_ports()
.into_iter()
.filter(DetectedPort::is_likely_hisilicon)
.collect()
}
#[cfg(feature = "native")]
pub fn auto_detect_port() -> Result<DetectedPort> {
let ports = detect_ports();
if let Some(port) = ports
.iter()
.find(|p| p.device == DeviceKind::HiSilicon)
{
info!("Auto-detected HiSilicon USB device: {}", port.name);
return Ok(port.clone());
}
if let Some(port) = ports
.iter()
.find(|p| {
p.device
.is_high_priority()
})
{
info!(
"Auto-detected {} USB-UART bridge: {}",
port.device
.name(),
port.name
);
return Ok(port.clone());
}
if let Some(port) = ports
.iter()
.find(|p| {
p.device
.is_known()
})
{
info!(
"Auto-detected {} USB-UART bridge: {}",
port.device
.name(),
port.name
);
return Ok(port.clone());
}
if let Some(port) = ports
.into_iter()
.next()
{
info!("Using first available port: {}", port.name);
return Ok(port);
}
Err(Error::DeviceNotFound)
}
#[cfg(not(feature = "native"))]
pub fn auto_detect_port() -> Result<DetectedPort> {
Err(Error::Unsupported(
"Auto-detection is not available in WASM. Use the Web Serial API to request a port."
.to_string(),
))
}
#[cfg(feature = "native")]
pub fn find_port_by_pattern(pattern: &str) -> Result<DetectedPort> {
let ports = detect_ports();
ports
.into_iter()
.find(|p| {
p.name
.contains(pattern)
})
.ok_or(Error::DeviceNotFound)
}
#[cfg(not(feature = "native"))]
pub fn find_port_by_pattern(_pattern: &str) -> Result<DetectedPort> {
Err(Error::Unsupported(
"Port enumeration is not available in WASM. Use the Web Serial API to request a port."
.to_string(),
))
}
pub fn format_port_list(ports: &[DetectedPort]) -> Vec<String> {
let mut result = Vec::new();
for port in ports {
let device_info = if port
.device
.is_known()
{
format!(
" [{}]",
port.device
.name()
)
} else if let (Some(vid), Some(pid)) = (port.vid, port.pid) {
format!(" [VID:{vid:04X} PID:{pid:04X}]")
} else {
String::new()
};
let product_info = port
.product
.as_ref()
.map(|p| format!(" - {p}"))
.unwrap_or_default();
result.push(format!("{}{}{}", port.name, device_info, product_info));
}
result
}
pub fn list_ports_pretty() -> Vec<String> {
let ports = detect_ports();
format_port_list(&ports)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_device_kind_from_vid_pid() {
assert_eq!(DeviceKind::from_vid_pid(0x1A86, 0x7523), DeviceKind::Ch340);
assert_eq!(DeviceKind::from_vid_pid(0x10C4, 0xEA60), DeviceKind::Cp210x);
assert_eq!(DeviceKind::from_vid_pid(0x0403, 0x6001), DeviceKind::Ftdi);
assert_eq!(
DeviceKind::from_vid_pid(0x067B, 0x2303),
DeviceKind::Prolific
);
assert_eq!(
DeviceKind::from_vid_pid(0x12D1, 0x1234),
DeviceKind::HiSilicon
);
assert_eq!(
DeviceKind::from_vid_pid(0x1234, 0x5678),
DeviceKind::Unknown
);
}
#[test]
fn test_device_kind_is_known() {
assert!(DeviceKind::Ch340.is_known());
assert!(!DeviceKind::Unknown.is_known());
}
#[test]
fn test_detected_port_is_likely_hisilicon() {
let known = DetectedPort {
name: "/dev/ttyUSB0".to_string(),
transport: TransportKind::Serial,
device: DeviceKind::Ch340,
vid: Some(0x1A86),
pid: Some(0x7523),
manufacturer: None,
product: None,
serial: None,
};
assert!(known.is_likely_hisilicon());
let unknown = DetectedPort {
name: "/dev/ttyS0".to_string(),
transport: TransportKind::Serial,
device: DeviceKind::Unknown,
vid: None,
pid: None,
manufacturer: None,
product: None,
serial: None,
};
assert!(!unknown.is_likely_hisilicon());
}
#[test]
fn test_format_port_list() {
let ports = vec![
DetectedPort {
name: "/dev/ttyUSB0".to_string(),
transport: TransportKind::Serial,
device: DeviceKind::Ch340,
vid: Some(0x1A86),
pid: Some(0x7523),
manufacturer: Some("WCH".to_string()),
product: Some("USB-Serial".to_string()),
serial: None,
},
DetectedPort {
name: "/dev/ttyUSB1".to_string(),
transport: TransportKind::Serial,
device: DeviceKind::Unknown,
vid: None,
pid: None,
manufacturer: None,
product: None,
serial: None,
},
];
let formatted = format_port_list(&ports);
assert_eq!(formatted.len(), 2);
assert!(formatted[0].contains("/dev/ttyUSB0"));
assert!(formatted[0].contains("CH340/CH341"));
assert!(formatted[1].contains("/dev/ttyUSB1"));
}
}