feat(hid): 添加 Consumer Control 多媒体按键和多平台键盘布局

- 新增 Consumer Control HID 支持(播放/暂停、音量控制等)
- 虚拟键盘支持 Windows/Mac/Android 三种布局切换
- 移除键盘 LED 反馈以节省 USB 端点(从 2 减至 1)
- InfoBar 优化:按键名称友好显示,移除未实现的 Num/Scroll 指示器
- 更新 HID 模块文档
This commit is contained in:
mofeng-git
2026-01-02 23:52:12 +08:00
parent ad401cdf1c
commit cb7d9882a2
27 changed files with 888 additions and 262 deletions

View File

@@ -5,14 +5,14 @@ use tracing::debug;
use super::configfs::{create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file};
use super::function::{FunctionMeta, GadgetFunction};
use super::report_desc::{KEYBOARD_WITH_LED, MOUSE_ABSOLUTE, MOUSE_RELATIVE};
use super::report_desc::{CONSUMER_CONTROL, KEYBOARD, MOUSE_ABSOLUTE, MOUSE_RELATIVE};
use crate::error::Result;
/// HID function type
#[derive(Debug, Clone)]
pub enum HidFunctionType {
/// Keyboard with LED feedback support
/// Uses 2 endpoints: IN (reports) + OUT (LED status)
/// Keyboard (no LED feedback)
/// Uses 1 endpoint: IN
Keyboard,
/// Relative mouse (traditional mouse movement)
/// Uses 1 endpoint: IN
@@ -20,33 +20,39 @@ pub enum HidFunctionType {
/// Absolute mouse (touchscreen-like positioning)
/// Uses 1 endpoint: IN
MouseAbsolute,
/// Consumer control (multimedia keys)
/// Uses 1 endpoint: IN
ConsumerControl,
}
impl HidFunctionType {
/// Get endpoints required for this function type
pub fn endpoints(&self) -> u8 {
match self {
HidFunctionType::Keyboard => 2, // IN + OUT for LED
HidFunctionType::Keyboard => 1,
HidFunctionType::MouseRelative => 1,
HidFunctionType::MouseAbsolute => 1,
HidFunctionType::ConsumerControl => 1,
}
}
/// Get HID protocol
pub fn protocol(&self) -> u8 {
match self {
HidFunctionType::Keyboard => 1, // Keyboard
HidFunctionType::MouseRelative => 2, // Mouse
HidFunctionType::MouseAbsolute => 2, // Mouse
HidFunctionType::Keyboard => 1, // Keyboard
HidFunctionType::MouseRelative => 2, // Mouse
HidFunctionType::MouseAbsolute => 2, // Mouse
HidFunctionType::ConsumerControl => 0, // None
}
}
/// Get HID subclass
pub fn subclass(&self) -> u8 {
match self {
HidFunctionType::Keyboard => 1, // Boot interface
HidFunctionType::MouseRelative => 1, // Boot interface
HidFunctionType::MouseAbsolute => 0, // No boot interface (absolute not in boot protocol)
HidFunctionType::Keyboard => 1, // Boot interface
HidFunctionType::MouseRelative => 1, // Boot interface
HidFunctionType::MouseAbsolute => 0, // No boot interface
HidFunctionType::ConsumerControl => 0, // No boot interface
}
}
@@ -56,15 +62,17 @@ impl HidFunctionType {
HidFunctionType::Keyboard => 8,
HidFunctionType::MouseRelative => 4,
HidFunctionType::MouseAbsolute => 6,
HidFunctionType::ConsumerControl => 2,
}
}
/// Get report descriptor
pub fn report_desc(&self) -> &'static [u8] {
match self {
HidFunctionType::Keyboard => KEYBOARD_WITH_LED,
HidFunctionType::Keyboard => KEYBOARD,
HidFunctionType::MouseRelative => MOUSE_RELATIVE,
HidFunctionType::MouseAbsolute => MOUSE_ABSOLUTE,
HidFunctionType::ConsumerControl => CONSUMER_CONTROL,
}
}
@@ -74,6 +82,7 @@ impl HidFunctionType {
HidFunctionType::Keyboard => "Keyboard",
HidFunctionType::MouseRelative => "Relative Mouse",
HidFunctionType::MouseAbsolute => "Absolute Mouse",
HidFunctionType::ConsumerControl => "Consumer Control",
}
}
}
@@ -117,6 +126,15 @@ impl HidFunction {
}
}
/// Create a consumer control function
pub fn consumer_control(instance: u8) -> Self {
Self {
instance,
func_type: HidFunctionType::ConsumerControl,
name: format!("hid.usb{}", instance),
}
}
/// Get function path in gadget
fn function_path(&self, gadget_path: &Path) -> PathBuf {
gadget_path.join("functions").join(self.name())
@@ -155,16 +173,6 @@ impl GadgetFunction for HidFunction {
write_file(&func_path.join("subclass"), &self.func_type.subclass().to_string())?;
write_file(&func_path.join("report_length"), &self.func_type.report_length().to_string())?;
// For keyboard, enable OUT endpoint for LED feedback
// no_out_endpoint: 0 = enable OUT endpoint, 1 = disable
if matches!(self.func_type, HidFunctionType::Keyboard) {
let no_out_path = func_path.join("no_out_endpoint");
if no_out_path.exists() || func_path.exists() {
// Try to write, ignore error if file doesn't exist yet
let _ = write_file(&no_out_path, "0");
}
}
// Write report descriptor
write_bytes(&func_path.join("report_desc"), self.func_type.report_desc())?;
@@ -205,7 +213,7 @@ mod tests {
#[test]
fn test_hid_function_types() {
assert_eq!(HidFunctionType::Keyboard.endpoints(), 2);
assert_eq!(HidFunctionType::Keyboard.endpoints(), 1);
assert_eq!(HidFunctionType::MouseRelative.endpoints(), 1);
assert_eq!(HidFunctionType::MouseAbsolute.endpoints(), 1);

View File

@@ -118,6 +118,15 @@ impl OtgGadgetManager {
Ok(device_path)
}
/// Add consumer control function (multimedia keys)
pub fn add_consumer_control(&mut self) -> Result<PathBuf> {
let func = HidFunction::consumer_control(self.hid_instance);
let device_path = func.device_path();
self.add_function(Box::new(func))?;
self.hid_instance += 1;
Ok(device_path)
}
/// Add MSD function (returns MsdFunction handle for LUN configuration)
pub fn add_msd(&mut self) -> Result<MsdFunction> {
let func = MsdFunction::new(self.msd_instance);

View File

@@ -31,5 +31,5 @@ pub use function::{FunctionMeta, GadgetFunction};
pub use hid::{HidFunction, HidFunctionType};
pub use manager::{wait_for_hid_devices, OtgGadgetManager};
pub use msd::{MsdFunction, MsdLunConfig};
pub use report_desc::{KEYBOARD_WITH_LED, MOUSE_ABSOLUTE, MOUSE_RELATIVE};
pub use report_desc::{KEYBOARD, MOUSE_ABSOLUTE, MOUSE_RELATIVE};
pub use service::{HidDevicePaths, OtgService, OtgServiceState};

View File

@@ -1,17 +1,11 @@
//! HID Report Descriptors
/// Keyboard HID Report Descriptor with LED output support
/// Keyboard HID Report Descriptor (no LED output - saves 1 endpoint)
/// Report format (8 bytes input):
/// [0] Modifier keys (8 bits)
/// [1] Reserved
/// [2-7] Key codes (6 keys)
/// LED output (1 byte):
/// Bit 0: Num Lock
/// Bit 1: Caps Lock
/// Bit 2: Scroll Lock
/// Bit 3: Compose
/// Bit 4: Kana
pub const KEYBOARD_WITH_LED: &[u8] = &[
pub const KEYBOARD: &[u8] = &[
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
@@ -28,17 +22,6 @@ pub const KEYBOARD_WITH_LED: &[u8] = &[
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Constant) - Reserved byte
// LED output (5 bits)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x05, 0x08, // Usage Page (LEDs)
0x19, 0x01, // Usage Minimum (1) - Num Lock
0x29, 0x05, // Usage Maximum (5) - Kana
0x91, 0x02, // Output (Data, Variable, Absolute) - LED bits
// LED padding (3 bits)
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x91, 0x01, // Output (Constant) - Padding
// Key array (6 bytes)
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
@@ -147,14 +130,33 @@ pub const MOUSE_ABSOLUTE: &[u8] = &[
0xC0, // End Collection
];
/// Consumer Control HID Report Descriptor (2 bytes report)
/// Report format:
/// [0-1] Consumer Control Usage (16-bit little-endian)
/// Supports: Play/Pause, Stop, Next/Prev Track, Mute, Volume Up/Down, etc.
pub const CONSUMER_CONTROL: &[u8] = &[
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x03, // Logical Maximum (1023)
0x19, 0x00, // Usage Minimum (0)
0x2A, 0xFF, 0x03, // Usage Maximum (1023)
0x75, 0x10, // Report Size (16)
0x95, 0x01, // Report Count (1)
0x81, 0x00, // Input (Data, Array)
0xC0, // End Collection
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_report_descriptor_sizes() {
assert!(!KEYBOARD_WITH_LED.is_empty());
assert!(!KEYBOARD.is_empty());
assert!(!MOUSE_RELATIVE.is_empty());
assert!(!MOUSE_ABSOLUTE.is_empty());
assert!(!CONSUMER_CONTROL.is_empty());
}
}

View File

@@ -39,6 +39,7 @@ pub struct HidDevicePaths {
pub keyboard: PathBuf,
pub mouse_relative: PathBuf,
pub mouse_absolute: PathBuf,
pub consumer: Option<PathBuf>,
}
impl Default for HidDevicePaths {
@@ -47,6 +48,7 @@ impl Default for HidDevicePaths {
keyboard: PathBuf::from("/dev/hidg0"),
mouse_relative: PathBuf::from("/dev/hidg1"),
mouse_absolute: PathBuf::from("/dev/hidg2"),
consumer: Some(PathBuf::from("/dev/hidg3")),
}
}
}
@@ -353,16 +355,18 @@ impl OtgService {
manager.add_keyboard(),
manager.add_mouse_relative(),
manager.add_mouse_absolute(),
manager.add_consumer_control(),
) {
(Ok(kb), Ok(rel), Ok(abs)) => {
(Ok(kb), Ok(rel), Ok(abs), Ok(consumer)) => {
hid_paths = Some(HidDevicePaths {
keyboard: kb,
mouse_relative: rel,
mouse_absolute: abs,
consumer: Some(consumer),
});
debug!("HID functions added to gadget");
}
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => {
(Err(e), _, _, _) | (_, Err(e), _, _) | (_, _, Err(e), _) | (_, _, _, Err(e)) => {
let error = format!("Failed to add HID functions: {}", e);
let mut state = self.state.write().await;
state.error = Some(error.clone());