mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 10:34:24 +08:00
fix(hid): share macOS drag compatibility across OTG and CH9329
This commit is contained in:
24
docs/macos-drag-compatibility.md
Normal file
24
docs/macos-drag-compatibility.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# macOS 拖拽兼容模式(实验性)
|
||||
|
||||
适用于 OTG 和 CH9329 后端的绝对鼠标输入。遇到目标 Mac 上拖拽只能移动一小段便停止时,可在设置页的 HID 配置中开启「macOS 拖拽兼容模式」。默认关闭。
|
||||
|
||||
配置字段为 `hid.mouse_macos_drag`,旧 PR 的 `ch9329_macos_drag` 字段仍可作为读取别名。OTG 必须同时启用相对鼠标和绝对鼠标接口,否则配置保存会被拒绝。CH9329 同时启用 Linux 兼容开关时,此模式优先。
|
||||
|
||||
## 报告行为
|
||||
|
||||
- 未按键时仍使用绝对坐标定位。
|
||||
- 从绝对输入开始的拖拽:按下使用绝对报告,移动和滚轮使用相对报告,释放时先更新相对按钮状态,再发送绝对按钮释放。
|
||||
- 按键报告的通道保持到所有按钮释放,不随拖拽期间的输入模式切换而改变。
|
||||
- 从相对输入开始的点击、拖拽继续使用相对按钮报告;此选项不修复原生相对模式的点击兼容问题。
|
||||
- 位移从原始 15 位输入坐标换算,累计小数余量,大位移按单包范围分包并保留总量及方向。
|
||||
- 运行时使用当前视频采集尺寸,并在 HID 重载后恢复尺寸;视频尚未提供尺寸时使用配置尺寸。
|
||||
|
||||
## 验证范围与限制
|
||||
|
||||
本次 OTG 扩展及位移换算修改没有 macOS 实机验证。原 PR 作者对旧 CH9329 实现的实机反馈不能视为本版本已经验证。
|
||||
|
||||
本地自动化覆盖位移累计/分包、1080p/4K 换算、往返移动、滚轮、输入模式切换、复位、CH9329 命令队列、OTG 临时文件模拟端点及配置校验。临时文件测试仅验证写出的字节,不验证 USB 枚举或目标系统的事件解释。
|
||||
|
||||
相对位移受 macOS 鼠标速度、加速、显示缩放及多屏布局影响,无法保证与绝对定位逐像素一致。松手报告携带最后的客户端绝对坐标,仍可能出现落点偏移或光标跳位;大位移分包也可能增加低波特率串口延迟。
|
||||
|
||||
后续实机验证应分别覆盖两个后端:窗口/文件/文本拖拽,快速及慢速移动,滚轮和多按钮组合,1080p/4K 与缩放显示,输入模式切换、断开重连、精确落点。异常时关闭此选项恢复默认行为。
|
||||
@@ -231,7 +231,8 @@ pub struct HidConfig {
|
||||
#[serde(default)]
|
||||
pub ch9329_hybrid_mouse: bool,
|
||||
#[serde(default)]
|
||||
pub ch9329_macos_drag: bool,
|
||||
#[serde(alias = "ch9329_macos_drag")]
|
||||
pub mouse_macos_drag: bool,
|
||||
#[serde(default)]
|
||||
pub ch9329_descriptor: Ch9329DescriptorConfig,
|
||||
pub mouse_absolute: bool,
|
||||
@@ -250,7 +251,7 @@ impl Default for HidConfig {
|
||||
ch9329_port: "/dev/ttyUSB0".to_string(),
|
||||
ch9329_baudrate: 9600,
|
||||
ch9329_hybrid_mouse: false,
|
||||
ch9329_macos_drag: false,
|
||||
mouse_macos_drag: false,
|
||||
ch9329_descriptor: Ch9329DescriptorConfig::default(),
|
||||
mouse_absolute: true,
|
||||
}
|
||||
@@ -276,6 +277,11 @@ impl HidConfig {
|
||||
}
|
||||
|
||||
let functions = self.effective_otg_functions();
|
||||
if self.mouse_macos_drag && (!functions.mouse_relative || !functions.mouse_absolute) {
|
||||
return Err(crate::error::AppError::BadRequest(
|
||||
"macOS drag compatibility requires both OTG mouse interfaces".to_string(),
|
||||
));
|
||||
}
|
||||
if functions.is_empty() {
|
||||
return Err(crate::error::AppError::BadRequest(
|
||||
"OTG HID functions cannot be empty".to_string(),
|
||||
@@ -311,6 +317,22 @@ impl HidConfig {
|
||||
mod bluetooth_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn mouse_compatibility_defaults_alias_and_otg_validation() {
|
||||
let defaults: HidConfig = serde_json::from_str(r#"{"backend":"otg"}"#).unwrap();
|
||||
assert!(!defaults.mouse_macos_drag);
|
||||
let mut config: HidConfig =
|
||||
serde_json::from_str(r#"{"backend":"otg","ch9329_macos_drag":true}"#).unwrap();
|
||||
assert!(config.mouse_macos_drag);
|
||||
assert!(config.validate_otg_functions().is_ok());
|
||||
config.otg_profile = OtgHidProfile::LegacyMouseRelative;
|
||||
assert!(config.validate_otg_functions().is_err());
|
||||
config.backend = HidBackend::Ch9329;
|
||||
assert!(config.validate_otg_functions().is_ok());
|
||||
let saved = serde_json::to_value(&config).unwrap();
|
||||
assert_eq!(saved["mouse_macos_drag"], true);
|
||||
assert!(saved.get("ch9329_macos_drag").is_none());
|
||||
}
|
||||
#[test]
|
||||
fn old_configs_keep_bluetooth_disabled_and_get_defaults() {
|
||||
let config: HidConfig = serde_json::from_str(r#"{"backend":"otg"}"#).unwrap();
|
||||
assert_eq!(config.backend, HidBackend::Otg);
|
||||
|
||||
@@ -17,7 +17,10 @@ fn default_ch9329_baud_rate() -> u32 {
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
#[derive(Default)]
|
||||
pub enum HidBackendType {
|
||||
Otg,
|
||||
Otg {
|
||||
#[serde(default)]
|
||||
macos_drag: bool,
|
||||
},
|
||||
Bluetooth {
|
||||
config: crate::config::BluetoothHidConfig,
|
||||
},
|
||||
@@ -37,7 +40,7 @@ pub enum HidBackendType {
|
||||
impl HidBackendType {
|
||||
pub fn name_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Otg => "otg",
|
||||
Self::Otg { .. } => "otg",
|
||||
Self::Bluetooth { .. } => "bluetooth",
|
||||
Self::Ch9329 { .. } => "ch9329",
|
||||
Self::None => "none",
|
||||
|
||||
@@ -239,6 +239,7 @@ pub struct Ch9329Backend {
|
||||
relative_mouse_active: Arc<AtomicBool>,
|
||||
hybrid_mouse: bool,
|
||||
macos_drag: bool,
|
||||
macos_drag_state: Mutex<super::macos_drag::MacosDrag>,
|
||||
runtime: Arc<Ch9329RuntimeState>,
|
||||
}
|
||||
|
||||
@@ -277,6 +278,7 @@ impl Ch9329Backend {
|
||||
relative_mouse_active: Arc::new(AtomicBool::new(false)),
|
||||
hybrid_mouse,
|
||||
macos_drag,
|
||||
macos_drag_state: Mutex::new(super::macos_drag::MacosDrag::default()),
|
||||
runtime: Arc::new(Ch9329RuntimeState::new()),
|
||||
})
|
||||
}
|
||||
@@ -1001,20 +1003,6 @@ impl Ch9329Backend {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -1298,6 +1286,31 @@ impl HidBackend for Ch9329Backend {
|
||||
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 (buttons, reports) = state.plan(event, buttons, *self.screen_resolution.read());
|
||||
self.mouse_buttons.store(buttons, Ordering::Relaxed);
|
||||
for report in reports {
|
||||
match report {
|
||||
MouseReport::Absolute { buttons, x, y } => {
|
||||
let x = (u32::from(x) * CH9329_MOUSE_RESOLUTION / 32768) as u16;
|
||||
let y = (u32::from(y) * CH9329_MOUSE_RESOLUTION / 32768) as u16;
|
||||
self.send_mouse_absolute(buttons, x, y, 0)?;
|
||||
}
|
||||
MouseReport::Relative {
|
||||
buttons,
|
||||
dx,
|
||||
dy,
|
||||
wheel,
|
||||
} => {
|
||||
self.send_mouse_relative(buttons, dx, dy, wheel)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match event.event_type {
|
||||
MouseEventType::Move => {
|
||||
self.relative_mouse_active.store(true, Ordering::Relaxed);
|
||||
@@ -1309,23 +1322,9 @@ 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;
|
||||
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)?;
|
||||
}
|
||||
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)?;
|
||||
}
|
||||
MouseEventType::Down => {
|
||||
if let Some(button) = event.button {
|
||||
@@ -1401,6 +1400,7 @@ impl HidBackend for Ch9329Backend {
|
||||
}
|
||||
|
||||
self.mouse_buttons.store(0, Ordering::Relaxed);
|
||||
self.macos_drag_state.lock().reset();
|
||||
self.last_abs_x.store(0, Ordering::Relaxed);
|
||||
self.last_abs_y.store(0, Ordering::Relaxed);
|
||||
self.relative_mouse_active.store(false, Ordering::Relaxed);
|
||||
@@ -1758,6 +1758,7 @@ mod tests {
|
||||
vec![0x02, 0x01, 0xE8, 0x03, 0xE8, 0x03, 0x00],
|
||||
),
|
||||
(cmd::SEND_MS_REL_DATA, vec![0x01, 0x01, 0x03, 0x02, 0x00]),
|
||||
(cmd::SEND_MS_REL_DATA, vec![0x01, 0x00, 0x00, 0x00, 0x00]),
|
||||
(
|
||||
cmd::SEND_MS_ABS_DATA,
|
||||
vec![0x02, 0x00, 0xF0, 0x03, 0xF0, 0x03, 0x00],
|
||||
@@ -1773,28 +1774,4 @@ 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ impl HidBackendFactory {
|
||||
|
||||
async fn create(&self, backend_type: &HidBackendType) -> Result<Option<Arc<dyn HidBackend>>> {
|
||||
match backend_type {
|
||||
HidBackendType::Otg => self.create_otg_backend().await.map(Some),
|
||||
HidBackendType::Otg { macos_drag } => {
|
||||
self.create_otg_backend(*macos_drag).await.map(Some)
|
||||
}
|
||||
HidBackendType::Ch9329 {
|
||||
port,
|
||||
baud_rate,
|
||||
@@ -88,7 +90,7 @@ impl HidBackendFactory {
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn create_otg_backend(&self) -> Result<Arc<dyn HidBackend>> {
|
||||
async fn create_otg_backend(&self, macos_drag: bool) -> Result<Arc<dyn HidBackend>> {
|
||||
let otg_service = self
|
||||
.otg_service
|
||||
.as_ref()
|
||||
@@ -100,11 +102,13 @@ impl HidBackendFactory {
|
||||
.ok_or_else(|| AppError::Config("OTG HID paths are not available".to_string()))?;
|
||||
|
||||
info!("Creating OTG HID backend from device paths");
|
||||
Ok(Arc::new(super::otg::OtgBackend::from_handles(handles)?))
|
||||
Ok(Arc::new(super::otg::OtgBackend::with_macos_drag(
|
||||
handles, macos_drag,
|
||||
)?))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
async fn create_otg_backend(&self) -> Result<Arc<dyn HidBackend>> {
|
||||
async fn create_otg_backend(&self, _macos_drag: bool) -> Result<Arc<dyn HidBackend>> {
|
||||
Err(AppError::Config(
|
||||
"OTG HID is only available on Linux".to_string(),
|
||||
))
|
||||
|
||||
256
src/hid/macos_drag.rs
Normal file
256
src/hid/macos_drag.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
//! Experimental absolute-button / relative-drag routing shared by USB backends.
|
||||
//! Relative counts are subject to host acceleration; they are not screen pixels.
|
||||
|
||||
use super::{MouseEvent, MouseEventType};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) enum MouseReport {
|
||||
Absolute {
|
||||
buttons: u8,
|
||||
x: u16,
|
||||
y: u16,
|
||||
},
|
||||
Relative {
|
||||
buttons: u8,
|
||||
dx: i8,
|
||||
dy: i8,
|
||||
wheel: i8,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct MacosDrag {
|
||||
position: (u16, u16),
|
||||
remainder: (i64, i64),
|
||||
extent: (u32, u32),
|
||||
relative_input: bool,
|
||||
absolute_drag: bool,
|
||||
}
|
||||
|
||||
impl MacosDrag {
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
pub fn plan(
|
||||
&mut self,
|
||||
event: MouseEvent,
|
||||
buttons: u8,
|
||||
extent: (u32, u32),
|
||||
) -> (u8, Vec<MouseReport>) {
|
||||
let mut reports = Vec::new();
|
||||
let mut next_buttons = buttons;
|
||||
if extent != self.extent || buttons == 0 {
|
||||
self.remainder = (0, 0);
|
||||
self.extent = extent;
|
||||
}
|
||||
match event.event_type {
|
||||
MouseEventType::MoveAbs => {
|
||||
let position = (
|
||||
event.x.clamp(0, 32767) as u16,
|
||||
event.y.clamp(0, 32767) as u16,
|
||||
);
|
||||
self.relative_input = false;
|
||||
if buttons != 0 {
|
||||
let dx =
|
||||
Self::delta(position.0, self.position.0, extent.0, &mut self.remainder.0);
|
||||
let dy =
|
||||
Self::delta(position.1, self.position.1, extent.1, &mut self.remainder.1);
|
||||
Self::motion(&mut reports, buttons, dx, dy);
|
||||
} else {
|
||||
reports.push(MouseReport::Absolute {
|
||||
buttons: 0,
|
||||
x: position.0,
|
||||
y: position.1,
|
||||
});
|
||||
}
|
||||
self.position = position;
|
||||
}
|
||||
MouseEventType::Move => {
|
||||
self.relative_input = true;
|
||||
Self::motion(
|
||||
&mut reports,
|
||||
buttons,
|
||||
i64::from(event.x.clamp(-127, 127)),
|
||||
i64::from(event.y.clamp(-127, 127)),
|
||||
);
|
||||
}
|
||||
MouseEventType::Down | MouseEventType::Up => {
|
||||
if let Some(button) = event.button {
|
||||
let down = event.event_type == MouseEventType::Down;
|
||||
next_buttons = if down {
|
||||
buttons | button.to_hid_bit()
|
||||
} else {
|
||||
buttons & !button.to_hid_bit()
|
||||
};
|
||||
if down && buttons == 0 {
|
||||
self.absolute_drag = !self.relative_input;
|
||||
}
|
||||
// Latch the button route until all buttons are released, even
|
||||
// if the client changes pointer mode during the drag.
|
||||
if self.absolute_drag {
|
||||
if !down {
|
||||
// Relative drag reports also carry buttons. Clear their
|
||||
// state before releasing the absolute collection.
|
||||
reports.push(MouseReport::Relative {
|
||||
buttons: next_buttons,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
wheel: 0,
|
||||
});
|
||||
}
|
||||
reports.push(MouseReport::Absolute {
|
||||
buttons: next_buttons,
|
||||
x: self.position.0,
|
||||
y: self.position.1,
|
||||
});
|
||||
} else {
|
||||
reports.push(MouseReport::Relative {
|
||||
buttons: next_buttons,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
wheel: 0,
|
||||
});
|
||||
}
|
||||
if next_buttons == 0 {
|
||||
self.absolute_drag = false;
|
||||
self.remainder = (0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseEventType::Scroll => {
|
||||
reports.push(MouseReport::Relative {
|
||||
buttons,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
wheel: event.scroll,
|
||||
});
|
||||
}
|
||||
}
|
||||
(next_buttons, reports)
|
||||
}
|
||||
|
||||
fn delta(current: u16, previous: u16, extent: u32, remainder: &mut i64) -> i64 {
|
||||
// Keep the original 15-bit input precision and carry fractional counts.
|
||||
let scaled =
|
||||
(i64::from(current) - i64::from(previous)) * i64::from(extent.max(1)) + *remainder;
|
||||
*remainder = scaled % 32768;
|
||||
scaled / 32768
|
||||
}
|
||||
|
||||
fn motion(reports: &mut Vec<MouseReport>, buttons: u8, dx: i64, dy: i64) {
|
||||
// Distribute both axes over the same packets to preserve diagonal paths.
|
||||
let count = (dx.abs().max(dy.abs()) + 126) / 127;
|
||||
let mut previous = (0, 0);
|
||||
for index in 1..=count {
|
||||
let position = (dx * index / count, dy * index / count);
|
||||
reports.push(MouseReport::Relative {
|
||||
buttons,
|
||||
dx: (position.0 - previous.0) as i8,
|
||||
dy: (position.1 - previous.1) as i8,
|
||||
wheel: 0,
|
||||
});
|
||||
previous = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hid::MouseButton;
|
||||
|
||||
fn drag(steps: &[i32], extent: (u32, u32)) -> (i64, i64) {
|
||||
let mut state = MacosDrag::default();
|
||||
state.plan(MouseEvent::move_abs(8000, 8000), 0, extent);
|
||||
state.plan(MouseEvent::button_down(MouseButton::Left), 0, extent);
|
||||
let mut total = (0, 0);
|
||||
for &x in steps {
|
||||
for report in state.plan(MouseEvent::move_abs(x, x), 1, extent).1 {
|
||||
if let MouseReport::Relative { dx, dy, .. } = report {
|
||||
total.0 += i64::from(dx);
|
||||
total.1 += i64::from(dy);
|
||||
} else {
|
||||
panic!("absolute movement during drag");
|
||||
}
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_total_across_event_splitting_and_large_moves() {
|
||||
let steps: Vec<_> = (8001..=16000).collect();
|
||||
assert_eq!(drag(&[16000], (1920, 1080)), (468, 263));
|
||||
assert_eq!(drag(&steps, (1920, 1080)), (468, 263));
|
||||
assert_eq!(drag(&[16000, 8000], (1920, 1080)), (0, 0));
|
||||
assert_eq!(drag(&[16000], (3840, 2160)), (937, 527));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_and_mode_switch_keep_absolute_drag_release() {
|
||||
let mut state = MacosDrag::default();
|
||||
let extent = (1920, 1080);
|
||||
state.plan(MouseEvent::move_abs(8000, 8000), 0, extent);
|
||||
state.plan(MouseEvent::button_down(MouseButton::Left), 0, extent);
|
||||
let mut wheel = MouseEvent::move_abs(0, 0);
|
||||
wheel.event_type = MouseEventType::Scroll;
|
||||
wheel.scroll = 1;
|
||||
assert_eq!(
|
||||
state.plan(wheel, 1, extent).1,
|
||||
vec![MouseReport::Relative {
|
||||
buttons: 1,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
wheel: 1
|
||||
}]
|
||||
);
|
||||
state.plan(MouseEvent::move_rel(10, 0), 1, extent);
|
||||
assert_eq!(
|
||||
state
|
||||
.plan(MouseEvent::button_up(MouseButton::Left), 1, extent)
|
||||
.1,
|
||||
vec![
|
||||
MouseReport::Relative {
|
||||
buttons: 0,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
wheel: 0
|
||||
},
|
||||
MouseReport::Absolute {
|
||||
buttons: 0,
|
||||
x: 8000,
|
||||
y: 8000
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_relative_clicks_and_reset() {
|
||||
let mut state = MacosDrag::default();
|
||||
state.plan(MouseEvent::move_rel(1, 1), 0, (1920, 1080));
|
||||
assert_eq!(
|
||||
state
|
||||
.plan(MouseEvent::button_down(MouseButton::Left), 0, (1920, 1080))
|
||||
.1,
|
||||
vec![MouseReport::Relative {
|
||||
buttons: 1,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
wheel: 0
|
||||
}]
|
||||
);
|
||||
state.reset();
|
||||
assert_eq!(
|
||||
state
|
||||
.plan(MouseEvent::button_down(MouseButton::Left), 0, (1920, 1080))
|
||||
.1,
|
||||
vec![MouseReport::Absolute {
|
||||
buttons: 1,
|
||||
x: 0,
|
||||
y: 0
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub mod consumer;
|
||||
pub mod datachannel;
|
||||
mod factory;
|
||||
pub mod keyboard;
|
||||
mod macos_drag;
|
||||
#[cfg(unix)]
|
||||
pub mod otg;
|
||||
#[cfg(unix)]
|
||||
@@ -135,6 +136,7 @@ pub struct HidController {
|
||||
runtime_worker: Mutex<Option<JoinHandle<()>>>,
|
||||
backend_available: Arc<AtomicBool>,
|
||||
reset_requested: Arc<AtomicBool>,
|
||||
screen_resolution: parking_lot::RwLock<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl HidController {
|
||||
@@ -157,6 +159,7 @@ impl HidController {
|
||||
runtime_worker: Mutex::new(None),
|
||||
backend_available: Arc::new(AtomicBool::new(false)),
|
||||
reset_requested: Arc::new(AtomicBool::new(false)),
|
||||
screen_resolution: parking_lot::RwLock::new((1920, 1080)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +182,7 @@ impl HidController {
|
||||
runtime_worker: Mutex::new(None),
|
||||
backend_available: Arc::new(AtomicBool::new(false)),
|
||||
reset_requested: Arc::new(AtomicBool::new(false)),
|
||||
screen_resolution: parking_lot::RwLock::new((1920, 1080)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +214,12 @@ impl HidController {
|
||||
}
|
||||
};
|
||||
|
||||
*self.backend.write().await = Some(backend);
|
||||
{
|
||||
let mut slot = self.backend.write().await;
|
||||
let (width, height) = *self.screen_resolution.read();
|
||||
backend.set_screen_resolution(width, height);
|
||||
*slot = Some(backend);
|
||||
}
|
||||
self.sync_runtime_state_from_backend().await;
|
||||
|
||||
self.start_event_worker().await;
|
||||
@@ -245,7 +254,8 @@ impl HidController {
|
||||
}
|
||||
|
||||
pub async fn prepare_otg_rebuild(&self) -> Result<()> {
|
||||
if !matches!(*self.backend_type.read().await, HidBackendType::Otg) {
|
||||
let backend_type = self.backend_type.read().await.clone();
|
||||
if !matches!(backend_type, HidBackendType::Otg { .. }) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -259,7 +269,7 @@ impl HidController {
|
||||
|
||||
let current = self.runtime_state.read().await.clone();
|
||||
let rebuilding_state = HidRuntimeState::with_error(
|
||||
&HidBackendType::Otg,
|
||||
&backend_type,
|
||||
¤t,
|
||||
"OTG gadget is rebuilding",
|
||||
"rebuilding",
|
||||
@@ -337,6 +347,24 @@ impl HidController {
|
||||
self.backend_type.read().await.clone()
|
||||
}
|
||||
|
||||
/// Keep the active capture dimensions across HID reloads and USB rebuilds.
|
||||
pub async fn set_screen_resolution(&self, width: u32, height: u32) {
|
||||
if width == 0 || height == 0 || width > 65535 || height > 65535 {
|
||||
return;
|
||||
}
|
||||
{
|
||||
let mut resolution = self.screen_resolution.write();
|
||||
if *resolution == (width, height) {
|
||||
return;
|
||||
}
|
||||
*resolution = (width, height);
|
||||
}
|
||||
if let Some(backend) = self.backend.read().await.as_ref() {
|
||||
let (width, height) = *self.screen_resolution.read();
|
||||
backend.set_screen_resolution(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn snapshot(&self) -> HidRuntimeState {
|
||||
self.runtime_state.read().await.clone()
|
||||
}
|
||||
@@ -403,7 +431,14 @@ impl HidController {
|
||||
}
|
||||
};
|
||||
|
||||
*self.backend.write().await = new_backend;
|
||||
{
|
||||
let mut slot = self.backend.write().await;
|
||||
if let Some(backend) = new_backend.as_ref() {
|
||||
let (width, height) = *self.screen_resolution.read();
|
||||
backend.set_screen_resolution(width, height);
|
||||
}
|
||||
*slot = new_backend;
|
||||
}
|
||||
|
||||
if matches!(new_backend_type, HidBackendType::None) {
|
||||
*self.backend_type.write().await = HidBackendType::None;
|
||||
@@ -687,6 +722,27 @@ fn merge_pending_move(pending: &mut Option<MouseEvent>, event: MouseEvent) {
|
||||
#[cfg(test)]
|
||||
mod queue_tests {
|
||||
use super::*;
|
||||
#[tokio::test]
|
||||
async fn screen_resolution_updates_backend_and_retains_valid_dimensions() {
|
||||
#[cfg(unix)]
|
||||
let controller = HidController::new(HidBackendType::None, None);
|
||||
#[cfg(not(unix))]
|
||||
let controller = HidController::new(HidBackendType::None);
|
||||
let backend = Arc::new(ch9329::Ch9329Backend::new("/dev/null").unwrap());
|
||||
*controller.backend.write().await = Some(backend.clone());
|
||||
controller.set_screen_resolution(3840, 2160).await;
|
||||
assert_eq!(
|
||||
backend.runtime_snapshot().screen_resolution,
|
||||
Some((3840, 2160))
|
||||
);
|
||||
controller.set_screen_resolution(0, 0).await;
|
||||
assert_eq!(*controller.screen_resolution.read(), (3840, 2160));
|
||||
controller.set_screen_resolution(1280, 720).await;
|
||||
assert_eq!(
|
||||
backend.runtime_snapshot().screen_resolution,
|
||||
Some((1280, 720))
|
||||
);
|
||||
}
|
||||
struct TestBackend {
|
||||
pressed: Arc<AtomicBool>,
|
||||
reset_done: Arc<tokio::sync::Notify>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -107,6 +107,8 @@ impl RuntimeBuilder {
|
||||
#[cfg(target_os = "linux")]
|
||||
hid.set_bond_store(config_store.hid_bonds());
|
||||
hid.set_event_bus(events.clone()).await;
|
||||
hid.set_screen_resolution(video_resolution.width, video_resolution.height)
|
||||
.await;
|
||||
if let Err(error) = hid.init().await {
|
||||
tracing::warn!("Failed to initialize HID backend: {}", error);
|
||||
}
|
||||
@@ -407,12 +409,14 @@ async fn build_otg(config: &AppConfig) -> Arc<OtgService> {
|
||||
|
||||
fn hid_backend_type(config: &AppConfig) -> HidBackendType {
|
||||
match config.hid.backend {
|
||||
config::HidBackend::Otg => HidBackendType::Otg,
|
||||
config::HidBackend::Otg => HidBackendType::Otg {
|
||||
macos_drag: config.hid.mouse_macos_drag,
|
||||
},
|
||||
config::HidBackend::Ch9329 => HidBackendType::Ch9329 {
|
||||
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,
|
||||
macos_drag: config.hid.mouse_macos_drag,
|
||||
},
|
||||
config::HidBackend::None => HidBackendType::None,
|
||||
config::HidBackend::Bluetooth => HidBackendType::Bluetooth {
|
||||
|
||||
@@ -175,14 +175,15 @@ 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
|
||||
|| old_config.ch9329_macos_drag != new_config.ch9329_macos_drag;
|
||||
let mouse_compatibility_changed = old_config.ch9329_hybrid_mouse
|
||||
!= new_config.ch9329_hybrid_mouse
|
||||
|| old_config.mouse_macos_drag != new_config.mouse_macos_drag;
|
||||
|
||||
if old_config.backend == new_config.backend
|
||||
&& old_config.ch9329_port == new_config.ch9329_port
|
||||
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
|
||||
&& old_config.bluetooth == new_config.bluetooth
|
||||
&& !ch9329_runtime_changed
|
||||
&& !mouse_compatibility_changed
|
||||
&& old_config.otg_udc == new_config.otg_udc
|
||||
&& !descriptor_changed
|
||||
&& !hid_functions_changed
|
||||
@@ -299,7 +300,7 @@ impl UsbCoordinator {
|
||||
|
||||
if hid_config.backend == HidBackend::Otg && (options.force || old_enabled != new_enabled) {
|
||||
self.hid
|
||||
.reload(HidBackendType::Otg)
|
||||
.reload(hid_backend_type(hid_config))
|
||||
.await
|
||||
.map_err(|error| AppError::Config(format!("OTG HID reload failed: {error}")))?;
|
||||
}
|
||||
@@ -309,12 +310,14 @@ impl UsbCoordinator {
|
||||
|
||||
fn hid_backend_type(config: &HidConfig) -> HidBackendType {
|
||||
match config.backend {
|
||||
HidBackend::Otg => HidBackendType::Otg,
|
||||
HidBackend::Otg => HidBackendType::Otg {
|
||||
macos_drag: config.mouse_macos_drag,
|
||||
},
|
||||
HidBackend::Ch9329 => HidBackendType::Ch9329 {
|
||||
port: config.ch9329_port.clone(),
|
||||
baud_rate: config.ch9329_baudrate,
|
||||
hybrid_mouse: config.ch9329_hybrid_mouse,
|
||||
macos_drag: config.ch9329_macos_drag,
|
||||
macos_drag: config.mouse_macos_drag,
|
||||
},
|
||||
HidBackend::None => HidBackendType::None,
|
||||
HidBackend::Bluetooth => HidBackendType::Bluetooth {
|
||||
|
||||
@@ -218,6 +218,11 @@ impl AppState {
|
||||
|
||||
pub async fn publish_device_info(&self) {
|
||||
let device_info = self.get_device_info().await;
|
||||
if let SystemEvent::DeviceInfo { video, .. } = &device_info {
|
||||
if let Some((width, height)) = video.resolution {
|
||||
self.hid.set_screen_resolution(width, height).await;
|
||||
}
|
||||
}
|
||||
let _ = self.device_info_tx.send(Some(device_info));
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,8 @@ pub struct HidConfigUpdate {
|
||||
pub ch9329_port: Option<String>,
|
||||
pub ch9329_baudrate: Option<u32>,
|
||||
pub ch9329_hybrid_mouse: Option<bool>,
|
||||
pub ch9329_macos_drag: Option<bool>,
|
||||
#[serde(alias = "ch9329_macos_drag")]
|
||||
pub mouse_macos_drag: Option<bool>,
|
||||
pub ch9329_descriptor: Option<Ch9329DescriptorConfigUpdate>,
|
||||
pub otg_udc: Option<String>,
|
||||
pub otg_descriptor: Option<OtgDescriptorConfigUpdate>,
|
||||
@@ -466,8 +467,8 @@ 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(enabled) = self.mouse_macos_drag {
|
||||
config.mouse_macos_drag = enabled;
|
||||
}
|
||||
if let Some(ref desc) = self.ch9329_descriptor {
|
||||
desc.apply_to(&mut config.ch9329_descriptor);
|
||||
|
||||
@@ -284,7 +284,7 @@ mod tests {
|
||||
ch9329_port: None,
|
||||
ch9329_baudrate: None,
|
||||
ch9329_hybrid_mouse: None,
|
||||
ch9329_macos_drag: None,
|
||||
mouse_macos_drag: None,
|
||||
ch9329_descriptor: None,
|
||||
otg_udc: None,
|
||||
otg_descriptor: None,
|
||||
|
||||
@@ -270,10 +270,8 @@ pub async fn msd_disk_mode_put(
|
||||
}));
|
||||
}
|
||||
|
||||
let hid_is_otg = matches!(
|
||||
state.hid.backend_type().await,
|
||||
crate::hid::HidBackendType::Otg
|
||||
);
|
||||
let hid_backend_type = state.hid.backend_type().await;
|
||||
let hid_is_otg = matches!(hid_backend_type, crate::hid::HidBackendType::Otg { .. });
|
||||
|
||||
if hid_is_otg {
|
||||
state
|
||||
@@ -294,7 +292,7 @@ pub async fn msd_disk_mode_put(
|
||||
let hid_reload_result = if hid_is_otg {
|
||||
state
|
||||
.hid
|
||||
.reload(crate::hid::HidBackendType::Otg)
|
||||
.reload(hid_backend_type)
|
||||
.await
|
||||
.map_err(|e| AppError::Config(format!("OTG HID reload failed: {e}")))
|
||||
} else {
|
||||
|
||||
@@ -818,8 +818,9 @@ 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',
|
||||
mouseMacosDrag: 'macOS Drag Compatibility',
|
||||
mouseMacosDragRequiresBoth: 'Enable both relative and absolute mouse interfaces before saving.',
|
||||
mouseMacosDragDesc: 'Experimental workaround for interrupted drags in absolute mouse mode (OTG / CH9329). OTG requires both mouse interfaces. Tracking speed and drop position may vary with macOS settings; this version has not been tested on a Mac.',
|
||||
ch9329Descriptor: 'CH9329 USB Device Descriptor',
|
||||
ch9329DescriptorDesc: 'Read USB identification fields from the CH9329 chip before editing',
|
||||
ch9329DescriptorLoading: 'Reading CH9329 descriptor...',
|
||||
|
||||
@@ -817,8 +817,9 @@ export default {
|
||||
ch9329OptionsDesc: '配置 CH9329 串口 HID 芯片的运行兼容性',
|
||||
ch9329HybridMouse: 'Linux 绝对鼠标兼容模式',
|
||||
ch9329HybridMouseDesc: '绝对移动仍使用绝对鼠标包,点击和滚轮改用相对鼠标包发送',
|
||||
ch9329MacosDrag: 'macOS 拖拽兼容模式',
|
||||
ch9329MacosDragDesc: '按钮按下与释放使用绝对鼠标包,按住期间改用相对鼠标包移动',
|
||||
mouseMacosDrag: 'macOS 拖拽兼容模式',
|
||||
mouseMacosDragRequiresBoth: '请同时启用相对鼠标和绝对鼠标接口后保存。',
|
||||
mouseMacosDragDesc: '实验性缓解绝对鼠标模式下的拖拽中断,适用于 OTG / CH9329。OTG 需同时启用两种鼠标接口。移动速度和落点可能受 macOS 设置影响;本版本尚未进行 Mac 实机验证。',
|
||||
ch9329Descriptor: 'CH9329 USB 设备描述符',
|
||||
ch9329DescriptorDesc: '先从 CH9329 芯片读取 USB 标识信息,读取成功后再修改',
|
||||
ch9329DescriptorLoading: '正在读取 CH9329 描述符...',
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface HidConfig {
|
||||
ch9329_port: string;
|
||||
ch9329_baudrate: number;
|
||||
ch9329_hybrid_mouse?: boolean;
|
||||
ch9329_macos_drag?: boolean;
|
||||
mouse_macos_drag?: boolean;
|
||||
ch9329_descriptor?: Ch9329DescriptorConfig;
|
||||
mouse_absolute: boolean;
|
||||
}
|
||||
@@ -571,7 +571,7 @@ export interface HidConfigUpdate {
|
||||
ch9329_port?: string;
|
||||
ch9329_baudrate?: number;
|
||||
ch9329_hybrid_mouse?: boolean;
|
||||
ch9329_macos_drag?: boolean;
|
||||
mouse_macos_drag?: boolean;
|
||||
ch9329_descriptor?: Ch9329DescriptorConfigUpdate;
|
||||
otg_udc?: string;
|
||||
otg_descriptor?: OtgDescriptorConfigUpdate;
|
||||
|
||||
@@ -692,7 +692,7 @@ const config = ref({
|
||||
} as OtgHidFunctions,
|
||||
hid_otg_keyboard_leds: false,
|
||||
hid_ch9329_hybrid_mouse: false,
|
||||
hid_ch9329_macos_drag: false,
|
||||
hid_mouse_macos_drag: false,
|
||||
msd_enabled: false,
|
||||
msd_dir: '',
|
||||
msd_flash_inquiry_string: 'One-KVM Virtual Flash',
|
||||
@@ -1177,6 +1177,8 @@ const isCh9329DescriptorDirty = computed(() => {
|
||||
|
||||
const isHidSettingsValid = computed(() =>
|
||||
isHidFunctionSelectionValid.value
|
||||
&& !(config.value.hid_backend === 'otg' && config.value.hid_mouse_macos_drag
|
||||
&& (!effectiveOtgFunctions.value.mouse_relative || !effectiveOtgFunctions.value.mouse_absolute))
|
||||
&& isCh9329DescriptorValid.value
|
||||
&& areMsdInquiryStringsValid.value
|
||||
)
|
||||
@@ -1457,8 +1459,10 @@ async function saveConfig() {
|
||||
const hidUpdate: HidConfigUpdate = configStore.hid?.backend === 'ch9329'
|
||||
? {
|
||||
ch9329_hybrid_mouse: config.value.hid_ch9329_hybrid_mouse,
|
||||
ch9329_macos_drag: config.value.hid_ch9329_macos_drag,
|
||||
} : {}
|
||||
if (['otg', 'ch9329'].includes(config.value.hid_backend)) {
|
||||
hidUpdate.mouse_macos_drag = config.value.hid_mouse_macos_drag
|
||||
}
|
||||
if (config.value.hid_backend === 'ch9329' && isCh9329DescriptorDirty.value) {
|
||||
hidUpdate.ch9329_descriptor = {
|
||||
vendor_id: parseInt(ch9329VendorIdHex.value, 16) || 0x1a86,
|
||||
@@ -1528,7 +1532,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', 'hid_ch9329_macos_drag'].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_mouse_macos_drag'].includes(key))),
|
||||
descriptor: [otgVendorIdHex.value, otgProductIdHex.value, otgManufacturer.value, otgProduct.value, otgSerialNumber.value],
|
||||
})
|
||||
}
|
||||
@@ -1570,7 +1574,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,
|
||||
hid_mouse_macos_drag: hid.mouse_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',
|
||||
@@ -3225,18 +3229,19 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
<Switch v-model="config.hid_ch9329_hybrid_mouse" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Label>{{ t('settings.ch9329MacosDrag') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.ch9329MacosDragDesc') }}</p>
|
||||
</div>
|
||||
<Switch v-model="config.hid_ch9329_macos_drag" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- OTG Descriptor Settings -->
|
||||
<div v-if="['otg', 'ch9329'].includes(config.hid_backend)" class="flex items-center justify-between gap-4 rounded-md border border-border/60 p-3">
|
||||
<div>
|
||||
<Label>{{ t('settings.mouseMacosDrag') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.mouseMacosDragDesc') }}</p>
|
||||
<p v-if="config.hid_backend === 'otg' && config.hid_mouse_macos_drag && (!effectiveOtgFunctions.mouse_relative || !effectiveOtgFunctions.mouse_absolute)" class="text-xs text-warning">{{ t('settings.mouseMacosDragRequiresBoth') }}</p>
|
||||
</div>
|
||||
<Switch v-model="config.hid_mouse_macos_drag" />
|
||||
</div>
|
||||
<template v-if="config.hid_backend === 'otg'">
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user