diff --git a/src/config/schema/hid.rs b/src/config/schema/hid.rs index 296aa2fb..af83cc35 100644 --- a/src/config/schema/hid.rs +++ b/src/config/schema/hid.rs @@ -231,6 +231,8 @@ pub struct HidConfig { #[serde(default)] pub ch9329_hybrid_mouse: bool, #[serde(default)] + pub ch9329_macos_drag: bool, + #[serde(default)] pub ch9329_descriptor: Ch9329DescriptorConfig, pub mouse_absolute: bool, } @@ -248,6 +250,7 @@ impl Default for HidConfig { ch9329_port: "/dev/ttyUSB0".to_string(), ch9329_baudrate: 9600, ch9329_hybrid_mouse: false, + ch9329_macos_drag: false, ch9329_descriptor: Ch9329DescriptorConfig::default(), mouse_absolute: true, } diff --git a/src/hid/backend.rs b/src/hid/backend.rs index 6f02583e..12c1d5bd 100644 --- a/src/hid/backend.rs +++ b/src/hid/backend.rs @@ -27,6 +27,8 @@ pub enum HidBackendType { baud_rate: u32, #[serde(default)] hybrid_mouse: bool, + #[serde(default)] + macos_drag: bool, }, #[default] None, diff --git a/src/hid/ch9329.rs b/src/hid/ch9329.rs index 49fe730e..df4ab5a5 100644 --- a/src/hid/ch9329.rs +++ b/src/hid/ch9329.rs @@ -238,6 +238,7 @@ pub struct Ch9329Backend { last_abs_y: Arc, relative_mouse_active: Arc, hybrid_mouse: bool, + macos_drag: bool, runtime: Arc, } @@ -251,6 +252,15 @@ impl Ch9329Backend { } pub fn with_options(port_path: &str, baud_rate: u32, hybrid_mouse: bool) -> Result { + Self::with_compatibility_options(port_path, baud_rate, hybrid_mouse, false) + } + + pub fn with_compatibility_options( + port_path: &str, + baud_rate: u32, + hybrid_mouse: bool, + macos_drag: bool, + ) -> Result { Ok(Self { port_path: port_path.to_string(), baud_rate, @@ -266,6 +276,7 @@ impl Ch9329Backend { last_abs_y: Arc::new(AtomicU16::new(0)), relative_mouse_active: Arc::new(AtomicBool::new(false)), hybrid_mouse, + macos_drag, runtime: Arc::new(Ch9329RuntimeState::new()), }) } @@ -978,17 +989,32 @@ impl Ch9329Backend { } fn should_send_button_wheel_relative(&self) -> bool { - self.hybrid_mouse || self.relative_mouse_active.load(Ordering::Relaxed) + (self.hybrid_mouse && !self.macos_drag) + || self.relative_mouse_active.load(Ordering::Relaxed) } fn absolute_move_buttons(&self, buttons: u8) -> u8 { - if self.hybrid_mouse { + if self.hybrid_mouse && !self.macos_drag { 0 } else { buttons } } + fn absolute_delta_to_relative(current: u16, previous: u16, extent: u32) -> i8 { + let delta = current as i32 - previous as i32; + if delta == 0 { + return 0; + } + + let scaled = delta * extent.max(1) as i32 / CH9329_MOUSE_RESOLUTION as i32; + if scaled == 0 { + delta.signum() as i8 + } else { + scaled.clamp(-127, 127) as i8 + } + } + fn worker_loop( port_path: String, baud_rate: u32, @@ -1283,9 +1309,23 @@ impl HidBackend for Ch9329Backend { self.relative_mouse_active.store(false, Ordering::Relaxed); let x = ((event.x.clamp(0, 32767) as u32) * CH9329_MOUSE_RESOLUTION / 32768) as u16; let y = ((event.y.clamp(0, 32767) as u32) * CH9329_MOUSE_RESOLUTION / 32768) as u16; - self.last_abs_x.store(x, Ordering::Relaxed); - self.last_abs_y.store(y, Ordering::Relaxed); - self.send_mouse_absolute(self.absolute_move_buttons(buttons), x, y, 0)?; + let previous_x = self.last_abs_x.swap(x, Ordering::Relaxed); + let previous_y = self.last_abs_y.swap(y, Ordering::Relaxed); + + if self.macos_drag && buttons != 0 { + // macOS accepts button edges from CH9329 absolute report ID 2, + // but may terminate a drag when movement continues on that + // report. Keep the absolute button held and move through the + // relative report until the matching absolute button-up. + let (width, height) = *self.screen_resolution.read(); + let dx = Self::absolute_delta_to_relative(x, previous_x, width); + let dy = Self::absolute_delta_to_relative(y, previous_y, height); + if dx != 0 || dy != 0 { + self.send_mouse_relative(buttons, dx, dy, 0)?; + } + } else { + self.send_mouse_absolute(self.absolute_move_buttons(buttons), x, y, 0)?; + } } MouseEventType::Down => { if let Some(button) = event.button { @@ -1666,13 +1706,66 @@ mod tests { } #[test] - fn test_hybrid_mouse_routes_buttons_and_wheel_to_relative_reports() { + fn test_hybrid_mouse_preserves_linux_compatibility_routing() { let backend = Ch9329Backend::with_options("/dev/null", DEFAULT_BAUD_RATE, true).unwrap(); assert!(backend.should_send_button_wheel_relative()); assert_eq!(backend.absolute_move_buttons(0x07), 0); } + #[tokio::test] + async fn test_macos_drag_uses_absolute_edges_and_relative_motion() { + let backend = + Ch9329Backend::with_compatibility_options("/dev/null", DEFAULT_BAUD_RATE, false, true) + .unwrap(); + let (worker_tx, worker_rx) = mpsc::channel(); + *backend.worker_tx.lock() = Some(worker_tx); + backend.set_screen_resolution(1920, 1080); + + backend + .send_mouse(MouseEvent::move_abs(8000, 8000)) + .await + .unwrap(); + backend + .send_mouse(MouseEvent::button_down(crate::hid::MouseButton::Left)) + .await + .unwrap(); + backend + .send_mouse(MouseEvent::move_abs(8064, 8064)) + .await + .unwrap(); + backend + .send_mouse(MouseEvent::button_up(crate::hid::MouseButton::Left)) + .await + .unwrap(); + + let packets: Vec<_> = worker_rx + .try_iter() + .filter_map(|command| match command { + WorkerCommand::Packet { cmd, data } => Some((cmd, data)), + _ => None, + }) + .collect(); + assert_eq!( + packets, + vec![ + ( + cmd::SEND_MS_ABS_DATA, + vec![0x02, 0x00, 0xE8, 0x03, 0xE8, 0x03, 0x00], + ), + ( + cmd::SEND_MS_ABS_DATA, + vec![0x02, 0x01, 0xE8, 0x03, 0xE8, 0x03, 0x00], + ), + (cmd::SEND_MS_REL_DATA, vec![0x01, 0x01, 0x03, 0x02, 0x00]), + ( + cmd::SEND_MS_ABS_DATA, + vec![0x02, 0x00, 0xF0, 0x03, 0xF0, 0x03, 0x00], + ), + ] + ); + } + #[test] fn test_default_mouse_mode_preserves_absolute_report_buttons() { let backend = Ch9329Backend::with_baud_rate("/dev/null", DEFAULT_BAUD_RATE).unwrap(); @@ -1680,4 +1773,28 @@ mod tests { assert!(!backend.should_send_button_wheel_relative()); assert_eq!(backend.absolute_move_buttons(0x07), 0x07); } + + #[test] + fn test_absolute_delta_to_relative_preserves_small_movements_and_clamps() { + assert_eq!( + Ch9329Backend::absolute_delta_to_relative(1001, 1000, 1920), + 1 + ); + assert_eq!( + Ch9329Backend::absolute_delta_to_relative(999, 1000, 1920), + -1 + ); + assert_eq!( + Ch9329Backend::absolute_delta_to_relative(2000, 1000, 1920), + 127 + ); + assert_eq!( + Ch9329Backend::absolute_delta_to_relative(0, 1000, 1920), + -127 + ); + assert_eq!( + Ch9329Backend::absolute_delta_to_relative(1000, 1000, 1920), + 0 + ); + } } diff --git a/src/hid/factory.rs b/src/hid/factory.rs index ce9f7ac0..3e88b3ac 100644 --- a/src/hid/factory.rs +++ b/src/hid/factory.rs @@ -49,16 +49,20 @@ impl HidBackendFactory { port, baud_rate, hybrid_mouse, + macos_drag, } => { info!( - "Initializing CH9329 HID backend on {} @ {} baud, hybrid_mouse={}", - port, baud_rate, hybrid_mouse + "Initializing CH9329 HID backend on {} @ {} baud, hybrid_mouse={}, macos_drag={}", + port, baud_rate, hybrid_mouse, macos_drag ); - Ok(Some(Arc::new(ch9329::Ch9329Backend::with_options( - port, - *baud_rate, - *hybrid_mouse, - )?))) + Ok(Some(Arc::new( + ch9329::Ch9329Backend::with_compatibility_options( + port, + *baud_rate, + *hybrid_mouse, + *macos_drag, + )?, + ))) } HidBackendType::Bluetooth { config } => { #[cfg(target_os = "linux")] diff --git a/src/runtime/builder.rs b/src/runtime/builder.rs index 0add2d16..5b29affa 100644 --- a/src/runtime/builder.rs +++ b/src/runtime/builder.rs @@ -412,6 +412,7 @@ fn hid_backend_type(config: &AppConfig) -> HidBackendType { port: config.hid.ch9329_port.clone(), baud_rate: config.hid.ch9329_baudrate, hybrid_mouse: config.hid.ch9329_hybrid_mouse, + macos_drag: config.hid.ch9329_macos_drag, }, config::HidBackend::None => HidBackendType::None, config::HidBackend::Bluetooth => HidBackendType::Bluetooth { diff --git a/src/runtime/usb.rs b/src/runtime/usb.rs index 5cde110a..f57500eb 100644 --- a/src/runtime/usb.rs +++ b/src/runtime/usb.rs @@ -175,8 +175,8 @@ impl UsbCoordinator { old_config.constrained_otg_functions() != new_config.constrained_otg_functions(); let keyboard_leds_changed = old_config.effective_otg_keyboard_leds() != new_config.effective_otg_keyboard_leds(); - let ch9329_runtime_changed = - old_config.ch9329_hybrid_mouse != new_config.ch9329_hybrid_mouse; + let ch9329_runtime_changed = old_config.ch9329_hybrid_mouse != new_config.ch9329_hybrid_mouse + || old_config.ch9329_macos_drag != new_config.ch9329_macos_drag; if old_config.backend == new_config.backend && old_config.ch9329_port == new_config.ch9329_port @@ -314,6 +314,7 @@ fn hid_backend_type(config: &HidConfig) -> HidBackendType { port: config.ch9329_port.clone(), baud_rate: config.ch9329_baudrate, hybrid_mouse: config.ch9329_hybrid_mouse, + macos_drag: config.ch9329_macos_drag, }, HidBackend::None => HidBackendType::None, HidBackend::Bluetooth => HidBackendType::Bluetooth { diff --git a/src/web/handlers/config/types.rs b/src/web/handlers/config/types.rs index f988ee03..ef389ec9 100644 --- a/src/web/handlers/config/types.rs +++ b/src/web/handlers/config/types.rs @@ -409,6 +409,7 @@ pub struct HidConfigUpdate { pub ch9329_port: Option, pub ch9329_baudrate: Option, pub ch9329_hybrid_mouse: Option, + pub ch9329_macos_drag: Option, pub ch9329_descriptor: Option, pub otg_udc: Option, pub otg_descriptor: Option, @@ -465,6 +466,9 @@ impl HidConfigUpdate { if let Some(enabled) = self.ch9329_hybrid_mouse { config.ch9329_hybrid_mouse = enabled; } + if let Some(enabled) = self.ch9329_macos_drag { + config.ch9329_macos_drag = enabled; + } if let Some(ref desc) = self.ch9329_descriptor { desc.apply_to(&mut config.ch9329_descriptor); } diff --git a/src/web/handlers/config/usb_update.rs b/src/web/handlers/config/usb_update.rs index a5d73267..d2f4d2c0 100644 --- a/src/web/handlers/config/usb_update.rs +++ b/src/web/handlers/config/usb_update.rs @@ -284,6 +284,7 @@ mod tests { ch9329_port: None, ch9329_baudrate: None, ch9329_hybrid_mouse: None, + ch9329_macos_drag: None, ch9329_descriptor: None, otg_udc: None, otg_descriptor: None, diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index 99e30dea..335ad754 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -818,6 +818,8 @@ export default { ch9329OptionsDesc: 'Configure runtime compatibility for the CH9329 serial HID chip', ch9329HybridMouse: 'Linux Absolute Mouse Compatibility', ch9329HybridMouseDesc: 'Keep absolute movement on absolute packets, but send buttons and wheel through relative packets', + ch9329MacosDrag: 'macOS Drag Compatibility', + ch9329MacosDragDesc: 'Use absolute packets for button edges and relative packets for movement while a button is held', ch9329Descriptor: 'CH9329 USB Device Descriptor', ch9329DescriptorDesc: 'Read USB identification fields from the CH9329 chip before editing', ch9329DescriptorLoading: 'Reading CH9329 descriptor...', diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index c0b0f643..ce2ef60a 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -817,6 +817,8 @@ export default { ch9329OptionsDesc: '配置 CH9329 串口 HID 芯片的运行兼容性', ch9329HybridMouse: 'Linux 绝对鼠标兼容模式', ch9329HybridMouseDesc: '绝对移动仍使用绝对鼠标包,点击和滚轮改用相对鼠标包发送', + ch9329MacosDrag: 'macOS 拖拽兼容模式', + ch9329MacosDragDesc: '按钮按下与释放使用绝对鼠标包,按住期间改用相对鼠标包移动', ch9329Descriptor: 'CH9329 USB 设备描述符', ch9329DescriptorDesc: '先从 CH9329 芯片读取 USB 标识信息,读取成功后再修改', ch9329DescriptorLoading: '正在读取 CH9329 描述符...', diff --git a/web/src/types/generated.ts b/web/src/types/generated.ts index 5b47af16..c8ee290e 100644 --- a/web/src/types/generated.ts +++ b/web/src/types/generated.ts @@ -71,6 +71,7 @@ export interface HidConfig { ch9329_port: string; ch9329_baudrate: number; ch9329_hybrid_mouse?: boolean; + ch9329_macos_drag?: boolean; ch9329_descriptor?: Ch9329DescriptorConfig; mouse_absolute: boolean; } @@ -570,6 +571,7 @@ export interface HidConfigUpdate { ch9329_port?: string; ch9329_baudrate?: number; ch9329_hybrid_mouse?: boolean; + ch9329_macos_drag?: boolean; ch9329_descriptor?: Ch9329DescriptorConfigUpdate; otg_udc?: string; otg_descriptor?: OtgDescriptorConfigUpdate; diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index f076783d..033ad08b 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -692,6 +692,7 @@ const config = ref({ } as OtgHidFunctions, hid_otg_keyboard_leds: false, hid_ch9329_hybrid_mouse: false, + hid_ch9329_macos_drag: false, msd_enabled: false, msd_dir: '', msd_flash_inquiry_string: 'One-KVM Virtual Flash', @@ -1454,7 +1455,10 @@ async function saveConfig() { return } const hidUpdate: HidConfigUpdate = configStore.hid?.backend === 'ch9329' - ? { ch9329_hybrid_mouse: config.value.hid_ch9329_hybrid_mouse } : {} + ? { + ch9329_hybrid_mouse: config.value.hid_ch9329_hybrid_mouse, + ch9329_macos_drag: config.value.hid_ch9329_macos_drag, + } : {} if (config.value.hid_backend === 'ch9329' && isCh9329DescriptorDirty.value) { hidUpdate.ch9329_descriptor = { vendor_id: parseInt(ch9329VendorIdHex.value, 16) || 0x1a86, @@ -1524,7 +1528,7 @@ async function saveConfig() { const hidFeatureBaseline = ref('') function hidFeatureSnapshot() { return JSON.stringify({ - fields: Object.fromEntries(Object.entries(config.value).filter(([key]) => key.startsWith('msd_') || key.startsWith('otg_network_') || key.startsWith('uac_') || ['hid_otg_functions', 'hid_otg_keyboard_leds', 'hid_ch9329_hybrid_mouse'].includes(key))), + fields: Object.fromEntries(Object.entries(config.value).filter(([key]) => key.startsWith('msd_') || key.startsWith('otg_network_') || key.startsWith('uac_') || ['hid_otg_functions', 'hid_otg_keyboard_leds', 'hid_ch9329_hybrid_mouse', 'hid_ch9329_macos_drag'].includes(key))), descriptor: [otgVendorIdHex.value, otgProductIdHex.value, otgManufacturer.value, otgProduct.value, otgSerialNumber.value], }) } @@ -1566,6 +1570,7 @@ async function loadConfig() { } as OtgHidFunctions, hid_otg_keyboard_leds: hid.otg_keyboard_leds ?? false, hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false, + hid_ch9329_macos_drag: hid.ch9329_macos_drag ?? false, msd_enabled: msd.enabled || false, msd_dir: msd.msd_dir || '', msd_flash_inquiry_string: msd.flash_inquiry_string || 'One-KVM Virtual Flash', @@ -3220,6 +3225,13 @@ watch(isWindows, () => { +
+
+ +

{{ t('settings.ch9329MacosDragDesc') }}

+
+ +