fix(hid): share macOS drag compatibility across OTG and CH9329

This commit is contained in:
mofeng-git
2026-09-10 23:00:05 +08:00
parent 1967d22dff
commit 78af9f67de
18 changed files with 542 additions and 101 deletions

View File

@@ -80,6 +80,8 @@ pub struct OtgBackend {
keyboard_leds_enabled: bool,
keyboard_state: Mutex<KeyboardReport>,
mouse_buttons: AtomicU8,
macos_drag: bool,
macos_drag_state: Mutex<super::macos_drag::MacosDrag>,
led_state: Arc<parking_lot::RwLock<LedState>>,
screen_resolution: parking_lot::RwLock<Option<(u32, u32)>>,
udc_name: Arc<parking_lot::RwLock<Option<String>>>,
@@ -99,6 +101,15 @@ const OTG_RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(500);
impl OtgBackend {
/// Gadget must already exist; paths come from `OtgService`.
pub fn from_handles(paths: HidDevicePaths) -> Result<Self> {
Self::with_macos_drag(paths, false)
}
pub fn with_macos_drag(paths: HidDevicePaths, macos_drag: bool) -> Result<Self> {
if macos_drag && (paths.mouse_relative.is_none() || paths.mouse_absolute.is_none()) {
return Err(AppError::Config(
"macOS drag compatibility requires both OTG mouse interfaces".into(),
));
}
let (runtime_notify_tx, _runtime_notify_rx) = watch::channel(());
Ok(Self {
keyboard_path: paths.keyboard,
@@ -112,6 +123,8 @@ impl OtgBackend {
keyboard_leds_enabled: paths.keyboard_leds_enabled,
keyboard_state: Mutex::new(KeyboardReport::default()),
mouse_buttons: AtomicU8::new(0),
macos_drag,
macos_drag_state: Mutex::new(super::macos_drag::MacosDrag::default()),
led_state: Arc::new(parking_lot::RwLock::new(LedState::default())),
screen_resolution: parking_lot::RwLock::new(Some((1920, 1080))),
udc_name: Arc::new(parking_lot::RwLock::new(paths.udc)),
@@ -852,6 +865,28 @@ impl HidBackend for OtgBackend {
async fn send_mouse(&self, event: MouseEvent) -> Result<()> {
let buttons = self.mouse_buttons.load(Ordering::Relaxed);
if self.macos_drag {
use super::macos_drag::MouseReport;
let mut state = self.macos_drag_state.lock();
let extent = self.screen_resolution.read().unwrap_or((1920, 1080));
let (buttons, reports) = state.plan(event, buttons, extent);
self.mouse_buttons.store(buttons, Ordering::Relaxed);
for report in reports {
match report {
MouseReport::Absolute { buttons, x, y } => {
self.send_mouse_report_absolute(buttons, x, y, 0)?
}
MouseReport::Relative {
buttons,
dx,
dy,
wheel,
} => self.send_mouse_report_relative(buttons, dx, dy, wheel)?,
}
}
return Ok(());
}
match event.event_type {
MouseEventType::Move => {
let dx = event.x.clamp(-127, 127) as i8;
@@ -896,6 +931,7 @@ impl HidBackend for OtgBackend {
}
self.mouse_buttons.store(0, Ordering::Relaxed);
self.macos_drag_state.lock().reset();
self.send_mouse_report_relative(0, 0, 0, 0)?;
self.send_mouse_report_absolute(0, 0, 0, 0)?;
@@ -988,6 +1024,51 @@ mod tests {
assert_eq!(kb_report.to_bytes().len(), 8);
}
#[tokio::test]
async fn mouse_compatibility_writes_both_endpoints_and_preserves_default() {
use crate::hid::MouseButton;
for enabled in [false, true] {
let relative = tempfile::NamedTempFile::new().unwrap();
let absolute = tempfile::NamedTempFile::new().unwrap();
let backend = OtgBackend::with_macos_drag(
HidDevicePaths {
mouse_relative: Some(relative.path().to_path_buf()),
mouse_absolute: Some(absolute.path().to_path_buf()),
..Default::default()
},
enabled,
)
.unwrap();
for event in [
MouseEvent::move_abs(8000, 8000),
MouseEvent::button_down(MouseButton::Left),
MouseEvent::move_abs(16000, 16000),
MouseEvent::button_up(MouseButton::Left),
] {
backend.send_mouse(event).await.unwrap();
}
let abs = fs::read(absolute.path()).unwrap();
let rel = fs::read(relative.path()).unwrap();
let buttons: Vec<_> = abs.chunks_exact(6).map(|packet| packet[0]).collect();
assert_eq!(buttons, if enabled { vec![0, 1, 0] } else { vec![0, 0] });
let dx: i32 = rel
.chunks_exact(4)
.map(|packet| i32::from(packet[1] as i8))
.sum();
let dy: i32 = rel
.chunks_exact(4)
.map(|packet| i32::from(packet[2] as i8))
.sum();
assert_eq!((dx, dy), if enabled { (468, 263) } else { (0, 0) });
assert_eq!(&rel[rel.len() - 4..], &[0, 0, 0, 0]);
}
}
#[test]
fn compatibility_requires_both_mouse_endpoints() {
assert!(OtgBackend::with_macos_drag(HidDevicePaths::default(), true).is_err());
}
#[tokio::test]
async fn prepare_rebuild_closes_devices_without_writing_reset_reports() {
let mut file = tempfile::tempfile().unwrap();