#[derive(Debug, Clone)]
pub struct FdcanusbInfo {
pub path: String,
pub serial_number: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SocketCanInfo {
pub interface: String,
pub fdcanusb_serial: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct DetectedDevices {
pub fdcanusb: Vec<FdcanusbInfo>,
pub socketcan: Vec<SocketCanInfo>,
}
const FDCANUSB_IDS: &[(u16, u16)] = &[
(0x0483, 0x5740), (0x1209, 0x2323), ];
pub fn is_fdcanusb_path(path: &str) -> bool {
#[cfg(target_os = "linux")]
{
is_fdcanusb_path_sysfs(path)
}
#[cfg(not(target_os = "linux"))]
{
is_fdcanusb_path_ports(path)
}
}
#[cfg(target_os = "linux")]
fn is_fdcanusb_path_sysfs(path: &str) -> bool {
use std::fs;
let Ok(resolved) = fs::canonicalize(path) else {
return false;
};
let Some(tty_name) = resolved.file_name().and_then(|n| n.to_str()) else {
return false;
};
let read_hex = |field: &str| -> Option<u16> {
let sysfs_path = format!("/sys/class/tty/{}/device/../{}", tty_name, field);
u16::from_str_radix(fs::read_to_string(sysfs_path).ok()?.trim(), 16).ok()
};
match (read_hex("idVendor"), read_hex("idProduct")) {
(Some(vid), Some(pid)) => FDCANUSB_IDS.contains(&(vid, pid)),
_ => false,
}
}
#[cfg(not(target_os = "linux"))]
fn is_fdcanusb_path_ports(path: &str) -> bool {
#[cfg(feature = "serialport")]
{
let resolved = std::fs::canonicalize(path).ok();
if let Ok(ports) = serialport::available_ports() {
for port in ports {
let matches = port.port_name == path
|| resolved
.as_deref()
.is_some_and(|r| r == std::path::Path::new(&port.port_name));
if !matches {
continue;
}
if let serialport::SerialPortType::UsbPort(usb_info) = &port.port_type {
return FDCANUSB_IDS.contains(&(usb_info.vid, usb_info.pid));
}
return false;
}
}
true
}
#[cfg(not(feature = "serialport"))]
{
let _ = path;
true
}
}
#[cfg(target_os = "linux")]
pub fn detect_fdcanusbs() -> Vec<FdcanusbInfo> {
use std::fs;
let mut result = Vec::new();
if let Ok(entries) = fs::read_dir("/dev/serial/by-id") {
for entry in entries.filter_map(|e| e.ok()) {
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
if name.to_lowercase().contains("fdcanusb") {
let path = entry.path();
let resolved = fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
let serial_number = name
.rsplit('_')
.next()
.and_then(|s| s.strip_suffix("-if00").or_else(|| s.strip_suffix("-if01")))
.map(String::from);
result.push(FdcanusbInfo {
path: resolved.to_string_lossy().to_string(),
serial_number,
});
}
}
}
if !result.is_empty() {
result.sort_by(|a, b| a.path.cmp(&b.path));
return result;
}
detect_fdcanusbs_by_vid_pid()
}
#[cfg(not(target_os = "linux"))]
pub fn detect_fdcanusbs() -> Vec<FdcanusbInfo> {
detect_fdcanusbs_by_vid_pid()
}
fn detect_fdcanusbs_by_vid_pid() -> Vec<FdcanusbInfo> {
#[cfg(feature = "serialport")]
{
use serialport::available_ports;
let mut result = Vec::new();
if let Ok(ports) = available_ports() {
for port in ports {
if let serialport::SerialPortType::UsbPort(usb_info) = port.port_type {
let is_fdcanusb = FDCANUSB_IDS
.iter()
.any(|(vid, pid)| usb_info.vid == *vid && usb_info.pid == *pid);
if is_fdcanusb {
result.push(FdcanusbInfo {
path: port.port_name,
serial_number: usb_info.serial_number,
});
}
}
}
}
#[cfg(target_os = "macos")]
{
let callouts: std::collections::HashSet<String> = result
.iter()
.filter_map(|info| info.path.strip_prefix("/dev/cu."))
.map(String::from)
.collect();
result.retain(|info| {
!info
.path
.strip_prefix("/dev/tty.")
.is_some_and(|suffix| callouts.contains(suffix))
});
}
result.sort_by(|a, b| a.path.cmp(&b.path));
result
}
#[cfg(not(feature = "serialport"))]
{
Vec::new()
}
}
#[cfg(target_os = "linux")]
pub fn detect_socketcan_interfaces() -> Vec<SocketCanInfo> {
use std::fs;
let mut result = Vec::new();
if let Ok(entries) = fs::read_dir("/sys/class/net") {
for entry in entries.filter_map(|e| e.ok()) {
let ifname = entry.file_name().to_string_lossy().to_string();
let type_path = entry.path().join("type");
if let Ok(type_str) = fs::read_to_string(&type_path) {
if type_str.trim() == "280" {
let fdcanusb_serial = detect_fdcanusb_serial_linux(&ifname);
result.push(SocketCanInfo {
interface: ifname,
fdcanusb_serial,
});
}
}
}
}
result.sort_by(|a, b| a.interface.cmp(&b.interface));
result
}
#[cfg(target_os = "linux")]
fn detect_fdcanusb_serial_linux(ifname: &str) -> Option<String> {
use std::fs;
use std::path::PathBuf;
let device_path = PathBuf::from(format!("/sys/class/net/{}/device", ifname));
let device_path = fs::canonicalize(&device_path).ok()?;
let mut current = device_path.as_path();
for _ in 0..10 {
current = current.parent()?;
let vid_path = current.join("idVendor");
let pid_path = current.join("idProduct");
let serial_path = current.join("serial");
if vid_path.exists() && pid_path.exists() {
let vid_str = fs::read_to_string(&vid_path).ok()?;
let pid_str = fs::read_to_string(&pid_path).ok()?;
let vid = u16::from_str_radix(vid_str.trim(), 16).ok()?;
let pid = u16::from_str_radix(pid_str.trim(), 16).ok()?;
if FDCANUSB_IDS.iter().any(|(v, p)| *v == vid && *p == pid) {
return fs::read_to_string(&serial_path)
.ok()
.map(|s| s.trim().to_string());
}
return None;
}
}
None
}
#[cfg(not(target_os = "linux"))]
pub fn detect_socketcan_interfaces() -> Vec<SocketCanInfo> {
Vec::new()
}
pub fn detect_all_devices() -> DetectedDevices {
DetectedDevices {
fdcanusb: detect_fdcanusbs(),
socketcan: detect_socketcan_interfaces(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_all_devices_runs() {
let devices = detect_all_devices();
let _ = devices;
}
#[test]
fn test_fdcanusb_ids() {
assert!(FDCANUSB_IDS.contains(&(0x0483, 0x5740)));
assert!(FDCANUSB_IDS.contains(&(0x1209, 0x2323)));
}
}