Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -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,
}
Expand All @@ -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,
}
Expand Down
2 changes: 2 additions & 0 deletions src/hid/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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 @@ -238,6 +238,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 @@ -251,6 +252,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 @@ -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()),
})
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1666,18 +1706,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 @@ -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")]
Expand Down
1 change: 1 addition & 0 deletions src/runtime/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/usb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
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 @@ -409,6 +409,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 @@ -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);
}
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 @@ -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,
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 @@ -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...',
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 @@ -817,6 +817,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 @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions web/src/views/SettingsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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],
})
}
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -3220,6 +3225,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