Skip to main content

usb_resolver/
lib.rs

1use crossbeam_channel::Sender;
2use serde::{Deserialize, Serialize};
3
4pub mod platform;
5
6#[cfg(target_os = "linux")]
7pub use platform::linux::LinuxMonitor as Monitor;
8#[cfg(target_os = "macos")]
9pub use platform::macos::MacMonitor as Monitor;
10#[cfg(target_os = "windows")]
11pub use platform::windows::WindowsMonitor as Monitor;
12
13/// 设备的唯一标识符(由业务层定义,如 "top_camera")
14pub type RoleId = String;
15
16/// 原始设备信息(底层 OS 扫描到的数据)
17#[derive(Debug, Clone)]
18pub struct RawDeviceInfo {
19    pub vid: u16,
20    pub pid: u16,
21    pub serial: Option<String>,
22    pub port_path: String,               // 平台特定的原生路径字符串
23    pub system_path: String,             // 主路径 (macOS 下优先存 /dev/cu.*)
24    pub system_path_alt: Option<String>, // 新增:备用路径 (macOS 下存 /dev/tty.*)
25}
26
27/// 匹配成功的设备
28#[derive(Debug, Clone)]
29pub struct ResolvedDevice {
30    pub role: RoleId,
31    pub device: RawDeviceInfo,
32    pub match_method: MatchMethod,
33}
34
35#[derive(Debug, Clone, Copy)]
36pub enum MatchMethod {
37    SerialExact,
38    TopologyFallback,
39    PortPath,
40    VidPidOnly,
41}
42
43/// 系统事件
44#[derive(Debug, Clone)]
45pub enum DeviceEvent {
46    /// 一个符合配置要求的设备已上线
47    Attached(RawDeviceInfo),
48    /// 已知的设备已移除
49    Detached(String),
50}
51
52/// 单个设备的配置规则
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct DeviceRule {
55    pub role: RoleId,
56    pub vid: u16,
57    pub pid: u16,
58    pub serial: Option<String>,    // 如果有 Serial,优先匹配
59    pub port_path: Option<String>, // 原生路径,用于回退
60}
61
62impl DeviceRule {
63    /// 核心匹配算法:严格模式
64    pub fn matches(&self, device: &RawDeviceInfo) -> Option<MatchMethod> {
65        // 1. 基础门槛:VID 和 PID 必须匹配 (Strict Mode)
66        if self.vid != device.vid || self.pid != device.pid {
67            return None;
68        }
69
70        // 2. 策略 A: 序列号匹配 (优先级最高)
71        if let (Some(rule_sn), Some(dev_sn)) = (&self.serial, &device.serial)
72            && rule_sn == dev_sn
73        {
74            return Some(MatchMethod::SerialExact);
75        }
76
77        // 2. 其次匹配物理路径
78        if let Some(rule_path) = &self.port_path
79            && rule_path == &device.port_path
80        {
81            return Some(MatchMethod::PortPath);
82        }
83
84        // 3. 如果规则里没写 SN 也没写 Path,则只要 VID/PID 对了就算匹配 (Loose 模式)
85        if self.serial.is_none() && self.port_path.is_none() {
86            return Some(MatchMethod::VidPidOnly);
87        }
88
89        // 3. 策略 B: 拓扑路径匹配 (回退策略)
90        // 只有当序列号没匹配上(或配置没写序列号),且配置了路径时才尝试
91        if let Some(cfg_path) = &self.port_path
92            && cfg_path == &device.port_path
93        {
94            return Some(MatchMethod::TopologyFallback);
95        }
96
97        None
98    }
99}
100
101/// 统一的监听器 trait
102pub trait DeviceMonitor {
103    /// 启动监听,阻塞当前线程或在后台运行,通过 channel 发送事件
104    fn start(&self, tx: Sender<DeviceEvent>) -> anyhow::Result<()>;
105
106    /// 立即扫描一次当前所有设备(用于程序启动时的初始状态构建)
107    fn scan_now(&self) -> anyhow::Result<Vec<RawDeviceInfo>>;
108}
109
110/// 工厂方法:获取当前平台的实现
111pub fn get_monitor() -> Box<dyn DeviceMonitor> {
112    #[cfg(target_os = "linux")]
113    return Box::new(platform::linux::LinuxMonitor::new());
114
115    #[cfg(target_os = "windows")]
116    return Box::new(platform::windows::WindowsMonitor::new());
117
118    #[cfg(target_os = "macos")]
119    return Box::new(platform::macos::MacMonitor::new());
120
121    #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
122    panic!("Unsupported OS");
123}