Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/config/schema/hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,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,
}
Expand All @@ -197,6 +199,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,
}
Expand Down
2 changes: 2 additions & 0 deletions src/hid/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub enum HidBackendType {
baud_rate: u32,
#[serde(default)]
hybrid_mouse: bool,
#[serde(default)]
macos_drag: bool,
},
#[default]
None,
Expand Down
129 changes: 123 additions & 6 deletions src/hid/ch9329.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ pub struct Ch9329Backend {
last_abs_y: Arc<AtomicU16>,
relative_mouse_active: Arc<AtomicBool>,
hybrid_mouse: bool,
macos_drag: bool,
runtime: Arc<Ch9329RuntimeState>,
}

Expand All @@ -248,6 +249,15 @@ impl Ch9329Backend {
}

pub fn with_options(port_path: &str, baud_rate: u32, hybrid_mouse: bool) -> Result<Self> {
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<Self> {
Ok(Self {
port_path: port_path.to_string(),
baud_rate,
Expand All @@ -263,6 +273,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()),
})
}
Expand Down Expand Up @@ -965,17 +976,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,
Expand Down Expand Up @@ -1267,9 +1293,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 {
Expand Down Expand Up @@ -1650,18 +1690,95 @@ 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();

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
);
}
}
18 changes: 11 additions & 7 deletions src/hid/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,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::None => {
warn!("HID backend disabled");
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ async fn main() -> anyhow::Result<()> {
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,
};
Expand Down
4 changes: 3 additions & 1 deletion src/web/handlers/config/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fn hid_backend_type(config: &HidConfig) -> crate::hid::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 => crate::hid::HidBackendType::None,
}
Expand Down Expand Up @@ -206,7 +207,8 @@ pub async fn apply_hid_config(
let hid_functions_changed = old_hid_functions != new_hid_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
Expand Down
4 changes: 4 additions & 0 deletions src/web/handlers/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ 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>,
pub ch9329_descriptor: Option<Ch9329DescriptorConfigUpdate>,
pub otg_udc: Option<String>,
pub otg_descriptor: Option<OtgDescriptorConfigUpdate>,
Expand Down Expand Up @@ -456,6 +457,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);
}
Expand Down
1 change: 1 addition & 0 deletions src/web/handlers/config/usb_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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,
Expand Down
2 changes: 2 additions & 0 deletions web/src/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,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...',
Expand Down
2 changes: 2 additions & 0 deletions web/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,8 @@ export default {
ch9329OptionsDesc: '配置 CH9329 串口 HID 芯片的运行兼容性',
ch9329HybridMouse: 'Linux 绝对鼠标兼容模式',
ch9329HybridMouseDesc: '绝对移动仍使用绝对鼠标包,点击和滚轮改用相对鼠标包发送',
ch9329MacosDrag: 'macOS 拖拽兼容模式',
ch9329MacosDragDesc: '按钮按下与释放使用绝对鼠标包,按住期间改用相对鼠标包移动',
ch9329Descriptor: 'CH9329 USB 设备描述符',
ch9329DescriptorDesc: '先从 CH9329 芯片读取 USB 标识信息,读取成功后再修改',
ch9329DescriptorLoading: '正在读取 CH9329 描述符...',
Expand Down
2 changes: 2 additions & 0 deletions web/src/types/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface HidConfig {
ch9329_port: string;
ch9329_baudrate: number;
ch9329_hybrid_mouse?: boolean;
ch9329_macos_drag?: boolean;
ch9329_descriptor?: Ch9329DescriptorConfig;
mouse_absolute: boolean;
}
Expand Down Expand Up @@ -546,6 +547,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;
Expand Down
10 changes: 10 additions & 0 deletions web/src/views/SettingsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,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',
Expand Down Expand Up @@ -1434,6 +1435,7 @@ async function saveConfig() {
ch9329_port: config.value.hid_serial_device || undefined,
ch9329_baudrate: config.value.hid_serial_baudrate,
ch9329_hybrid_mouse: config.value.hid_ch9329_hybrid_mouse,
ch9329_macos_drag: config.value.hid_ch9329_macos_drag,
otg_udc: config.value.hid_otg_udc,
}
if (config.value.hid_backend === 'ch9329' && isCh9329DescriptorDirty.value) {
Expand Down Expand Up @@ -1533,6 +1535,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',
Expand Down Expand Up @@ -3198,6 +3201,13 @@ 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>
Expand Down