use crate::error::{Error, Result};
#[cfg(feature = "native")]
use log::{debug, info, trace};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UsbDevice {
Ch340,
Cp210x,
Ftdi,
Prolific,
HiSilicon,
Unknown,
}
const KNOWN_USB_DEVICES: &[(u16, &[u16], UsbDevice)] = &[
(
0x1A86,
&[0x7523, 0x7522, 0x5523, 0x5512, 0x55D4],
UsbDevice::Ch340,
),
(0x10C4, &[0xEA60, 0xEA70, 0xEA71, 0xEA63], UsbDevice::Cp210x),
(
0x0403,
&[
0x6001, 0x6010, 0x6011, 0x6014, 0x6015, ],
UsbDevice::Ftdi,
),
(
0x067B,
&[0x2303, 0x23A3, 0x23C3, 0x23D3],
UsbDevice::Prolific,
),
(0x12D1, &[], UsbDevice::HiSilicon), ];
impl UsbDevice {
#[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 {
if 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 device: UsbDevice,
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(),
device: UsbDevice::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 = UsbDevice::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 == UsbDevice::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_usb_device_from_vid_pid() {
assert_eq!(UsbDevice::from_vid_pid(0x1A86, 0x7523), UsbDevice::Ch340);
assert_eq!(UsbDevice::from_vid_pid(0x1A86, 0x7522), UsbDevice::Ch340);
assert_eq!(UsbDevice::from_vid_pid(0x1A86, 0x5523), UsbDevice::Ch340);
assert_eq!(UsbDevice::from_vid_pid(0x1A86, 0x5512), UsbDevice::Ch340);
assert_eq!(UsbDevice::from_vid_pid(0x1A86, 0x55D4), UsbDevice::Ch340);
assert_eq!(UsbDevice::from_vid_pid(0x10C4, 0xEA60), UsbDevice::Cp210x);
assert_eq!(UsbDevice::from_vid_pid(0x10C4, 0xEA70), UsbDevice::Cp210x);
assert_eq!(UsbDevice::from_vid_pid(0x10C4, 0xEA71), UsbDevice::Cp210x);
assert_eq!(UsbDevice::from_vid_pid(0x10C4, 0xEA63), UsbDevice::Cp210x);
assert_eq!(UsbDevice::from_vid_pid(0x0403, 0x6001), UsbDevice::Ftdi);
assert_eq!(UsbDevice::from_vid_pid(0x0403, 0x6010), UsbDevice::Ftdi);
assert_eq!(UsbDevice::from_vid_pid(0x0403, 0x6011), UsbDevice::Ftdi);
assert_eq!(UsbDevice::from_vid_pid(0x0403, 0x6014), UsbDevice::Ftdi);
assert_eq!(UsbDevice::from_vid_pid(0x0403, 0x6015), UsbDevice::Ftdi);
assert_eq!(UsbDevice::from_vid_pid(0x067B, 0x2303), UsbDevice::Prolific);
assert_eq!(UsbDevice::from_vid_pid(0x067B, 0x23A3), UsbDevice::Prolific);
assert_eq!(UsbDevice::from_vid_pid(0x067B, 0x23C3), UsbDevice::Prolific);
assert_eq!(UsbDevice::from_vid_pid(0x067B, 0x23D3), UsbDevice::Prolific);
assert_eq!(
UsbDevice::from_vid_pid(0x12D1, 0x1234),
UsbDevice::HiSilicon
);
assert_eq!(
UsbDevice::from_vid_pid(0x12D1, 0x0000),
UsbDevice::HiSilicon
);
assert_eq!(
UsbDevice::from_vid_pid(0x12D1, 0xFFFF),
UsbDevice::HiSilicon
);
assert_eq!(UsbDevice::from_vid_pid(0x0000, 0x0000), UsbDevice::Unknown);
assert_eq!(UsbDevice::from_vid_pid(0x1234, 0x5678), UsbDevice::Unknown);
assert_eq!(UsbDevice::from_vid_pid(0xFFFF, 0xFFFF), UsbDevice::Unknown);
assert_eq!(UsbDevice::from_vid_pid(0x1A86, 0x1234), UsbDevice::Unknown);
assert_eq!(UsbDevice::from_vid_pid(0x10C4, 0x0000), UsbDevice::Unknown);
}
#[test]
fn test_usb_device_is_known() {
assert!(UsbDevice::Ch340.is_known());
assert!(UsbDevice::Cp210x.is_known());
assert!(UsbDevice::Ftdi.is_known());
assert!(UsbDevice::Prolific.is_known());
assert!(UsbDevice::HiSilicon.is_known());
assert!(!UsbDevice::Unknown.is_known());
}
#[test]
fn test_usb_device_is_high_priority() {
assert!(UsbDevice::HiSilicon.is_high_priority());
assert!(UsbDevice::Ch340.is_high_priority());
assert!(UsbDevice::Cp210x.is_high_priority());
assert!(!UsbDevice::Ftdi.is_high_priority());
assert!(!UsbDevice::Prolific.is_high_priority());
assert!(!UsbDevice::Unknown.is_high_priority());
}
#[test]
fn test_usb_device_name() {
assert_eq!(UsbDevice::Ch340.name(), "CH340/CH341");
assert_eq!(UsbDevice::Cp210x.name(), "CP210x");
assert_eq!(UsbDevice::Ftdi.name(), "FTDI");
assert_eq!(UsbDevice::Prolific.name(), "PL2303");
assert_eq!(UsbDevice::HiSilicon.name(), "HiSilicon");
assert_eq!(UsbDevice::Unknown.name(), "Unknown");
}
#[test]
fn test_detected_port_is_likely_hisilicon() {
let port_known = DetectedPort {
name: "/dev/ttyUSB0".to_string(),
device: UsbDevice::Ch340,
vid: Some(0x1A86),
pid: Some(0x7523),
manufacturer: None,
product: None,
serial: None,
};
assert!(port_known.is_likely_hisilicon());
let port_unknown = DetectedPort {
name: "/dev/ttyS0".to_string(),
device: UsbDevice::Unknown,
vid: None,
pid: None,
manufacturer: None,
product: None,
serial: None,
};
assert!(!port_unknown.is_likely_hisilicon());
}
#[cfg(feature = "native")]
#[test]
fn test_detect_ports_does_not_panic() {
let ports = detect_ports();
let _ = ports.len();
}
#[cfg(feature = "native")]
#[test]
fn test_detect_hisilicon_ports() {
let ports = detect_hisilicon_ports();
for port in &ports {
assert!(port.device.is_known());
}
}
#[cfg(feature = "native")]
#[test]
fn test_auto_detect_port_does_not_panic() {
let _ = auto_detect_port();
}
#[test]
fn test_format_port_list() {
let ports = vec![
DetectedPort {
name: "/dev/ttyUSB0".to_string(),
device: UsbDevice::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(),
device: UsbDevice::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"));
}
}