use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::Result;
use tokio::sync::broadcast;
use crate::kernel::event::{
BoardEvent, ConnectionEvent, DeviceInfo, DisconnectReason, ErrorEvent, ReconnectEvent,
ReconnectState,
};
use crate::kernel::protocol_hid::*;
use crate::kernel::types::ConnectionType;
#[cfg(feature = "usb")]
use {
crate::kernel::types::is_usb_audio_device_name,
crate::runtime::usb::device_manager::{DeviceConnection, DeviceManager},
crate::runtime::usb::monitor::{HidMonitor, MonitorConfig},
cpal::traits::{DeviceTrait, HostTrait},
hidapi::{BusType, HidApi},
};
#[cfg(feature = "ble")]
use crate::runtime::ble::gatt_client::VendorGattClient;
pub(crate) async fn spawn_blocking_with_runloop<F, T>(f: F) -> std::thread::Result<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
#[cfg(target_os = "macos")]
{
hid_runloop_thread::execute(f)
}
#[cfg(not(target_os = "macos"))]
{
match tokio::task::spawn_blocking(f).await {
Ok(value) => Ok(value),
Err(join_err) => match join_err.try_into_panic() {
Ok(payload) => Err(payload),
Err(join_err) => {
Err(Box::new(join_err.to_string()) as Box<dyn std::any::Any + Send>)
}
},
}
}
}
#[cfg(target_os = "macos")]
mod hid_runloop_thread {
use std::sync::Mutex;
type Job = Box<dyn FnOnce() + Send + 'static>;
static JOB_QUEUE: Mutex<Vec<Job>> = Mutex::new(Vec::new());
#[repr(C)]
struct CFRunLoopTimerContext {
version: isize,
info: *mut std::ffi::c_void,
retain: Option<unsafe extern "C" fn(*const std::ffi::c_void) -> *const std::ffi::c_void>,
release: Option<unsafe extern "C" fn(*const std::ffi::c_void)>,
copy_description:
Option<unsafe extern "C" fn(*const std::ffi::c_void) -> *mut std::ffi::c_void>,
}
extern "C" {
fn CFRunLoopGetCurrent() -> *mut std::ffi::c_void;
fn CFRunLoopRun();
fn CFRunLoopTimerCreate(
allocator: *mut std::ffi::c_void,
fire_date: f64,
interval: f64,
flags: u64,
order: u64,
callout: extern "C" fn(*mut std::ffi::c_void, *mut std::ffi::c_void),
context: *mut CFRunLoopTimerContext,
) -> *mut std::ffi::c_void;
fn CFRunLoopAddTimer(
rl: *mut std::ffi::c_void,
timer: *mut std::ffi::c_void,
mode: *const std::ffi::c_void,
);
fn CFAbsoluteTimeGetCurrent() -> f64;
static kCFRunLoopCommonModes: *const std::ffi::c_void;
}
extern "C" fn timer_callback(_timer: *mut std::ffi::c_void, _info: *mut std::ffi::c_void) {
let jobs: Vec<Job> = std::mem::take(&mut *JOB_QUEUE.lock().unwrap());
for job in jobs {
job();
}
}
pub fn execute<F, T>(f: F) -> std::thread::Result<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
static RUNLOOP_STARTED: std::sync::OnceLock<
Result<(), Box<dyn std::error::Error + Send + Sync>>,
> = std::sync::OnceLock::new();
let init = RUNLOOP_STARTED.get_or_init(|| {
std::thread::Builder::new()
.name("hid-runloop".to_string())
.spawn(|| unsafe {
let rl = CFRunLoopGetCurrent();
let now = CFAbsoluteTimeGetCurrent();
let timer = CFRunLoopTimerCreate(
std::ptr::null_mut(),
now,
0.005, 0,
0,
timer_callback,
&CFRunLoopTimerContext {
version: 0,
info: std::ptr::null_mut(),
retain: None,
release: None,
copy_description: None,
} as *const _ as *mut _,
);
if !timer.is_null() {
CFRunLoopAddTimer(rl, timer, kCFRunLoopCommonModes);
}
CFRunLoopRun();
})
.map(|_| ())
.map_err(|e| {
log::error!(
target: "hotplug",
"HID runloop 线程创建失败: {} (USB 设备路径将不可用)",
e
);
Box::new(e) as Box<dyn std::error::Error + Send + Sync>
})
});
if let Err(e) = init {
return Err(Box::new(std::io::Error::other(e.to_string())));
}
let (tx, rx) = std::sync::mpsc::channel::<std::thread::Result<T>>();
JOB_QUEUE.lock().unwrap().push(Box::new(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
let _ = tx.send(result);
}));
rx.recv().unwrap_or_else(|e| Err(Box::new(e)))
}
}
#[derive(Debug, Clone)]
pub struct HotplugConfig {
pub retry_interval: Duration,
pub check_interval: Duration,
}
impl Default for HotplugConfig {
fn default() -> Self {
Self {
retry_interval: Duration::from_secs(2),
check_interval: Duration::from_secs(5),
}
}
}
#[cfg(feature = "usb")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HidDetectionDecision {
Connected,
ProbeMode,
NotConnected,
}
#[cfg(feature = "usb")]
fn has_target_usb_audio<I>(device_names: I) -> bool
where
I: IntoIterator,
I::Item: AsRef<str>,
{
device_names
.into_iter()
.any(|name| is_usb_audio_device_name(name.as_ref()))
}
#[cfg(feature = "usb")]
fn probe_target_usb_audio() -> bool {
let devices = match cpal::default_host().devices() {
Ok(devices) => devices,
Err(e) => {
log::warn!(target: "hid", "cpal 无法枚举音频设备: {e}");
return false;
}
};
devices
.filter(|d| d.name().is_ok_and(|n| has_target_usb_audio([n])))
.any(|d| {
d.supported_input_configs()
.is_ok_and(|mut configs| configs.next().is_some())
&& d.default_input_config().is_ok()
})
}
#[cfg(feature = "usb")]
fn with_reused_hid_api<T>(f: impl FnOnce(&HidApi) -> T) -> Option<T> {
use std::cell::RefCell;
thread_local! {
static HID_API: RefCell<Option<HidApi>> = const { RefCell::new(None) };
}
HID_API.with(|cell| {
let mut api = cell.borrow_mut().take();
if let Some(existing) = api.as_mut() {
if let Err(e) = existing.refresh_devices() {
log::warn!(
target: "hid",
"HidApi refresh_devices 失败,丢弃缓存重建: {e}"
);
api = None;
}
}
if api.is_none() {
match HidApi::new() {
Ok(created) => api = Some(created),
Err(e) => {
log::warn!(target: "hid", "HidApi 创建失败: {e}");
return None;
}
}
}
let result = api.as_ref().map(f);
*cell.borrow_mut() = api;
result
})
}
#[cfg(feature = "usb")]
fn full_check_interval_ticks(check_interval: Duration) -> u32 {
const FULL_CHECK_PERIOD_MS: u128 = 5_000;
const MAX_LIGHTWEIGHT_STREAK: u128 = 20;
let interval_ms = check_interval.as_millis().max(1);
(FULL_CHECK_PERIOD_MS / interval_ms).clamp(1, MAX_LIGHTWEIGHT_STREAK) as u32
}
#[cfg(feature = "usb")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BlePreemptionCheck {
Preempt,
Keep,
NeedsFullCheck,
}
#[cfg(feature = "usb")]
fn scan_target_hid_buses(api: &HidApi) -> (bool, bool) {
let mut has_usb_hid = false;
let mut has_unknown_hid = false;
for device in api
.device_list()
.filter(|d| d.vendor_id() == VID && is_target_pid(d.product_id()))
{
match device.bus_type() {
BusType::Usb => has_usb_hid = true,
BusType::Unknown => has_unknown_hid = true,
BusType::Bluetooth | BusType::I2c | BusType::Spi => {}
}
}
(has_usb_hid, has_unknown_hid)
}
#[cfg(feature = "usb")]
fn decide_ble_preemption(has_usb_hid: bool, has_unknown_hid: bool) -> bool {
has_usb_hid || has_unknown_hid
}
#[cfg(feature = "usb")]
fn ble_preemption_from_hid(hid_says_preempt: Option<bool>) -> BlePreemptionCheck {
match hid_says_preempt {
Some(true) => BlePreemptionCheck::Preempt,
Some(false) => BlePreemptionCheck::Keep,
None => BlePreemptionCheck::NeedsFullCheck,
}
}
#[cfg(feature = "usb")]
fn decide_hid_detection(
has_usb_audio: bool,
has_usb_hid: bool,
has_unknown_hid: bool,
) -> HidDetectionDecision {
if has_usb_audio || has_usb_hid {
HidDetectionDecision::Connected
} else if has_unknown_hid {
HidDetectionDecision::ProbeMode
} else {
HidDetectionDecision::NotConnected
}
}
pub type OnConnectionChange = Box<dyn Fn(Option<ConnectionType>) + Send + Sync>;
#[cfg(feature = "usb")]
pub type OnMonitorReady = Box<
dyn Fn(
Arc<Mutex<Option<crate::runtime::usb::device_manager::DeviceConnection>>>,
Arc<AtomicBool>,
) + Send
+ Sync,
>;
#[cfg(feature = "ble")]
pub type OnBleDeviceNameUpdated = Box<dyn Fn(&str) + Send + Sync>;
#[cfg(feature = "ble")]
pub type EnsureVendorGattClient = Box<
dyn Fn() -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Arc<VendorGattClient>>> + Send>,
> + Send
+ Sync,
>;
pub struct HotplugManager {
running: Arc<AtomicBool>,
stop_requested: Option<Arc<AtomicBool>>,
event_tx: broadcast::Sender<BoardEvent>,
config: HotplugConfig,
on_connection_change: Option<OnConnectionChange>,
#[cfg(feature = "usb")]
on_monitor_ready: Option<OnMonitorReady>,
#[cfg(feature = "ble")]
vendor_gatt_client_slot: Arc<Mutex<Option<Arc<VendorGattClient>>>>,
#[cfg(feature = "ble")]
ensure_client: Option<EnsureVendorGattClient>,
#[cfg(feature = "ble")]
ble_last_device_name: Arc<Mutex<Option<String>>>,
#[cfg(feature = "ble")]
ble_auto_connect: Arc<AtomicBool>,
#[cfg(feature = "ble")]
on_ble_device_name_updated: Option<OnBleDeviceNameUpdated>,
}
impl HotplugManager {
pub fn new(event_tx: broadcast::Sender<BoardEvent>, config: HotplugConfig) -> Self {
Self {
running: Arc::new(AtomicBool::new(false)),
stop_requested: None,
event_tx,
config,
on_connection_change: None,
#[cfg(feature = "usb")]
on_monitor_ready: None,
#[cfg(feature = "ble")]
vendor_gatt_client_slot: Arc::new(Mutex::new(None)),
#[cfg(feature = "ble")]
ensure_client: None,
#[cfg(feature = "ble")]
ble_last_device_name: Arc::new(Mutex::new(None)),
#[cfg(feature = "ble")]
ble_auto_connect: Arc::new(AtomicBool::new(true)),
#[cfg(feature = "ble")]
on_ble_device_name_updated: None,
}
}
pub fn on_connection_change(mut self, cb: OnConnectionChange) -> Self {
self.on_connection_change = Some(cb);
self
}
#[cfg(feature = "usb")]
pub fn on_monitor_ready(mut self, cb: OnMonitorReady) -> Self {
self.on_monitor_ready = Some(cb);
self
}
pub fn with_running_flag(mut self, flag: Arc<AtomicBool>) -> Self {
self.stop_requested = Some(flag);
self
}
#[cfg(feature = "ble")]
pub fn with_vendor_gatt_client_slot(
mut self,
slot: Arc<Mutex<Option<Arc<VendorGattClient>>>>,
) -> Self {
self.vendor_gatt_client_slot = slot;
self
}
#[cfg(feature = "ble")]
pub fn with_ble_ensure_client(mut self, ensure: EnsureVendorGattClient) -> Self {
self.ensure_client = Some(ensure);
self
}
#[cfg(feature = "ble")]
pub fn with_ble_target_device_name(mut self, name: Arc<Mutex<Option<String>>>) -> Self {
self.ble_last_device_name = name;
self
}
#[cfg(feature = "ble")]
pub fn with_ble_auto_connect(mut self, flag: Arc<AtomicBool>) -> Self {
self.ble_auto_connect = flag;
self
}
#[cfg(feature = "ble")]
pub fn on_ble_device_name_updated(mut self, cb: OnBleDeviceNameUpdated) -> Self {
self.on_ble_device_name_updated = Some(cb);
self
}
#[cfg(feature = "ble")]
pub fn set_ble_auto_connect(&self, on: bool) {
self.ble_auto_connect.store(on, Ordering::SeqCst);
}
#[cfg(feature = "ble")]
pub fn set_ble_target(&self, name: Option<&str>) {
*self.ble_last_device_name.lock().unwrap() = name.map(|s| s.to_string());
}
pub fn stop(&self) {
self.running.store(false, Ordering::SeqCst);
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst) && !self.is_stop_requested()
}
fn is_stop_requested(&self) -> bool {
self.stop_requested
.as_ref()
.map(|f| !f.load(Ordering::SeqCst))
.unwrap_or(false)
}
pub async fn run(&mut self) {
self.running.store(true, Ordering::SeqCst);
log::info!(target: "hotplug", "热插拔管理器启动");
while self.running.load(Ordering::SeqCst) && !self.is_stop_requested() {
self.emit_reconnect(ReconnectState::WaitingForDevice, None, None);
let conn_type = match self.wait_for_device().await {
Some(ct) => ct,
None => break,
};
match conn_type {
#[cfg(feature = "usb")]
ConnectionType::Usb => self.run_usb_session().await,
#[cfg(not(feature = "usb"))]
ConnectionType::Usb => {
log::warn!(target: "hotplug", "检测到 USB 连接但 usb feature 未启用,跳过");
}
ConnectionType::Ble => self.run_ble_session().await,
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
log::info!(target: "hotplug", "热插拔管理器停止");
}
async fn wait_for_device(&self) -> Option<ConnectionType> {
while self.running.load(Ordering::SeqCst) && !self.is_stop_requested() {
if let Some(ct) = self.detect_connection().await {
return Some(ct);
}
tokio::time::sleep(self.config.retry_interval).await;
}
None
}
async fn detect_connection(&self) -> Option<ConnectionType> {
#[cfg(feature = "usb")]
if let Some(ct) = Self::detect_hid_connection_async().await {
return Some(ct);
}
#[cfg(feature = "ble")]
{
let has_target = self.ble_last_device_name.lock().unwrap().is_some();
if has_target && self.ble_auto_connect.load(Ordering::SeqCst) {
return Some(ConnectionType::Ble);
}
}
#[allow(unreachable_code)]
None
}
#[cfg(feature = "usb")]
async fn detect_hid_connection_async() -> Option<ConnectionType> {
spawn_blocking_with_runloop(Self::detect_hid_connection)
.await
.ok()
.flatten()
}
#[cfg(feature = "usb")]
async fn check_usb_preemption_async() -> BlePreemptionCheck {
spawn_blocking_with_runloop(Self::check_usb_preemption)
.await
.unwrap_or(BlePreemptionCheck::NeedsFullCheck)
}
#[cfg(feature = "usb")]
fn check_usb_preemption() -> BlePreemptionCheck {
let decision = with_reused_hid_api(|api| {
let (has_usb_hid, has_unknown_hid) = scan_target_hid_buses(api);
decide_ble_preemption(has_usb_hid, has_unknown_hid)
});
ble_preemption_from_hid(decision)
}
#[cfg(feature = "usb")]
fn detect_hid_connection() -> Option<ConnectionType> {
let hid_decision = with_reused_hid_api(|api| {
let (has_usb_hid, has_unknown_hid) = scan_target_hid_buses(api);
let has_usb_audio = !has_usb_hid && probe_target_usb_audio();
match decide_hid_detection(has_usb_audio, has_usb_hid, has_unknown_hid) {
HidDetectionDecision::Connected => Some(ConnectionType::Usb),
HidDetectionDecision::ProbeMode => match Self::probe_device_mode(api) {
Some(3) => Some(ConnectionType::Usb),
_ => None,
},
HidDetectionDecision::NotConnected => None,
}
});
if let Some(decision) = hid_decision {
decision
} else if probe_target_usb_audio() {
log::warn!(
target: "hid",
"HidApi::new 失败但有 USB Audio,USB HID 命令路径将不可用(后续连接可能失败循环)"
);
Some(ConnectionType::Usb)
} else {
None
}
}
#[cfg(feature = "usb")]
async fn connect_device_hid(
&self,
) -> Result<(
HidMonitor,
Arc<Mutex<Option<DeviceConnection>>>,
Arc<AtomicBool>,
)> {
let event_tx = self.event_tx.clone();
let monitor = spawn_blocking_with_runloop(move || -> Result<HidMonitor> {
let mut device_mgr = DeviceManager::new()?;
device_mgr.refresh()?;
let mut monitor = HidMonitor::new(event_tx);
let monitor_config = MonitorConfig::default();
let config_conn = device_mgr
.connect_usage_page_and_usage(USAGE_PAGE_CONFIG, 0x0002)
.or_else(|_| device_mgr.connect_usage_page(USAGE_PAGE_CONFIG));
match config_conn {
Ok(conn) => monitor.start_config_monitor(conn, monitor_config)?,
Err(e) => {
log::warn!(target: "hid", "Config 接口连接失败: {},等待重试", e);
return Err(e);
}
}
if let Ok(conn) = device_mgr.connect_usage_page(USAGE_PAGE_CONSUMER) {
let _ = monitor.start_consumer_monitor(conn);
}
Ok(monitor)
})
.await
.map_err(|e| anyhow::anyhow!("HID 线程崩溃: {:?}", e))??;
let config_conn = monitor.config_conn();
let paused = monitor.paused_arc();
if let Some(ref cb) = self.on_monitor_ready {
cb(config_conn.clone(), paused.clone());
}
self.notify_connected(ConnectionType::Usb);
log::info!(target: "hid", "设备已连接: USB");
Ok((monitor, config_conn, paused))
}
#[cfg(feature = "usb")]
async fn run_usb_session(&mut self) {
self.emit_reconnect(ReconnectState::Connecting, None, None);
let mut monitor = match self.connect_device_hid().await {
Ok((m, _, _)) => m,
Err(e) => {
log::warn!(target: "hid", "USB 连接失败: {}", e);
self.emit_error(format!("USB 连接失败: {e}"), true);
self.notify_disconnected(DisconnectReason::DeviceGone);
return;
}
};
if let Some(info) = spawn_blocking_with_runloop(|| {
HidApi::new()
.ok()
.and_then(|api| Self::probe_usb_device_info(&api))
})
.await
.ok()
.flatten()
{
#[cfg(feature = "ble")]
{
let ble_name = format!("REAI_VB_{}", info.chip_id);
log::info!(target: "hotplug", "USB 读到 chip_id={} → BLE 目标 {}", info.chip_id, ble_name);
self.set_ble_target(Some(&ble_name));
}
let _ = self.event_tx.send(BoardEvent::DeviceInfo(info));
}
let reason = self.monitor_hid_while_connected(&monitor).await;
monitor.stop();
log::info!(target: "hid", "USB 设备已断开: {:?}", reason);
self.notify_disconnected(reason);
}
#[cfg(feature = "usb")]
async fn monitor_hid_while_connected(&self, monitor: &HidMonitor) -> DisconnectReason {
loop {
if !self.running.load(Ordering::SeqCst) || self.is_stop_requested() {
return DisconnectReason::UserAction;
}
if !monitor.is_running() {
return DisconnectReason::DeviceGone;
}
tokio::time::sleep(self.config.check_interval).await;
if Self::detect_hid_connection_async().await.is_none() {
log::warn!(target: "hid", "定时探测:USB 设备已不在");
return DisconnectReason::DeviceGone;
}
}
}
#[cfg(feature = "ble")]
async fn run_ble_session(&mut self) {
let client = {
let guard = self.vendor_gatt_client_slot.lock().unwrap();
guard.clone()
};
let client = match client {
Some(c) => c,
None => {
let ensure = match self.ensure_client.as_ref() {
Some(f) => f,
None => {
log::error!(target: "hotplug", "BLE 模式未设置 ensure_client 回调");
return;
}
};
match ensure().await {
Ok(c) => c,
Err(e) => {
log::warn!(target: "hotplug", "BLE 延迟创建 VendorGattClient 失败: {}", e);
tokio::time::sleep(Duration::from_secs(5)).await;
return;
}
}
}
};
let target = self.ble_last_device_name.lock().unwrap().clone();
self.emit_reconnect(ReconnectState::Scanning, None, None);
let (peripheral, found_name) = match client.scan_for_device(target.as_deref()).await {
Ok((p, name)) => {
log::info!(target: "hotplug", "扫描到 BLE 设备: {}", name);
(p, name)
}
Err(e) => {
log::warn!(target: "hotplug", "BLE 扫描未发现设备: {}", e);
self.emit_error(format!("BLE 扫描未发现设备: {e}"), true);
tokio::time::sleep(Duration::from_secs(5)).await;
return;
}
};
let current_target = self.ble_last_device_name.lock().unwrap().clone();
if !self.ble_auto_connect.load(Ordering::SeqCst)
|| current_target.as_deref() != Some(found_name.as_str())
{
log::info!(target: "hotplug", "BLE 目标已取消或改变,丢弃扫描结果 {}", found_name);
return;
}
*self.ble_last_device_name.lock().unwrap() = Some(found_name.clone());
if let Some(ref cb) = self.on_ble_device_name_updated {
cb(&found_name);
}
self.emit_reconnect(ReconnectState::Connecting, None, None);
if let Err(e) = client.connect(&peripheral).await {
log::warn!(target: "hotplug", "BLE 连接失败: {},5s 后重试", e);
self.emit_error(format!("BLE 连接失败: {e}"), true);
tokio::time::sleep(Duration::from_secs(5)).await;
return;
}
self.notify_connected(ConnectionType::Ble);
client.start_notification_loop();
log::info!(
target: "hotplug",
"BLE 已连接: {:?}",
self.ble_last_device_name.lock().unwrap()
);
let vendor_running = client.running();
#[cfg(feature = "usb")]
let mut usb_detected = false;
#[cfg(feature = "usb")]
let full_check_every = full_check_interval_ticks(self.config.check_interval);
#[cfg(feature = "usb")]
let mut ticks_since_full = 0u32;
loop {
if !self.running.load(Ordering::SeqCst) || self.is_stop_requested() {
break;
}
if !vendor_running.load(Ordering::SeqCst) {
break;
}
#[cfg(feature = "usb")]
if usb_detected {
break;
}
tokio::time::sleep(self.config.check_interval).await;
#[cfg(feature = "usb")]
{
ticks_since_full += 1;
let detected = if ticks_since_full >= full_check_every {
ticks_since_full = 0;
Self::detect_hid_connection_async().await == Some(ConnectionType::Usb)
} else {
match Self::check_usb_preemption_async().await {
BlePreemptionCheck::Preempt => true,
BlePreemptionCheck::Keep => false,
BlePreemptionCheck::NeedsFullCheck => {
ticks_since_full = 0;
Self::detect_hid_connection_async().await == Some(ConnectionType::Usb)
}
}
};
if detected {
usb_detected = true;
log::info!(target: "hotplug", "USB 已插入,退出 BLE 会话(保留设备名供重连)");
}
}
}
let _ = client.disconnect().await;
self.notify_disconnected(DisconnectReason::DeviceGone);
log::info!(target: "hotplug", "BLE 设备已断开");
}
#[cfg(not(feature = "ble"))]
async fn run_ble_session(&mut self) {
log::warn!(target: "hotplug", "检测到 BLE 连接但 ble feature 未启用,跳过");
}
#[cfg(feature = "usb")]
fn probe_device_mode(api: &HidApi) -> Option<u8> {
let config_devices: Vec<_> = api
.device_list()
.filter(|d| {
d.vendor_id() == VID
&& is_target_pid(d.product_id())
&& d.usage_page() == USAGE_PAGE_CONFIG
})
.collect();
let target = config_devices
.iter()
.find(|d| d.usage() == 0x0002)
.or_else(|| config_devices.first())?;
let device = api.open_path(target.path()).ok()?;
let cmd = HidPacket::get_device_info();
device.write(&cmd).ok()?;
let mut buf = [0u8; 64];
for attempt in 0..10 {
match device.read_timeout(&mut buf, 200) {
Ok(n) if n >= 5 && buf[1] == CMD_GET_DEVICE_INFO => {
let mode = buf[4];
log::debug!(target: "hid", "CMD 0x13 探测成功: mode={} (skip={})", mode, attempt);
return Some(mode);
}
Ok(_) => continue,
Err(_) => return None,
}
}
None
}
#[cfg(feature = "usb")]
fn probe_usb_device_info(api: &HidApi) -> Option<DeviceInfo> {
let config_devices: Vec<_> = api
.device_list()
.filter(|d| {
d.vendor_id() == VID
&& is_target_pid(d.product_id())
&& d.usage_page() == USAGE_PAGE_CONFIG
})
.collect();
let target = config_devices
.iter()
.find(|d| d.usage() == 0x0002)
.or_else(|| config_devices.first())?;
let device = api.open_path(target.path()).ok()?;
let cmd = HidPacket::get_device_info();
device.write(&cmd).ok()?;
let mut buf = [0u8; 64];
for _ in 0..10 {
match device.read_timeout(&mut buf, 200) {
Ok(n) if n >= 5 && buf[1] == CMD_GET_DEVICE_INFO => break,
Ok(_) => continue,
Err(_) => return None,
}
}
let info =
crate::tool::parse::parse_device_info_from_buf(&buf, 4, ConnectionType::Usb).ok()?;
log::debug!(
target: "hid",
"CMD 0x13 完整探测: chip_id={} fw={} battery={}%",
info.chip_id,
info.firmware_version,
info.battery_level
);
Some(info)
}
fn emit_error(&self, msg: impl std::fmt::Display, recoverable: bool) {
let _ = self.event_tx.send(BoardEvent::Error(ErrorEvent {
message: msg.to_string(),
recoverable,
}));
}
fn notify_connected(&self, conn_type: ConnectionType) {
if let Some(ref cb) = self.on_connection_change {
cb(Some(conn_type));
}
let _ = self.event_tx.send(BoardEvent::Connection(ConnectionEvent {
connected: true,
connection_type: Some(conn_type),
reason: None,
}));
let _ = self.event_tx.send(BoardEvent::Reconnect(ReconnectEvent {
state: ReconnectState::Connected,
attempt: None,
message: None,
}));
}
fn notify_disconnected(&self, reason: DisconnectReason) {
if let Some(ref cb) = self.on_connection_change {
cb(None);
}
let _ = self.event_tx.send(BoardEvent::Connection(ConnectionEvent {
connected: false,
connection_type: None,
reason: Some(reason),
}));
}
fn emit_reconnect(&self, state: ReconnectState, attempt: Option<u32>, message: Option<String>) {
let _ = self.event_tx.send(BoardEvent::Reconnect(ReconnectEvent {
state,
attempt,
message,
}));
}
}
#[cfg(all(test, feature = "usb"))]
mod tests {
use super::*;
#[test]
fn explicit_usb_hid_connects_without_waiting_for_audio() {
assert_eq!(
decide_hid_detection(false, true, false),
HidDetectionDecision::Connected
);
}
#[test]
fn audio_signal_cannot_change_verdict_once_usb_hid_is_seen() {
for has_unknown_hid in [false, true] {
assert_eq!(
decide_hid_detection(false, true, has_unknown_hid),
decide_hid_detection(true, true, has_unknown_hid),
"has_usb_hid 为真时音频信号不应影响判定(has_unknown_hid={has_unknown_hid})"
);
}
}
#[test]
fn unknown_hid_bus_keeps_command_probe_fallback() {
assert_eq!(
decide_hid_detection(false, false, true),
HidDetectionDecision::ProbeMode
);
}
#[test]
fn bluetooth_only_target_is_not_misclassified_as_usb() {
assert_eq!(
decide_hid_detection(false, false, false),
HidDetectionDecision::NotConnected
);
}
#[test]
fn usb_audio_remains_sufficient_during_hid_enumeration_delay() {
assert_eq!(
decide_hid_detection(true, false, false),
HidDetectionDecision::Connected
);
}
#[test]
fn unrelated_usb_audio_devices_do_not_count_as_the_keyboard() {
assert!(!has_target_usb_audio(["USB Microphone"]));
assert!(!has_target_usb_audio(["Generic USB Audio"]));
assert!(!has_target_usb_audio([
"Blue Yeti USB",
"USB Audio CODEC",
"Scarlett Solo USB"
]));
}
#[test]
fn keyword_matched_devices_still_count() {
assert!(has_target_usb_audio(["ReAI Audio-HID"]));
assert!(has_target_usb_audio(["Audio-HID"]));
assert!(has_target_usb_audio(["AI Vibe Board"]));
assert!(has_target_usb_audio([
"MacBook Pro Microphone",
"USB Microphone",
"ReAI Vibe Board"
]));
}
#[test]
fn no_audio_devices_means_no_match() {
assert!(!has_target_usb_audio(Vec::<String>::new()));
assert!(!has_target_usb_audio(["MacBook Pro Microphone"]));
}
#[test]
fn unknown_bus_target_still_triggers_preemption() {
assert!(decide_ble_preemption(false, true));
assert_eq!(
decide_hid_detection(false, false, true),
HidDetectionDecision::ProbeMode
);
}
#[test]
fn usb_bus_target_triggers_preemption() {
assert!(decide_ble_preemption(true, false));
}
#[test]
fn no_usb_side_target_keeps_the_ble_session() {
assert!(!decide_ble_preemption(false, false));
}
#[test]
fn hid_unavailable_falls_back_to_full_check() {
assert_eq!(
ble_preemption_from_hid(None),
BlePreemptionCheck::NeedsFullCheck
);
}
#[test]
fn hid_available_maps_directly() {
assert_eq!(
ble_preemption_from_hid(Some(true)),
BlePreemptionCheck::Preempt
);
assert_eq!(
ble_preemption_from_hid(Some(false)),
BlePreemptionCheck::Keep
);
}
#[test]
fn driver_interval_keeps_most_checks_lightweight() {
assert_eq!(full_check_interval_ticks(Duration::from_millis(500)), 10);
}
#[test]
fn sdk_default_interval_always_runs_full_check() {
assert_eq!(full_check_interval_ticks(Duration::from_secs(5)), 1);
}
#[test]
fn long_interval_never_disables_full_check() {
assert_eq!(full_check_interval_ticks(Duration::from_secs(60)), 1);
}
#[test]
fn tiny_interval_does_not_starve_the_full_check() {
assert_eq!(full_check_interval_ticks(Duration::from_millis(1)), 20);
assert_eq!(full_check_interval_ticks(Duration::from_millis(0)), 20);
assert_eq!(full_check_interval_ticks(Duration::from_millis(100)), 20);
}
}