From e09a906f9303cee24aebd0625288b1fad8e9f87c Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Thu, 26 Mar 2026 22:51:29 +0800 Subject: [PATCH] =?UTF-8?q?=20refactor(hid):=20=E7=BB=9F=E4=B8=80=20HID=20?= =?UTF-8?q?=E9=94=AE=E7=9B=98=20CanonicalKey=20=E8=AF=AD=E4=B9=89=E5=B9=B6?= =?UTF-8?q?=E6=B8=85=E7=90=86=E5=89=8D=E7=AB=AF=E5=B8=83=E5=B1=80=E4=B8=8E?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E9=93=BE=E8=B7=AF=E5=86=97=E4=BD=99=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hid/ch9329.rs | 12 +- src/hid/datachannel.rs | 22 +- src/hid/keyboard.rs | 409 +++++++++++++++++++++ src/hid/keymap.rs | 430 ---------------------- src/hid/mod.rs | 3 +- src/hid/otg.rs | 12 +- src/hid/types.rs | 19 +- src/rustdesk/connection.rs | 10 +- src/rustdesk/hid_adapter.rs | 10 +- web/src/api/index.ts | 3 +- web/src/components/InfoBar.vue | 6 +- web/src/components/PasteModal.vue | 10 +- web/src/components/VirtualKeyboard.vue | 141 +++++--- web/src/composables/useHidInput.ts | 343 ------------------ web/src/lib/charToHid.ts | 201 +++++------ web/src/lib/keyboardLayouts.ts | 221 +----------- web/src/lib/keyboardMappings.ts | 475 ++++++++++++++----------- web/src/types/generated.ts | 142 +++++++- web/src/types/hid.ts | 7 +- web/src/views/ConsoleView.vue | 45 +-- 20 files changed, 1083 insertions(+), 1438 deletions(-) create mode 100644 src/hid/keyboard.rs delete mode 100644 src/hid/keymap.rs delete mode 100644 web/src/composables/useHidInput.ts diff --git a/src/hid/ch9329.rs b/src/hid/ch9329.rs index b889d148..091cc4e8 100644 --- a/src/hid/ch9329.rs +++ b/src/hid/ch9329.rs @@ -28,7 +28,6 @@ use std::time::{Duration, Instant}; use tracing::{info, trace, warn}; use super::backend::{HidBackend, HidBackendStatus}; -use super::keymap; use super::types::{KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType}; use crate::error::{AppError, Result}; @@ -1095,18 +1094,13 @@ impl HidBackend for Ch9329Backend { } async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> { - // Convert JS keycode to USB HID if needed (skip if already USB HID) - let usb_key = if event.is_usb_hid { - event.key - } else { - keymap::js_to_usb(event.key).unwrap_or(event.key) - }; + let usb_key = event.key.to_hid_usage(); // Handle modifier keys separately - if keymap::is_modifier_key(usb_key) { + if event.key.is_modifier() { let mut state = self.keyboard_state.lock(); - if let Some(bit) = keymap::modifier_bit(usb_key) { + if let Some(bit) = event.key.modifier_bit() { match event.event_type { KeyEventType::Down => state.modifiers |= bit, KeyEventType::Up => state.modifiers &= !bit, diff --git a/src/hid/datachannel.rs b/src/hid/datachannel.rs index 04c76d21..95401da3 100644 --- a/src/hid/datachannel.rs +++ b/src/hid/datachannel.rs @@ -9,7 +9,7 @@ //! //! Keyboard event (type 0x01): //! - Byte 1: Event type (0x00 = down, 0x01 = up) -//! - Byte 2: Key code (USB HID usage code) +//! - Byte 2: Canonical key code (stable One-KVM key id aligned with HID usage) //! - Byte 3: Modifiers bitmask //! - Bit 0: Left Ctrl //! - Bit 1: Left Shift @@ -38,7 +38,8 @@ use tracing::warn; use super::types::ConsumerEvent; use super::{ - KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent, MouseEventType, + CanonicalKey, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent, + MouseEventType, }; /// Message types @@ -101,7 +102,13 @@ fn parse_keyboard_message(data: &[u8]) -> Option { } }; - let key = data[1]; + let key = match CanonicalKey::from_hid_usage(data[1]) { + Some(key) => key, + None => { + warn!("Unknown canonical keyboard key code: 0x{:02X}", data[1]); + return None; + } + }; let modifiers_byte = data[2]; let modifiers = KeyboardModifiers { @@ -119,7 +126,6 @@ fn parse_keyboard_message(data: &[u8]) -> Option { event_type, key, modifiers, - is_usb_hid: true, // WebRTC/WebSocket HID channel sends USB HID usages })) } @@ -193,7 +199,7 @@ pub fn encode_keyboard_event(event: &KeyboardEvent) -> Vec { let modifiers = event.modifiers.to_hid_byte(); - vec![MSG_KEYBOARD, event_type, event.key, modifiers] + vec![MSG_KEYBOARD, event_type, event.key.to_hid_usage(), modifiers] } /// Encode a mouse event to binary format (for sending to client if needed) @@ -242,10 +248,9 @@ mod tests { match event { HidChannelEvent::Keyboard(kb) => { assert!(matches!(kb.event_type, KeyEventType::Down)); - assert_eq!(kb.key, 0x04); + assert_eq!(kb.key, CanonicalKey::KeyA); assert!(kb.modifiers.left_ctrl); assert!(!kb.modifiers.left_shift); - assert!(kb.is_usb_hid); } _ => panic!("Expected keyboard event"), } @@ -270,7 +275,7 @@ mod tests { fn test_encode_keyboard() { let event = KeyboardEvent { event_type: KeyEventType::Down, - key: 0x04, + key: CanonicalKey::KeyA, modifiers: KeyboardModifiers { left_ctrl: true, left_shift: false, @@ -281,7 +286,6 @@ mod tests { right_alt: false, right_meta: false, }, - is_usb_hid: true, }; let encoded = encode_keyboard_event(&event); diff --git a/src/hid/keyboard.rs b/src/hid/keyboard.rs new file mode 100644 index 00000000..f3c24038 --- /dev/null +++ b/src/hid/keyboard.rs @@ -0,0 +1,409 @@ +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +/// Shared canonical keyboard key identifiers used across frontend and backend. +/// +/// The enum names intentionally mirror `KeyboardEvent.code` style values so the +/// browser, virtual keyboard, and HID backend can all speak the same language. +#[typeshare] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum CanonicalKey { + KeyA, + KeyB, + KeyC, + KeyD, + KeyE, + KeyF, + KeyG, + KeyH, + KeyI, + KeyJ, + KeyK, + KeyL, + KeyM, + KeyN, + KeyO, + KeyP, + KeyQ, + KeyR, + KeyS, + KeyT, + KeyU, + KeyV, + KeyW, + KeyX, + KeyY, + KeyZ, + Digit1, + Digit2, + Digit3, + Digit4, + Digit5, + Digit6, + Digit7, + Digit8, + Digit9, + Digit0, + Enter, + Escape, + Backspace, + Tab, + Space, + Minus, + Equal, + BracketLeft, + BracketRight, + Backslash, + Semicolon, + Quote, + Backquote, + Comma, + Period, + Slash, + CapsLock, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + PrintScreen, + ScrollLock, + Pause, + Insert, + Home, + PageUp, + Delete, + End, + PageDown, + ArrowRight, + ArrowLeft, + ArrowDown, + ArrowUp, + NumLock, + NumpadDivide, + NumpadMultiply, + NumpadSubtract, + NumpadAdd, + NumpadEnter, + Numpad1, + Numpad2, + Numpad3, + Numpad4, + Numpad5, + Numpad6, + Numpad7, + Numpad8, + Numpad9, + Numpad0, + NumpadDecimal, + IntlBackslash, + ContextMenu, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + F21, + F22, + F23, + F24, + ControlLeft, + ShiftLeft, + AltLeft, + MetaLeft, + ControlRight, + ShiftRight, + AltRight, + MetaRight, +} + +impl CanonicalKey { + /// Convert the canonical key to a stable wire code. + /// + /// The wire code intentionally matches the USB HID usage for keyboard page + /// keys so existing low-level behavior stays intact while the semantic type + /// becomes explicit. + pub const fn to_hid_usage(self) -> u8 { + match self { + Self::KeyA => 0x04, + Self::KeyB => 0x05, + Self::KeyC => 0x06, + Self::KeyD => 0x07, + Self::KeyE => 0x08, + Self::KeyF => 0x09, + Self::KeyG => 0x0A, + Self::KeyH => 0x0B, + Self::KeyI => 0x0C, + Self::KeyJ => 0x0D, + Self::KeyK => 0x0E, + Self::KeyL => 0x0F, + Self::KeyM => 0x10, + Self::KeyN => 0x11, + Self::KeyO => 0x12, + Self::KeyP => 0x13, + Self::KeyQ => 0x14, + Self::KeyR => 0x15, + Self::KeyS => 0x16, + Self::KeyT => 0x17, + Self::KeyU => 0x18, + Self::KeyV => 0x19, + Self::KeyW => 0x1A, + Self::KeyX => 0x1B, + Self::KeyY => 0x1C, + Self::KeyZ => 0x1D, + Self::Digit1 => 0x1E, + Self::Digit2 => 0x1F, + Self::Digit3 => 0x20, + Self::Digit4 => 0x21, + Self::Digit5 => 0x22, + Self::Digit6 => 0x23, + Self::Digit7 => 0x24, + Self::Digit8 => 0x25, + Self::Digit9 => 0x26, + Self::Digit0 => 0x27, + Self::Enter => 0x28, + Self::Escape => 0x29, + Self::Backspace => 0x2A, + Self::Tab => 0x2B, + Self::Space => 0x2C, + Self::Minus => 0x2D, + Self::Equal => 0x2E, + Self::BracketLeft => 0x2F, + Self::BracketRight => 0x30, + Self::Backslash => 0x31, + Self::Semicolon => 0x33, + Self::Quote => 0x34, + Self::Backquote => 0x35, + Self::Comma => 0x36, + Self::Period => 0x37, + Self::Slash => 0x38, + Self::CapsLock => 0x39, + Self::F1 => 0x3A, + Self::F2 => 0x3B, + Self::F3 => 0x3C, + Self::F4 => 0x3D, + Self::F5 => 0x3E, + Self::F6 => 0x3F, + Self::F7 => 0x40, + Self::F8 => 0x41, + Self::F9 => 0x42, + Self::F10 => 0x43, + Self::F11 => 0x44, + Self::F12 => 0x45, + Self::PrintScreen => 0x46, + Self::ScrollLock => 0x47, + Self::Pause => 0x48, + Self::Insert => 0x49, + Self::Home => 0x4A, + Self::PageUp => 0x4B, + Self::Delete => 0x4C, + Self::End => 0x4D, + Self::PageDown => 0x4E, + Self::ArrowRight => 0x4F, + Self::ArrowLeft => 0x50, + Self::ArrowDown => 0x51, + Self::ArrowUp => 0x52, + Self::NumLock => 0x53, + Self::NumpadDivide => 0x54, + Self::NumpadMultiply => 0x55, + Self::NumpadSubtract => 0x56, + Self::NumpadAdd => 0x57, + Self::NumpadEnter => 0x58, + Self::Numpad1 => 0x59, + Self::Numpad2 => 0x5A, + Self::Numpad3 => 0x5B, + Self::Numpad4 => 0x5C, + Self::Numpad5 => 0x5D, + Self::Numpad6 => 0x5E, + Self::Numpad7 => 0x5F, + Self::Numpad8 => 0x60, + Self::Numpad9 => 0x61, + Self::Numpad0 => 0x62, + Self::NumpadDecimal => 0x63, + Self::IntlBackslash => 0x64, + Self::ContextMenu => 0x65, + Self::F13 => 0x68, + Self::F14 => 0x69, + Self::F15 => 0x6A, + Self::F16 => 0x6B, + Self::F17 => 0x6C, + Self::F18 => 0x6D, + Self::F19 => 0x6E, + Self::F20 => 0x6F, + Self::F21 => 0x70, + Self::F22 => 0x71, + Self::F23 => 0x72, + Self::F24 => 0x73, + Self::ControlLeft => 0xE0, + Self::ShiftLeft => 0xE1, + Self::AltLeft => 0xE2, + Self::MetaLeft => 0xE3, + Self::ControlRight => 0xE4, + Self::ShiftRight => 0xE5, + Self::AltRight => 0xE6, + Self::MetaRight => 0xE7, + } + } + + /// Convert a wire code / USB HID usage to its canonical key. + pub const fn from_hid_usage(usage: u8) -> Option { + match usage { + 0x04 => Some(Self::KeyA), + 0x05 => Some(Self::KeyB), + 0x06 => Some(Self::KeyC), + 0x07 => Some(Self::KeyD), + 0x08 => Some(Self::KeyE), + 0x09 => Some(Self::KeyF), + 0x0A => Some(Self::KeyG), + 0x0B => Some(Self::KeyH), + 0x0C => Some(Self::KeyI), + 0x0D => Some(Self::KeyJ), + 0x0E => Some(Self::KeyK), + 0x0F => Some(Self::KeyL), + 0x10 => Some(Self::KeyM), + 0x11 => Some(Self::KeyN), + 0x12 => Some(Self::KeyO), + 0x13 => Some(Self::KeyP), + 0x14 => Some(Self::KeyQ), + 0x15 => Some(Self::KeyR), + 0x16 => Some(Self::KeyS), + 0x17 => Some(Self::KeyT), + 0x18 => Some(Self::KeyU), + 0x19 => Some(Self::KeyV), + 0x1A => Some(Self::KeyW), + 0x1B => Some(Self::KeyX), + 0x1C => Some(Self::KeyY), + 0x1D => Some(Self::KeyZ), + 0x1E => Some(Self::Digit1), + 0x1F => Some(Self::Digit2), + 0x20 => Some(Self::Digit3), + 0x21 => Some(Self::Digit4), + 0x22 => Some(Self::Digit5), + 0x23 => Some(Self::Digit6), + 0x24 => Some(Self::Digit7), + 0x25 => Some(Self::Digit8), + 0x26 => Some(Self::Digit9), + 0x27 => Some(Self::Digit0), + 0x28 => Some(Self::Enter), + 0x29 => Some(Self::Escape), + 0x2A => Some(Self::Backspace), + 0x2B => Some(Self::Tab), + 0x2C => Some(Self::Space), + 0x2D => Some(Self::Minus), + 0x2E => Some(Self::Equal), + 0x2F => Some(Self::BracketLeft), + 0x30 => Some(Self::BracketRight), + 0x31 => Some(Self::Backslash), + 0x33 => Some(Self::Semicolon), + 0x34 => Some(Self::Quote), + 0x35 => Some(Self::Backquote), + 0x36 => Some(Self::Comma), + 0x37 => Some(Self::Period), + 0x38 => Some(Self::Slash), + 0x39 => Some(Self::CapsLock), + 0x3A => Some(Self::F1), + 0x3B => Some(Self::F2), + 0x3C => Some(Self::F3), + 0x3D => Some(Self::F4), + 0x3E => Some(Self::F5), + 0x3F => Some(Self::F6), + 0x40 => Some(Self::F7), + 0x41 => Some(Self::F8), + 0x42 => Some(Self::F9), + 0x43 => Some(Self::F10), + 0x44 => Some(Self::F11), + 0x45 => Some(Self::F12), + 0x46 => Some(Self::PrintScreen), + 0x47 => Some(Self::ScrollLock), + 0x48 => Some(Self::Pause), + 0x49 => Some(Self::Insert), + 0x4A => Some(Self::Home), + 0x4B => Some(Self::PageUp), + 0x4C => Some(Self::Delete), + 0x4D => Some(Self::End), + 0x4E => Some(Self::PageDown), + 0x4F => Some(Self::ArrowRight), + 0x50 => Some(Self::ArrowLeft), + 0x51 => Some(Self::ArrowDown), + 0x52 => Some(Self::ArrowUp), + 0x53 => Some(Self::NumLock), + 0x54 => Some(Self::NumpadDivide), + 0x55 => Some(Self::NumpadMultiply), + 0x56 => Some(Self::NumpadSubtract), + 0x57 => Some(Self::NumpadAdd), + 0x58 => Some(Self::NumpadEnter), + 0x59 => Some(Self::Numpad1), + 0x5A => Some(Self::Numpad2), + 0x5B => Some(Self::Numpad3), + 0x5C => Some(Self::Numpad4), + 0x5D => Some(Self::Numpad5), + 0x5E => Some(Self::Numpad6), + 0x5F => Some(Self::Numpad7), + 0x60 => Some(Self::Numpad8), + 0x61 => Some(Self::Numpad9), + 0x62 => Some(Self::Numpad0), + 0x63 => Some(Self::NumpadDecimal), + 0x64 => Some(Self::IntlBackslash), + 0x65 => Some(Self::ContextMenu), + 0x68 => Some(Self::F13), + 0x69 => Some(Self::F14), + 0x6A => Some(Self::F15), + 0x6B => Some(Self::F16), + 0x6C => Some(Self::F17), + 0x6D => Some(Self::F18), + 0x6E => Some(Self::F19), + 0x6F => Some(Self::F20), + 0x70 => Some(Self::F21), + 0x71 => Some(Self::F22), + 0x72 => Some(Self::F23), + 0x73 => Some(Self::F24), + 0xE0 => Some(Self::ControlLeft), + 0xE1 => Some(Self::ShiftLeft), + 0xE2 => Some(Self::AltLeft), + 0xE3 => Some(Self::MetaLeft), + 0xE4 => Some(Self::ControlRight), + 0xE5 => Some(Self::ShiftRight), + 0xE6 => Some(Self::AltRight), + 0xE7 => Some(Self::MetaRight), + _ => None, + } + } + + pub const fn is_modifier(self) -> bool { + matches!( + self, + Self::ControlLeft + | Self::ShiftLeft + | Self::AltLeft + | Self::MetaLeft + | Self::ControlRight + | Self::ShiftRight + | Self::AltRight + | Self::MetaRight + ) + } + + pub const fn modifier_bit(self) -> Option { + match self { + Self::ControlLeft => Some(0x01), + Self::ShiftLeft => Some(0x02), + Self::AltLeft => Some(0x04), + Self::MetaLeft => Some(0x08), + Self::ControlRight => Some(0x10), + Self::ShiftRight => Some(0x20), + Self::AltRight => Some(0x40), + Self::MetaRight => Some(0x80), + _ => None, + } + } +} diff --git a/src/hid/keymap.rs b/src/hid/keymap.rs deleted file mode 100644 index 7d570e33..00000000 --- a/src/hid/keymap.rs +++ /dev/null @@ -1,430 +0,0 @@ -//! USB HID keyboard key codes mapping -//! -//! This module provides mapping between JavaScript key codes and USB HID usage codes. -//! Reference: USB HID Usage Tables 1.12, Section 10 (Keyboard/Keypad Page) - -/// USB HID key codes (Usage Page 0x07) -#[allow(dead_code)] -pub mod usb { - // Letters A-Z (0x04 - 0x1D) - pub const KEY_A: u8 = 0x04; - pub const KEY_B: u8 = 0x05; - pub const KEY_C: u8 = 0x06; - pub const KEY_D: u8 = 0x07; - pub const KEY_E: u8 = 0x08; - pub const KEY_F: u8 = 0x09; - pub const KEY_G: u8 = 0x0A; - pub const KEY_H: u8 = 0x0B; - pub const KEY_I: u8 = 0x0C; - pub const KEY_J: u8 = 0x0D; - pub const KEY_K: u8 = 0x0E; - pub const KEY_L: u8 = 0x0F; - pub const KEY_M: u8 = 0x10; - pub const KEY_N: u8 = 0x11; - pub const KEY_O: u8 = 0x12; - pub const KEY_P: u8 = 0x13; - pub const KEY_Q: u8 = 0x14; - pub const KEY_R: u8 = 0x15; - pub const KEY_S: u8 = 0x16; - pub const KEY_T: u8 = 0x17; - pub const KEY_U: u8 = 0x18; - pub const KEY_V: u8 = 0x19; - pub const KEY_W: u8 = 0x1A; - pub const KEY_X: u8 = 0x1B; - pub const KEY_Y: u8 = 0x1C; - pub const KEY_Z: u8 = 0x1D; - - // Numbers 1-9, 0 (0x1E - 0x27) - pub const KEY_1: u8 = 0x1E; - pub const KEY_2: u8 = 0x1F; - pub const KEY_3: u8 = 0x20; - pub const KEY_4: u8 = 0x21; - pub const KEY_5: u8 = 0x22; - pub const KEY_6: u8 = 0x23; - pub const KEY_7: u8 = 0x24; - pub const KEY_8: u8 = 0x25; - pub const KEY_9: u8 = 0x26; - pub const KEY_0: u8 = 0x27; - - // Control keys - pub const KEY_ENTER: u8 = 0x28; - pub const KEY_ESCAPE: u8 = 0x29; - pub const KEY_BACKSPACE: u8 = 0x2A; - pub const KEY_TAB: u8 = 0x2B; - pub const KEY_SPACE: u8 = 0x2C; - pub const KEY_MINUS: u8 = 0x2D; - pub const KEY_EQUAL: u8 = 0x2E; - pub const KEY_LEFT_BRACKET: u8 = 0x2F; - pub const KEY_RIGHT_BRACKET: u8 = 0x30; - pub const KEY_BACKSLASH: u8 = 0x31; - pub const KEY_HASH: u8 = 0x32; // Non-US # and ~ - pub const KEY_SEMICOLON: u8 = 0x33; - pub const KEY_APOSTROPHE: u8 = 0x34; - pub const KEY_GRAVE: u8 = 0x35; - pub const KEY_COMMA: u8 = 0x36; - pub const KEY_PERIOD: u8 = 0x37; - pub const KEY_SLASH: u8 = 0x38; - pub const KEY_CAPS_LOCK: u8 = 0x39; - - // Function keys F1-F12 - pub const KEY_F1: u8 = 0x3A; - pub const KEY_F2: u8 = 0x3B; - pub const KEY_F3: u8 = 0x3C; - pub const KEY_F4: u8 = 0x3D; - pub const KEY_F5: u8 = 0x3E; - pub const KEY_F6: u8 = 0x3F; - pub const KEY_F7: u8 = 0x40; - pub const KEY_F8: u8 = 0x41; - pub const KEY_F9: u8 = 0x42; - pub const KEY_F10: u8 = 0x43; - pub const KEY_F11: u8 = 0x44; - pub const KEY_F12: u8 = 0x45; - - // Special keys - pub const KEY_PRINT_SCREEN: u8 = 0x46; - pub const KEY_SCROLL_LOCK: u8 = 0x47; - pub const KEY_PAUSE: u8 = 0x48; - pub const KEY_INSERT: u8 = 0x49; - pub const KEY_HOME: u8 = 0x4A; - pub const KEY_PAGE_UP: u8 = 0x4B; - pub const KEY_DELETE: u8 = 0x4C; - pub const KEY_END: u8 = 0x4D; - pub const KEY_PAGE_DOWN: u8 = 0x4E; - pub const KEY_RIGHT_ARROW: u8 = 0x4F; - pub const KEY_LEFT_ARROW: u8 = 0x50; - pub const KEY_DOWN_ARROW: u8 = 0x51; - pub const KEY_UP_ARROW: u8 = 0x52; - - // Numpad - pub const KEY_NUM_LOCK: u8 = 0x53; - pub const KEY_NUMPAD_DIVIDE: u8 = 0x54; - pub const KEY_NUMPAD_MULTIPLY: u8 = 0x55; - pub const KEY_NUMPAD_MINUS: u8 = 0x56; - pub const KEY_NUMPAD_PLUS: u8 = 0x57; - pub const KEY_NUMPAD_ENTER: u8 = 0x58; - pub const KEY_NUMPAD_1: u8 = 0x59; - pub const KEY_NUMPAD_2: u8 = 0x5A; - pub const KEY_NUMPAD_3: u8 = 0x5B; - pub const KEY_NUMPAD_4: u8 = 0x5C; - pub const KEY_NUMPAD_5: u8 = 0x5D; - pub const KEY_NUMPAD_6: u8 = 0x5E; - pub const KEY_NUMPAD_7: u8 = 0x5F; - pub const KEY_NUMPAD_8: u8 = 0x60; - pub const KEY_NUMPAD_9: u8 = 0x61; - pub const KEY_NUMPAD_0: u8 = 0x62; - pub const KEY_NUMPAD_DECIMAL: u8 = 0x63; - - // Additional keys - pub const KEY_NON_US_BACKSLASH: u8 = 0x64; - pub const KEY_APPLICATION: u8 = 0x65; // Context menu - pub const KEY_POWER: u8 = 0x66; - pub const KEY_NUMPAD_EQUAL: u8 = 0x67; - - // F13-F24 - pub const KEY_F13: u8 = 0x68; - pub const KEY_F14: u8 = 0x69; - pub const KEY_F15: u8 = 0x6A; - pub const KEY_F16: u8 = 0x6B; - pub const KEY_F17: u8 = 0x6C; - pub const KEY_F18: u8 = 0x6D; - pub const KEY_F19: u8 = 0x6E; - pub const KEY_F20: u8 = 0x6F; - pub const KEY_F21: u8 = 0x70; - pub const KEY_F22: u8 = 0x71; - pub const KEY_F23: u8 = 0x72; - pub const KEY_F24: u8 = 0x73; - - // Modifier keys (these are handled separately in the modifier byte) - pub const KEY_LEFT_CTRL: u8 = 0xE0; - pub const KEY_LEFT_SHIFT: u8 = 0xE1; - pub const KEY_LEFT_ALT: u8 = 0xE2; - pub const KEY_LEFT_META: u8 = 0xE3; - pub const KEY_RIGHT_CTRL: u8 = 0xE4; - pub const KEY_RIGHT_SHIFT: u8 = 0xE5; - pub const KEY_RIGHT_ALT: u8 = 0xE6; - pub const KEY_RIGHT_META: u8 = 0xE7; -} - -/// JavaScript key codes (event.keyCode / event.code) -#[allow(dead_code)] -pub mod js { - // Letters - pub const KEY_A: u8 = 65; - pub const KEY_B: u8 = 66; - pub const KEY_C: u8 = 67; - pub const KEY_D: u8 = 68; - pub const KEY_E: u8 = 69; - pub const KEY_F: u8 = 70; - pub const KEY_G: u8 = 71; - pub const KEY_H: u8 = 72; - pub const KEY_I: u8 = 73; - pub const KEY_J: u8 = 74; - pub const KEY_K: u8 = 75; - pub const KEY_L: u8 = 76; - pub const KEY_M: u8 = 77; - pub const KEY_N: u8 = 78; - pub const KEY_O: u8 = 79; - pub const KEY_P: u8 = 80; - pub const KEY_Q: u8 = 81; - pub const KEY_R: u8 = 82; - pub const KEY_S: u8 = 83; - pub const KEY_T: u8 = 84; - pub const KEY_U: u8 = 85; - pub const KEY_V: u8 = 86; - pub const KEY_W: u8 = 87; - pub const KEY_X: u8 = 88; - pub const KEY_Y: u8 = 89; - pub const KEY_Z: u8 = 90; - - // Numbers (top row) - pub const KEY_0: u8 = 48; - pub const KEY_1: u8 = 49; - pub const KEY_2: u8 = 50; - pub const KEY_3: u8 = 51; - pub const KEY_4: u8 = 52; - pub const KEY_5: u8 = 53; - pub const KEY_6: u8 = 54; - pub const KEY_7: u8 = 55; - pub const KEY_8: u8 = 56; - pub const KEY_9: u8 = 57; - - // Function keys - pub const KEY_F1: u8 = 112; - pub const KEY_F2: u8 = 113; - pub const KEY_F3: u8 = 114; - pub const KEY_F4: u8 = 115; - pub const KEY_F5: u8 = 116; - pub const KEY_F6: u8 = 117; - pub const KEY_F7: u8 = 118; - pub const KEY_F8: u8 = 119; - pub const KEY_F9: u8 = 120; - pub const KEY_F10: u8 = 121; - pub const KEY_F11: u8 = 122; - pub const KEY_F12: u8 = 123; - - // Control keys - pub const KEY_BACKSPACE: u8 = 8; - pub const KEY_TAB: u8 = 9; - pub const KEY_ENTER: u8 = 13; - pub const KEY_SHIFT: u8 = 16; - pub const KEY_CTRL: u8 = 17; - pub const KEY_ALT: u8 = 18; - pub const KEY_PAUSE: u8 = 19; - pub const KEY_CAPS_LOCK: u8 = 20; - pub const KEY_ESCAPE: u8 = 27; - pub const KEY_SPACE: u8 = 32; - pub const KEY_PAGE_UP: u8 = 33; - pub const KEY_PAGE_DOWN: u8 = 34; - pub const KEY_END: u8 = 35; - pub const KEY_HOME: u8 = 36; - pub const KEY_LEFT: u8 = 37; - pub const KEY_UP: u8 = 38; - pub const KEY_RIGHT: u8 = 39; - pub const KEY_DOWN: u8 = 40; - pub const KEY_INSERT: u8 = 45; - pub const KEY_DELETE: u8 = 46; - - // Punctuation - pub const KEY_SEMICOLON: u8 = 186; - pub const KEY_EQUAL: u8 = 187; - pub const KEY_COMMA: u8 = 188; - pub const KEY_MINUS: u8 = 189; - pub const KEY_PERIOD: u8 = 190; - pub const KEY_SLASH: u8 = 191; - pub const KEY_GRAVE: u8 = 192; - pub const KEY_LEFT_BRACKET: u8 = 219; - pub const KEY_BACKSLASH: u8 = 220; - pub const KEY_RIGHT_BRACKET: u8 = 221; - pub const KEY_APOSTROPHE: u8 = 222; - - // Numpad - pub const KEY_NUMPAD_0: u8 = 96; - pub const KEY_NUMPAD_1: u8 = 97; - pub const KEY_NUMPAD_2: u8 = 98; - pub const KEY_NUMPAD_3: u8 = 99; - pub const KEY_NUMPAD_4: u8 = 100; - pub const KEY_NUMPAD_5: u8 = 101; - pub const KEY_NUMPAD_6: u8 = 102; - pub const KEY_NUMPAD_7: u8 = 103; - pub const KEY_NUMPAD_8: u8 = 104; - pub const KEY_NUMPAD_9: u8 = 105; - pub const KEY_NUMPAD_MULTIPLY: u8 = 106; - pub const KEY_NUMPAD_ADD: u8 = 107; - pub const KEY_NUMPAD_SUBTRACT: u8 = 109; - pub const KEY_NUMPAD_DECIMAL: u8 = 110; - pub const KEY_NUMPAD_DIVIDE: u8 = 111; - - // Lock keys - pub const KEY_NUM_LOCK: u8 = 144; - pub const KEY_SCROLL_LOCK: u8 = 145; - - // Windows keys - pub const KEY_META_LEFT: u8 = 91; - pub const KEY_META_RIGHT: u8 = 92; - pub const KEY_CONTEXT_MENU: u8 = 93; -} - -/// JavaScript keyCode to USB HID keyCode mapping table -/// Using a fixed-size array for O(1) lookup instead of HashMap -/// Index = JavaScript keyCode, Value = USB HID keyCode (0 means unmapped) -static JS_TO_USB_TABLE: [u8; 256] = { - let mut table = [0u8; 256]; - - // Letters A-Z (JS 65-90 -> USB 0x04-0x1D) - let mut i = 0u8; - while i < 26 { - table[(65 + i) as usize] = usb::KEY_A + i; - i += 1; - } - - // Numbers 1-9, 0 (JS 49-57, 48 -> USB 0x1E-0x27) - table[49] = usb::KEY_1; // 1 - table[50] = usb::KEY_2; // 2 - table[51] = usb::KEY_3; // 3 - table[52] = usb::KEY_4; // 4 - table[53] = usb::KEY_5; // 5 - table[54] = usb::KEY_6; // 6 - table[55] = usb::KEY_7; // 7 - table[56] = usb::KEY_8; // 8 - table[57] = usb::KEY_9; // 9 - table[48] = usb::KEY_0; // 0 - - // Function keys F1-F12 (JS 112-123 -> USB 0x3A-0x45) - table[112] = usb::KEY_F1; - table[113] = usb::KEY_F2; - table[114] = usb::KEY_F3; - table[115] = usb::KEY_F4; - table[116] = usb::KEY_F5; - table[117] = usb::KEY_F6; - table[118] = usb::KEY_F7; - table[119] = usb::KEY_F8; - table[120] = usb::KEY_F9; - table[121] = usb::KEY_F10; - table[122] = usb::KEY_F11; - table[123] = usb::KEY_F12; - - // Control keys - table[13] = usb::KEY_ENTER; // Enter - table[27] = usb::KEY_ESCAPE; // Escape - table[8] = usb::KEY_BACKSPACE; // Backspace - table[9] = usb::KEY_TAB; // Tab - table[32] = usb::KEY_SPACE; // Space - table[20] = usb::KEY_CAPS_LOCK; // Caps Lock - - // Punctuation (JS codes vary by browser/layout) - table[189] = usb::KEY_MINUS; // - - table[187] = usb::KEY_EQUAL; // = - table[219] = usb::KEY_LEFT_BRACKET; // [ - table[221] = usb::KEY_RIGHT_BRACKET; // ] - table[220] = usb::KEY_BACKSLASH; // \ - table[186] = usb::KEY_SEMICOLON; // ; - table[222] = usb::KEY_APOSTROPHE; // ' - table[192] = usb::KEY_GRAVE; // ` - table[188] = usb::KEY_COMMA; // , - table[190] = usb::KEY_PERIOD; // . - table[191] = usb::KEY_SLASH; // / - - // Navigation keys - table[45] = usb::KEY_INSERT; - table[46] = usb::KEY_DELETE; - table[36] = usb::KEY_HOME; - table[35] = usb::KEY_END; - table[33] = usb::KEY_PAGE_UP; - table[34] = usb::KEY_PAGE_DOWN; - - // Arrow keys - table[39] = usb::KEY_RIGHT_ARROW; - table[37] = usb::KEY_LEFT_ARROW; - table[40] = usb::KEY_DOWN_ARROW; - table[38] = usb::KEY_UP_ARROW; - - // Numpad - table[144] = usb::KEY_NUM_LOCK; - table[111] = usb::KEY_NUMPAD_DIVIDE; - table[106] = usb::KEY_NUMPAD_MULTIPLY; - table[109] = usb::KEY_NUMPAD_MINUS; - table[107] = usb::KEY_NUMPAD_PLUS; - table[96] = usb::KEY_NUMPAD_0; - table[97] = usb::KEY_NUMPAD_1; - table[98] = usb::KEY_NUMPAD_2; - table[99] = usb::KEY_NUMPAD_3; - table[100] = usb::KEY_NUMPAD_4; - table[101] = usb::KEY_NUMPAD_5; - table[102] = usb::KEY_NUMPAD_6; - table[103] = usb::KEY_NUMPAD_7; - table[104] = usb::KEY_NUMPAD_8; - table[105] = usb::KEY_NUMPAD_9; - table[110] = usb::KEY_NUMPAD_DECIMAL; - - // Special keys - table[19] = usb::KEY_PAUSE; - table[145] = usb::KEY_SCROLL_LOCK; - table[93] = usb::KEY_APPLICATION; // Context menu - - // Modifier keys - table[17] = usb::KEY_LEFT_CTRL; - table[16] = usb::KEY_LEFT_SHIFT; - table[18] = usb::KEY_LEFT_ALT; - table[91] = usb::KEY_LEFT_META; // Left Windows/Command - table[92] = usb::KEY_RIGHT_META; // Right Windows/Command - - table -}; - -/// Convert JavaScript keyCode to USB HID keyCode -/// -/// Uses a fixed-size lookup table for O(1) performance. -/// Returns None if the key code is not mapped. -#[inline] -pub fn js_to_usb(js_code: u8) -> Option { - let usb_code = JS_TO_USB_TABLE[js_code as usize]; - if usb_code != 0 { - Some(usb_code) - } else { - None - } -} - -/// Check if a key code is a modifier key -pub fn is_modifier_key(usb_code: u8) -> bool { - (0xE0..=0xE7).contains(&usb_code) -} - -/// Get modifier bit for a modifier key -pub fn modifier_bit(usb_code: u8) -> Option { - match usb_code { - usb::KEY_LEFT_CTRL => Some(0x01), - usb::KEY_LEFT_SHIFT => Some(0x02), - usb::KEY_LEFT_ALT => Some(0x04), - usb::KEY_LEFT_META => Some(0x08), - usb::KEY_RIGHT_CTRL => Some(0x10), - usb::KEY_RIGHT_SHIFT => Some(0x20), - usb::KEY_RIGHT_ALT => Some(0x40), - usb::KEY_RIGHT_META => Some(0x80), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_letter_mapping() { - assert_eq!(js_to_usb(65), Some(usb::KEY_A)); // A - assert_eq!(js_to_usb(90), Some(usb::KEY_Z)); // Z - } - - #[test] - fn test_number_mapping() { - assert_eq!(js_to_usb(48), Some(usb::KEY_0)); - assert_eq!(js_to_usb(49), Some(usb::KEY_1)); - } - - #[test] - fn test_modifier_key() { - assert!(is_modifier_key(usb::KEY_LEFT_CTRL)); - assert!(is_modifier_key(usb::KEY_RIGHT_SHIFT)); - assert!(!is_modifier_key(usb::KEY_A)); - } -} diff --git a/src/hid/mod.rs b/src/hid/mod.rs index f6e93449..5ebdd0a1 100644 --- a/src/hid/mod.rs +++ b/src/hid/mod.rs @@ -15,12 +15,13 @@ pub mod backend; pub mod ch9329; pub mod consumer; pub mod datachannel; -pub mod keymap; +pub mod keyboard; pub mod otg; pub mod types; pub mod websocket; pub use backend::{HidBackend, HidBackendStatus, HidBackendType}; +pub use keyboard::CanonicalKey; pub use otg::LedState; pub use types::{ ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent, diff --git a/src/hid/otg.rs b/src/hid/otg.rs index c1c6f9bd..696e4dba 100644 --- a/src/hid/otg.rs +++ b/src/hid/otg.rs @@ -29,7 +29,6 @@ use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use tracing::{debug, info, trace, warn}; use super::backend::{HidBackend, HidBackendStatus}; -use super::keymap; use super::types::{ ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType, }; @@ -873,18 +872,13 @@ impl HidBackend for OtgBackend { } async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> { - // Convert JS keycode to USB HID if needed (skip if already USB HID) - let usb_key = if event.is_usb_hid { - event.key - } else { - keymap::js_to_usb(event.key).unwrap_or(event.key) - }; + let usb_key = event.key.to_hid_usage(); // Handle modifier keys separately - if keymap::is_modifier_key(usb_key) { + if event.key.is_modifier() { let mut state = self.keyboard_state.lock(); - if let Some(bit) = keymap::modifier_bit(usb_key) { + if let Some(bit) = event.key.modifier_bit() { match event.event_type { KeyEventType::Down => state.modifiers |= bit, KeyEventType::Up => state.modifiers &= !bit, diff --git a/src/hid/types.rs b/src/hid/types.rs index c6848d4a..5545ac62 100644 --- a/src/hid/types.rs +++ b/src/hid/types.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; +use super::keyboard::CanonicalKey; + /// Keyboard event type #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -105,34 +107,29 @@ pub struct KeyboardEvent { /// Event type (down/up) #[serde(rename = "type")] pub event_type: KeyEventType, - /// Key code (USB HID usage code or JavaScript key code) - pub key: u8, + /// Canonical keyboard key identifier shared across frontend and backend + pub key: CanonicalKey, /// Modifier keys state #[serde(default)] pub modifiers: KeyboardModifiers, - /// If true, key is already USB HID code (skip js_to_usb conversion) - #[serde(default)] - pub is_usb_hid: bool, } impl KeyboardEvent { - /// Create a key down event (JS keycode, needs conversion) - pub fn key_down(key: u8, modifiers: KeyboardModifiers) -> Self { + /// Create a key down event + pub fn key_down(key: CanonicalKey, modifiers: KeyboardModifiers) -> Self { Self { event_type: KeyEventType::Down, key, modifiers, - is_usb_hid: false, } } - /// Create a key up event (JS keycode, needs conversion) - pub fn key_up(key: u8, modifiers: KeyboardModifiers) -> Self { + /// Create a key up event + pub fn key_up(key: CanonicalKey, modifiers: KeyboardModifiers) -> Self { Self { event_type: KeyEventType::Up, key, modifiers, - is_usb_hid: false, } } } diff --git a/src/rustdesk/connection.rs b/src/rustdesk/connection.rs index c2437012..2c9c5e1b 100644 --- a/src/rustdesk/connection.rs +++ b/src/rustdesk/connection.rs @@ -22,7 +22,7 @@ use tokio::sync::{broadcast, mpsc, Mutex}; use tracing::{debug, error, info, warn}; use crate::audio::AudioController; -use crate::hid::{HidController, KeyEventType, KeyboardEvent, KeyboardModifiers}; +use crate::hid::{CanonicalKey, HidController, KeyEventType, KeyboardEvent, KeyboardModifiers}; use crate::video::codec_constraints::{ encoder_codec_to_id, encoder_codec_to_video_codec, video_codec_to_encoder_codec, }; @@ -1328,15 +1328,13 @@ impl Connection { ); let caps_down = KeyboardEvent { event_type: KeyEventType::Down, - key: 0x39, // USB HID CapsLock + key: CanonicalKey::CapsLock, modifiers: KeyboardModifiers::default(), - is_usb_hid: true, }; let caps_up = KeyboardEvent { event_type: KeyEventType::Up, - key: 0x39, + key: CanonicalKey::CapsLock, modifiers: KeyboardModifiers::default(), - is_usb_hid: true, }; if let Err(e) = hid.send_keyboard(caps_down).await { warn!("Failed to send CapsLock down: {}", e); @@ -1351,7 +1349,7 @@ impl Connection { if let Some(kb_event) = convert_key_event(ke) { debug!( "Converted to HID: key=0x{:02X}, event_type={:?}, modifiers={:02X}", - kb_event.key, + kb_event.key.to_hid_usage(), kb_event.event_type, kb_event.modifiers.to_hid_byte() ); diff --git a/src/rustdesk/hid_adapter.rs b/src/rustdesk/hid_adapter.rs index 7a89cdbb..dbb478d3 100644 --- a/src/rustdesk/hid_adapter.rs +++ b/src/rustdesk/hid_adapter.rs @@ -5,8 +5,8 @@ use super::protocol::hbb::message::key_event as ke_union; use super::protocol::{ControlKey, KeyEvent, MouseEvent}; use crate::hid::{ - KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent as OneKvmMouseEvent, - MouseEventType, + CanonicalKey, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, + MouseEvent as OneKvmMouseEvent, MouseEventType, }; use protobuf::Enum; @@ -217,11 +217,11 @@ pub fn convert_key_event(event: &KeyEvent) -> Option { // Handle control keys if let Some(ke_union::Union::ControlKey(ck)) = &event.union { if let Some(key) = control_key_to_hid(ck.value()) { + let key = CanonicalKey::from_hid_usage(key)?; return Some(KeyboardEvent { event_type, key, modifiers, - is_usb_hid: true, // Already converted to USB HID code }); } } @@ -230,11 +230,11 @@ pub fn convert_key_event(event: &KeyEvent) -> Option { if let Some(ke_union::Union::Chr(chr)) = &event.union { // chr contains USB HID scancode on Windows, X11 keycode on Linux if let Some(key) = keycode_to_hid(*chr) { + let key = CanonicalKey::from_hid_usage(key)?; return Some(KeyboardEvent { event_type, key, modifiers, - is_usb_hid: true, // Already converted to USB HID code }); } } @@ -608,6 +608,6 @@ mod tests { let kb_event = result.unwrap(); assert_eq!(kb_event.event_type, KeyEventType::Down); - assert_eq!(kb_event.key, 0x28); // Return key USB HID code + assert_eq!(kb_event.key, CanonicalKey::Enter); } } diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 4bb92cbe..9f2a7f7c 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,6 +1,7 @@ // API client for One-KVM backend import { request, ApiError } from './request' +import type { CanonicalKey } from '@/types/generated' const API_BASE = '/api' @@ -357,7 +358,7 @@ export const hidApi = { }> }>('/hid/otg/self-check'), - keyboard: async (type: 'down' | 'up', key: number, modifier?: number) => { + keyboard: async (type: 'down' | 'up', key: CanonicalKey, modifier?: number) => { await ensureHidConnection() const event: HidKeyboardEvent = { type: type === 'down' ? 'keydown' : 'keyup', diff --git a/web/src/components/InfoBar.vue b/web/src/components/InfoBar.vue index 35cd3db1..6a051a6a 100644 --- a/web/src/components/InfoBar.vue +++ b/web/src/components/InfoBar.vue @@ -1,10 +1,11 @@