mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
refactor(otg): 简化运行时与设置逻辑
This commit is contained in:
@@ -7,14 +7,15 @@ use super::configfs::{
|
||||
create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file,
|
||||
};
|
||||
use super::function::{FunctionMeta, GadgetFunction};
|
||||
use super::report_desc::{CONSUMER_CONTROL, KEYBOARD, MOUSE_ABSOLUTE, MOUSE_RELATIVE};
|
||||
use super::report_desc::{
|
||||
CONSUMER_CONTROL, KEYBOARD, KEYBOARD_WITH_LED, MOUSE_ABSOLUTE, MOUSE_RELATIVE,
|
||||
};
|
||||
use crate::error::Result;
|
||||
|
||||
/// HID function type
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HidFunctionType {
|
||||
/// Keyboard (no LED feedback)
|
||||
/// Uses 1 endpoint: IN
|
||||
/// Keyboard
|
||||
Keyboard,
|
||||
/// Relative mouse (traditional mouse movement)
|
||||
/// Uses 1 endpoint: IN
|
||||
@@ -28,7 +29,7 @@ pub enum HidFunctionType {
|
||||
}
|
||||
|
||||
impl HidFunctionType {
|
||||
/// Get endpoints required for this function type
|
||||
/// Get the base endpoint cost for this function type.
|
||||
pub fn endpoints(&self) -> u8 {
|
||||
match self {
|
||||
HidFunctionType::Keyboard => 1,
|
||||
@@ -59,7 +60,7 @@ impl HidFunctionType {
|
||||
}
|
||||
|
||||
/// Get report length in bytes
|
||||
pub fn report_length(&self) -> u8 {
|
||||
pub fn report_length(&self, _keyboard_leds: bool) -> u8 {
|
||||
match self {
|
||||
HidFunctionType::Keyboard => 8,
|
||||
HidFunctionType::MouseRelative => 4,
|
||||
@@ -69,9 +70,15 @@ impl HidFunctionType {
|
||||
}
|
||||
|
||||
/// Get report descriptor
|
||||
pub fn report_desc(&self) -> &'static [u8] {
|
||||
pub fn report_desc(&self, keyboard_leds: bool) -> &'static [u8] {
|
||||
match self {
|
||||
HidFunctionType::Keyboard => KEYBOARD,
|
||||
HidFunctionType::Keyboard => {
|
||||
if keyboard_leds {
|
||||
KEYBOARD_WITH_LED
|
||||
} else {
|
||||
KEYBOARD
|
||||
}
|
||||
}
|
||||
HidFunctionType::MouseRelative => MOUSE_RELATIVE,
|
||||
HidFunctionType::MouseAbsolute => MOUSE_ABSOLUTE,
|
||||
HidFunctionType::ConsumerControl => CONSUMER_CONTROL,
|
||||
@@ -98,15 +105,18 @@ pub struct HidFunction {
|
||||
func_type: HidFunctionType,
|
||||
/// Cached function name (avoids repeated allocation)
|
||||
name: String,
|
||||
/// Whether keyboard LED/status feedback is enabled.
|
||||
keyboard_leds: bool,
|
||||
}
|
||||
|
||||
impl HidFunction {
|
||||
/// Create a keyboard function
|
||||
pub fn keyboard(instance: u8) -> Self {
|
||||
pub fn keyboard(instance: u8, keyboard_leds: bool) -> Self {
|
||||
Self {
|
||||
instance,
|
||||
func_type: HidFunctionType::Keyboard,
|
||||
name: format!("hid.usb{}", instance),
|
||||
keyboard_leds,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +126,7 @@ impl HidFunction {
|
||||
instance,
|
||||
func_type: HidFunctionType::MouseRelative,
|
||||
name: format!("hid.usb{}", instance),
|
||||
keyboard_leds: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +136,7 @@ impl HidFunction {
|
||||
instance,
|
||||
func_type: HidFunctionType::MouseAbsolute,
|
||||
name: format!("hid.usb{}", instance),
|
||||
keyboard_leds: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +146,7 @@ impl HidFunction {
|
||||
instance,
|
||||
func_type: HidFunctionType::ConsumerControl,
|
||||
name: format!("hid.usb{}", instance),
|
||||
keyboard_leds: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,11 +194,14 @@ impl GadgetFunction for HidFunction {
|
||||
)?;
|
||||
write_file(
|
||||
&func_path.join("report_length"),
|
||||
&self.func_type.report_length().to_string(),
|
||||
&self.func_type.report_length(self.keyboard_leds).to_string(),
|
||||
)?;
|
||||
|
||||
// Write report descriptor
|
||||
write_bytes(&func_path.join("report_desc"), self.func_type.report_desc())?;
|
||||
write_bytes(
|
||||
&func_path.join("report_desc"),
|
||||
self.func_type.report_desc(self.keyboard_leds),
|
||||
)?;
|
||||
|
||||
debug!(
|
||||
"Created HID function: {} at {}",
|
||||
@@ -232,14 +248,15 @@ mod tests {
|
||||
assert_eq!(HidFunctionType::MouseRelative.endpoints(), 1);
|
||||
assert_eq!(HidFunctionType::MouseAbsolute.endpoints(), 1);
|
||||
|
||||
assert_eq!(HidFunctionType::Keyboard.report_length(), 8);
|
||||
assert_eq!(HidFunctionType::MouseRelative.report_length(), 4);
|
||||
assert_eq!(HidFunctionType::MouseAbsolute.report_length(), 6);
|
||||
assert_eq!(HidFunctionType::Keyboard.report_length(false), 8);
|
||||
assert_eq!(HidFunctionType::Keyboard.report_length(true), 8);
|
||||
assert_eq!(HidFunctionType::MouseRelative.report_length(false), 4);
|
||||
assert_eq!(HidFunctionType::MouseAbsolute.report_length(false), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_function_names() {
|
||||
let kb = HidFunction::keyboard(0);
|
||||
let kb = HidFunction::keyboard(0, false);
|
||||
assert_eq!(kb.name(), "hid.usb0");
|
||||
assert_eq!(kb.device_path(), PathBuf::from("/dev/hidg0"));
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::error::{AppError, Result};
|
||||
const REBIND_DELAY_MS: u64 = 300;
|
||||
|
||||
/// USB Gadget device descriptor configuration
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GadgetDescriptor {
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
@@ -131,8 +131,8 @@ impl OtgGadgetManager {
|
||||
|
||||
/// Add keyboard function
|
||||
/// Returns the expected device path (e.g., /dev/hidg0)
|
||||
pub fn add_keyboard(&mut self) -> Result<PathBuf> {
|
||||
let func = HidFunction::keyboard(self.hid_instance);
|
||||
pub fn add_keyboard(&mut self, keyboard_leds: bool) -> Result<PathBuf> {
|
||||
let func = HidFunction::keyboard(self.hid_instance, keyboard_leds);
|
||||
let device_path = func.device_path();
|
||||
self.add_function(Box::new(func))?;
|
||||
self.hid_instance += 1;
|
||||
@@ -245,12 +245,8 @@ impl OtgGadgetManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bind gadget to UDC
|
||||
pub fn bind(&mut self) -> Result<()> {
|
||||
let udc = Self::find_udc().ok_or_else(|| {
|
||||
AppError::Internal("No USB Device Controller (UDC) found".to_string())
|
||||
})?;
|
||||
|
||||
/// Bind gadget to a specific UDC
|
||||
pub fn bind(&mut self, udc: &str) -> Result<()> {
|
||||
// Recreate config symlinks before binding to avoid kernel gadget issues after rebind
|
||||
if let Err(e) = self.recreate_config_links() {
|
||||
warn!("Failed to recreate gadget config links before bind: {}", e);
|
||||
@@ -258,7 +254,7 @@ impl OtgGadgetManager {
|
||||
|
||||
info!("Binding gadget to UDC: {}", udc);
|
||||
write_file(&self.gadget_path.join("UDC"), &udc)?;
|
||||
self.bound_udc = Some(udc);
|
||||
self.bound_udc = Some(udc.to_string());
|
||||
std::thread::sleep(std::time::Duration::from_millis(REBIND_DELAY_MS));
|
||||
|
||||
Ok(())
|
||||
@@ -504,7 +500,7 @@ mod tests {
|
||||
let mut manager = OtgGadgetManager::with_config("test", 8);
|
||||
|
||||
// Keyboard uses 1 endpoint
|
||||
let _ = manager.add_keyboard();
|
||||
let _ = manager.add_keyboard(false);
|
||||
assert_eq!(manager.endpoint_allocator.used(), 1);
|
||||
|
||||
// Mouse uses 1 endpoint each
|
||||
|
||||
@@ -32,4 +32,4 @@ pub use hid::{HidFunction, HidFunctionType};
|
||||
pub use manager::{wait_for_hid_devices, OtgGadgetManager};
|
||||
pub use msd::{MsdFunction, MsdLunConfig};
|
||||
pub use report_desc::{KEYBOARD, MOUSE_ABSOLUTE, MOUSE_RELATIVE};
|
||||
pub use service::{HidDevicePaths, OtgService, OtgServiceState};
|
||||
pub use service::{HidDevicePaths, OtgDesiredState, OtgService, OtgServiceState};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! HID Report Descriptors
|
||||
|
||||
/// Keyboard HID Report Descriptor (no LED output - saves 1 endpoint)
|
||||
/// Keyboard HID Report Descriptor (no LED output)
|
||||
/// Report format (8 bytes input):
|
||||
/// [0] Modifier keys (8 bits)
|
||||
/// [1] Reserved
|
||||
@@ -34,6 +34,53 @@ pub const KEYBOARD: &[u8] = &[
|
||||
0xC0, // End Collection
|
||||
];
|
||||
|
||||
/// Keyboard HID Report Descriptor with LED output support.
|
||||
/// Input report format (8 bytes):
|
||||
/// [0] Modifier keys (8 bits)
|
||||
/// [1] Reserved
|
||||
/// [2-7] Key codes (6 keys)
|
||||
/// Output report format (1 byte):
|
||||
/// [0] Num Lock / Caps Lock / Scroll Lock / Compose / Kana
|
||||
pub const KEYBOARD_WITH_LED: &[u8] = &[
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
0xA1, 0x01, // Collection (Application)
|
||||
// Modifier keys input (8 bits)
|
||||
0x05, 0x07, // Usage Page (Key Codes)
|
||||
0x19, 0xE0, // Usage Minimum (224) - Left Control
|
||||
0x29, 0xE7, // Usage Maximum (231) - Right GUI
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x08, // Report Count (8)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute) - Modifier byte
|
||||
// Reserved byte
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x81, 0x01, // Input (Constant) - Reserved byte
|
||||
// LED output bits
|
||||
0x95, 0x05, // Report Count (5)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x05, 0x08, // Usage Page (LEDs)
|
||||
0x19, 0x01, // Usage Minimum (1)
|
||||
0x29, 0x05, // Usage Maximum (5)
|
||||
0x91, 0x02, // Output (Data, Variable, Absolute)
|
||||
// LED padding
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x03, // Report Size (3)
|
||||
0x91, 0x01, // Output (Constant)
|
||||
// Key array (6 bytes)
|
||||
0x95, 0x06, // Report Count (6)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xFF, 0x00, // Logical Maximum (255)
|
||||
0x05, 0x07, // Usage Page (Key Codes)
|
||||
0x19, 0x00, // Usage Minimum (0)
|
||||
0x2A, 0xFF, 0x00, // Usage Maximum (255)
|
||||
0x81, 0x00, // Input (Data, Array) - Key array (6 keys)
|
||||
0xC0, // End Collection
|
||||
];
|
||||
|
||||
/// Relative Mouse HID Report Descriptor (4 bytes report)
|
||||
/// Report format:
|
||||
/// [0] Buttons (5 bits) + padding (3 bits)
|
||||
@@ -155,6 +202,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_report_descriptor_sizes() {
|
||||
assert!(!KEYBOARD.is_empty());
|
||||
assert!(!KEYBOARD_WITH_LED.is_empty());
|
||||
assert!(!MOUSE_RELATIVE.is_empty());
|
||||
assert!(!MOUSE_ABSOLUTE.is_empty());
|
||||
assert!(!CONSUMER_CONTROL.is_empty());
|
||||
|
||||
@@ -1,39 +1,18 @@
|
||||
//! OTG Service - unified gadget lifecycle management
|
||||
//!
|
||||
//! This module provides centralized management for USB OTG gadget functions.
|
||||
//! It solves the ownership problem where both HID and MSD need access to the
|
||||
//! same USB gadget but should be independently configurable.
|
||||
//!
|
||||
//! Architecture:
|
||||
//! ```text
|
||||
//! ┌─────────────────────────┐
|
||||
//! │ OtgService │
|
||||
//! │ ┌───────────────────┐ │
|
||||
//! │ │ OtgGadgetManager │ │
|
||||
//! │ └───────────────────┘ │
|
||||
//! │ ↓ ↓ │
|
||||
//! │ ┌─────┐ ┌─────┐ │
|
||||
//! │ │ HID │ │ MSD │ │
|
||||
//! │ └─────┘ └─────┘ │
|
||||
//! └─────────────────────────┘
|
||||
//! ↑ ↑
|
||||
//! HidController MsdController
|
||||
//! ```
|
||||
//! It is the single owner of the USB gadget desired state and reconciles
|
||||
//! ConfigFS to match that state.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager};
|
||||
use super::msd::MsdFunction;
|
||||
use crate::config::{OtgDescriptorConfig, OtgHidFunctions};
|
||||
use crate::config::{HidBackend, HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// Bitflags for requested functions (lock-free)
|
||||
const FLAG_HID: u8 = 0b01;
|
||||
const FLAG_MSD: u8 = 0b10;
|
||||
|
||||
/// HID device paths
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HidDevicePaths {
|
||||
@@ -41,6 +20,8 @@ pub struct HidDevicePaths {
|
||||
pub mouse_relative: Option<PathBuf>,
|
||||
pub mouse_absolute: Option<PathBuf>,
|
||||
pub consumer: Option<PathBuf>,
|
||||
pub udc: Option<String>,
|
||||
pub keyboard_leds_enabled: bool,
|
||||
}
|
||||
|
||||
impl HidDevicePaths {
|
||||
@@ -62,6 +43,59 @@ impl HidDevicePaths {
|
||||
}
|
||||
}
|
||||
|
||||
/// Desired OTG gadget state derived from configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OtgDesiredState {
|
||||
pub udc: Option<String>,
|
||||
pub descriptor: GadgetDescriptor,
|
||||
pub hid_functions: Option<OtgHidFunctions>,
|
||||
pub keyboard_leds: bool,
|
||||
pub msd_enabled: bool,
|
||||
pub max_endpoints: u8,
|
||||
}
|
||||
|
||||
impl Default for OtgDesiredState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
udc: None,
|
||||
descriptor: GadgetDescriptor::default(),
|
||||
hid_functions: None,
|
||||
keyboard_leds: false,
|
||||
msd_enabled: false,
|
||||
max_endpoints: super::endpoint::DEFAULT_MAX_ENDPOINTS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OtgDesiredState {
|
||||
pub fn from_config(hid: &HidConfig, msd: &MsdConfig) -> Result<Self> {
|
||||
let hid_functions = if hid.backend == HidBackend::Otg {
|
||||
let functions = hid.constrained_otg_functions();
|
||||
Some(functions)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
hid.validate_otg_endpoint_budget(msd.enabled)?;
|
||||
|
||||
Ok(Self {
|
||||
udc: hid.resolved_otg_udc(),
|
||||
descriptor: GadgetDescriptor::from(&hid.otg_descriptor),
|
||||
hid_functions,
|
||||
keyboard_leds: hid.effective_otg_keyboard_leds(),
|
||||
msd_enabled: msd.enabled,
|
||||
max_endpoints: hid
|
||||
.resolved_otg_endpoint_limit()
|
||||
.unwrap_or(super::endpoint::DEFAULT_MAX_ENDPOINTS),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn hid_enabled(&self) -> bool {
|
||||
self.hid_functions.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// OTG Service state
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct OtgServiceState {
|
||||
@@ -71,19 +105,23 @@ pub struct OtgServiceState {
|
||||
pub hid_enabled: bool,
|
||||
/// Whether MSD function is enabled
|
||||
pub msd_enabled: bool,
|
||||
/// Bound UDC name
|
||||
pub configured_udc: Option<String>,
|
||||
/// HID device paths (set after gadget setup)
|
||||
pub hid_paths: Option<HidDevicePaths>,
|
||||
/// HID function selection (set after gadget setup)
|
||||
pub hid_functions: Option<OtgHidFunctions>,
|
||||
/// Whether keyboard LED/status feedback is enabled.
|
||||
pub keyboard_leds_enabled: bool,
|
||||
/// Applied endpoint budget.
|
||||
pub max_endpoints: u8,
|
||||
/// Applied descriptor configuration
|
||||
pub descriptor: Option<GadgetDescriptor>,
|
||||
/// Error message if setup failed
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// OTG Service - unified gadget lifecycle management
|
||||
///
|
||||
/// This service owns the OtgGadgetManager and provides a high-level interface
|
||||
/// for enabling/disabling HID and MSD functions. It ensures proper coordination
|
||||
/// between the two subsystems and handles gadget lifecycle management.
|
||||
pub struct OtgService {
|
||||
/// The underlying gadget manager
|
||||
manager: Mutex<Option<OtgGadgetManager>>,
|
||||
@@ -91,12 +129,8 @@ pub struct OtgService {
|
||||
state: RwLock<OtgServiceState>,
|
||||
/// MSD function handle (for runtime LUN configuration)
|
||||
msd_function: RwLock<Option<MsdFunction>>,
|
||||
/// Requested functions flags (atomic, lock-free read/write)
|
||||
requested_flags: AtomicU8,
|
||||
/// Requested HID function set
|
||||
hid_functions: RwLock<OtgHidFunctions>,
|
||||
/// Current descriptor configuration
|
||||
current_descriptor: RwLock<GadgetDescriptor>,
|
||||
/// Desired OTG state
|
||||
desired: RwLock<OtgDesiredState>,
|
||||
}
|
||||
|
||||
impl OtgService {
|
||||
@@ -106,41 +140,7 @@ impl OtgService {
|
||||
manager: Mutex::new(None),
|
||||
state: RwLock::new(OtgServiceState::default()),
|
||||
msd_function: RwLock::new(None),
|
||||
requested_flags: AtomicU8::new(0),
|
||||
hid_functions: RwLock::new(OtgHidFunctions::default()),
|
||||
current_descriptor: RwLock::new(GadgetDescriptor::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if HID is requested (lock-free)
|
||||
#[inline]
|
||||
fn is_hid_requested(&self) -> bool {
|
||||
self.requested_flags.load(Ordering::Acquire) & FLAG_HID != 0
|
||||
}
|
||||
|
||||
/// Check if MSD is requested (lock-free)
|
||||
#[inline]
|
||||
fn is_msd_requested(&self) -> bool {
|
||||
self.requested_flags.load(Ordering::Acquire) & FLAG_MSD != 0
|
||||
}
|
||||
|
||||
/// Set HID requested flag (lock-free)
|
||||
#[inline]
|
||||
fn set_hid_requested(&self, requested: bool) {
|
||||
if requested {
|
||||
self.requested_flags.fetch_or(FLAG_HID, Ordering::Release);
|
||||
} else {
|
||||
self.requested_flags.fetch_and(!FLAG_HID, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set MSD requested flag (lock-free)
|
||||
#[inline]
|
||||
fn set_msd_requested(&self, requested: bool) {
|
||||
if requested {
|
||||
self.requested_flags.fetch_or(FLAG_MSD, Ordering::Release);
|
||||
} else {
|
||||
self.requested_flags.fetch_and(!FLAG_MSD, Ordering::Release);
|
||||
desired: RwLock::new(OtgDesiredState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,220 +180,81 @@ impl OtgService {
|
||||
self.state.read().await.hid_paths.clone()
|
||||
}
|
||||
|
||||
/// Get current HID function selection
|
||||
pub async fn hid_functions(&self) -> OtgHidFunctions {
|
||||
self.hid_functions.read().await.clone()
|
||||
}
|
||||
|
||||
/// Update HID function selection
|
||||
pub async fn update_hid_functions(&self, functions: OtgHidFunctions) -> Result<()> {
|
||||
if functions.is_empty() {
|
||||
return Err(AppError::BadRequest(
|
||||
"OTG HID functions cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
{
|
||||
let mut current = self.hid_functions.write().await;
|
||||
if *current == functions {
|
||||
return Ok(());
|
||||
}
|
||||
*current = functions;
|
||||
}
|
||||
|
||||
// If HID is active, recreate gadget with new function set
|
||||
if self.is_hid_requested() {
|
||||
self.recreate_gadget().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get MSD function handle (for LUN configuration)
|
||||
pub async fn msd_function(&self) -> Option<MsdFunction> {
|
||||
self.msd_function.read().await.clone()
|
||||
}
|
||||
|
||||
/// Enable HID functions
|
||||
///
|
||||
/// This will create the gadget if not already created, add HID functions,
|
||||
/// and bind the gadget to UDC.
|
||||
pub async fn enable_hid(&self) -> Result<HidDevicePaths> {
|
||||
info!("Enabling HID functions via OtgService");
|
||||
|
||||
// Mark HID as requested (lock-free)
|
||||
self.set_hid_requested(true);
|
||||
|
||||
// Check if already enabled and function set unchanged
|
||||
let requested_functions = self.hid_functions.read().await.clone();
|
||||
{
|
||||
let state = self.state.read().await;
|
||||
if state.hid_enabled && state.hid_functions.as_ref() == Some(&requested_functions) {
|
||||
if let Some(ref paths) = state.hid_paths {
|
||||
info!("HID already enabled, returning existing paths");
|
||||
return Ok(paths.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate gadget with both HID and MSD if needed
|
||||
self.recreate_gadget().await?;
|
||||
|
||||
// Get HID paths from state
|
||||
let state = self.state.read().await;
|
||||
state
|
||||
.hid_paths
|
||||
.clone()
|
||||
.ok_or_else(|| AppError::Internal("HID paths not set after gadget setup".to_string()))
|
||||
/// Apply desired OTG state derived from the current application config.
|
||||
pub async fn apply_config(&self, hid: &HidConfig, msd: &MsdConfig) -> Result<()> {
|
||||
let desired = OtgDesiredState::from_config(hid, msd)?;
|
||||
self.apply_desired_state(desired).await
|
||||
}
|
||||
|
||||
/// Disable HID functions
|
||||
///
|
||||
/// This will unbind the gadget, remove HID functions, and optionally
|
||||
/// recreate the gadget with only MSD if MSD is still enabled.
|
||||
pub async fn disable_hid(&self) -> Result<()> {
|
||||
info!("Disabling HID functions via OtgService");
|
||||
|
||||
// Mark HID as not requested (lock-free)
|
||||
self.set_hid_requested(false);
|
||||
|
||||
// Check if HID is enabled
|
||||
/// Apply a fully materialized desired OTG state.
|
||||
pub async fn apply_desired_state(&self, desired: OtgDesiredState) -> Result<()> {
|
||||
{
|
||||
let state = self.state.read().await;
|
||||
if !state.hid_enabled {
|
||||
info!("HID already disabled");
|
||||
return Ok(());
|
||||
}
|
||||
let mut current = self.desired.write().await;
|
||||
*current = desired;
|
||||
}
|
||||
|
||||
// Recreate gadget without HID (or destroy if MSD also disabled)
|
||||
self.recreate_gadget().await
|
||||
self.reconcile_gadget().await
|
||||
}
|
||||
|
||||
/// Enable MSD function
|
||||
///
|
||||
/// This will create the gadget if not already created, add MSD function,
|
||||
/// and bind the gadget to UDC.
|
||||
pub async fn enable_msd(&self) -> Result<MsdFunction> {
|
||||
info!("Enabling MSD function via OtgService");
|
||||
|
||||
// Mark MSD as requested (lock-free)
|
||||
self.set_msd_requested(true);
|
||||
|
||||
// Check if already enabled
|
||||
{
|
||||
let state = self.state.read().await;
|
||||
if state.msd_enabled {
|
||||
let msd = self.msd_function.read().await;
|
||||
if let Some(ref func) = *msd {
|
||||
info!("MSD already enabled, returning existing function");
|
||||
return Ok(func.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate gadget with both HID and MSD if needed
|
||||
self.recreate_gadget().await?;
|
||||
|
||||
// Get MSD function
|
||||
let msd = self.msd_function.read().await;
|
||||
msd.clone().ok_or_else(|| {
|
||||
AppError::Internal("MSD function not set after gadget setup".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Disable MSD function
|
||||
///
|
||||
/// This will unbind the gadget, remove MSD function, and optionally
|
||||
/// recreate the gadget with only HID if HID is still enabled.
|
||||
pub async fn disable_msd(&self) -> Result<()> {
|
||||
info!("Disabling MSD function via OtgService");
|
||||
|
||||
// Mark MSD as not requested (lock-free)
|
||||
self.set_msd_requested(false);
|
||||
|
||||
// Check if MSD is enabled
|
||||
{
|
||||
let state = self.state.read().await;
|
||||
if !state.msd_enabled {
|
||||
info!("MSD already disabled");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate gadget without MSD (or destroy if HID also disabled)
|
||||
self.recreate_gadget().await
|
||||
}
|
||||
|
||||
/// Recreate the gadget with currently requested functions
|
||||
///
|
||||
/// This is called whenever the set of enabled functions changes.
|
||||
/// It will:
|
||||
/// 1. Check if recreation is needed (function set changed)
|
||||
/// 2. If needed: cleanup existing gadget
|
||||
/// 3. Create new gadget with requested functions
|
||||
/// 4. Setup and bind
|
||||
async fn recreate_gadget(&self) -> Result<()> {
|
||||
// Read requested flags atomically (lock-free)
|
||||
let hid_requested = self.is_hid_requested();
|
||||
let msd_requested = self.is_msd_requested();
|
||||
let hid_functions = if hid_requested {
|
||||
self.hid_functions.read().await.clone()
|
||||
} else {
|
||||
OtgHidFunctions::default()
|
||||
};
|
||||
async fn reconcile_gadget(&self) -> Result<()> {
|
||||
let desired = self.desired.read().await.clone();
|
||||
|
||||
info!(
|
||||
"Recreating gadget with: HID={}, MSD={}",
|
||||
hid_requested, msd_requested
|
||||
"Reconciling OTG gadget: HID={}, MSD={}, UDC={:?}",
|
||||
desired.hid_enabled(),
|
||||
desired.msd_enabled,
|
||||
desired.udc
|
||||
);
|
||||
|
||||
// Check if gadget already matches requested state
|
||||
{
|
||||
let state = self.state.read().await;
|
||||
let functions_match = if hid_requested {
|
||||
state.hid_functions.as_ref() == Some(&hid_functions)
|
||||
} else {
|
||||
state.hid_functions.is_none()
|
||||
};
|
||||
if state.gadget_active
|
||||
&& state.hid_enabled == hid_requested
|
||||
&& state.msd_enabled == msd_requested
|
||||
&& functions_match
|
||||
&& state.hid_enabled == desired.hid_enabled()
|
||||
&& state.msd_enabled == desired.msd_enabled
|
||||
&& state.configured_udc == desired.udc
|
||||
&& state.hid_functions == desired.hid_functions
|
||||
&& state.keyboard_leds_enabled == desired.keyboard_leds
|
||||
&& state.max_endpoints == desired.max_endpoints
|
||||
&& state.descriptor.as_ref() == Some(&desired.descriptor)
|
||||
{
|
||||
info!("Gadget already has requested functions, skipping recreate");
|
||||
info!("OTG gadget already matches desired state");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup existing gadget
|
||||
{
|
||||
let mut manager = self.manager.lock().await;
|
||||
if let Some(mut m) = manager.take() {
|
||||
info!("Cleaning up existing gadget before recreate");
|
||||
info!("Cleaning up existing gadget before OTG reconcile");
|
||||
if let Err(e) = m.cleanup() {
|
||||
warn!("Error cleaning up existing gadget: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear MSD function
|
||||
*self.msd_function.write().await = None;
|
||||
|
||||
// Update state to inactive
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.gadget_active = false;
|
||||
state.hid_enabled = false;
|
||||
state.msd_enabled = false;
|
||||
state.configured_udc = None;
|
||||
state.hid_paths = None;
|
||||
state.hid_functions = None;
|
||||
state.keyboard_leds_enabled = false;
|
||||
state.max_endpoints = super::endpoint::DEFAULT_MAX_ENDPOINTS;
|
||||
state.descriptor = None;
|
||||
state.error = None;
|
||||
}
|
||||
|
||||
// If nothing requested, we're done
|
||||
if !hid_requested && !msd_requested {
|
||||
info!("No functions requested, gadget destroyed");
|
||||
if !desired.hid_enabled() && !desired.msd_enabled {
|
||||
info!("OTG desired state is empty, gadget removed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -401,41 +262,37 @@ impl OtgService {
|
||||
warn!("Failed to ensure libcomposite is available: {}", e);
|
||||
}
|
||||
|
||||
// Check if OTG is available
|
||||
if !Self::is_available() {
|
||||
let error = "OTG not available: ConfigFS not mounted or no UDC found".to_string();
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
if !OtgGadgetManager::is_available() {
|
||||
let error = "OTG not available: ConfigFS not mounted".to_string();
|
||||
self.state.write().await.error = Some(error.clone());
|
||||
return Err(AppError::Internal(error));
|
||||
}
|
||||
|
||||
// Create new gadget manager with current descriptor
|
||||
let descriptor = self.current_descriptor.read().await.clone();
|
||||
let udc = desired.udc.clone().ok_or_else(|| {
|
||||
let error = "OTG not available: no UDC found".to_string();
|
||||
AppError::Internal(error)
|
||||
})?;
|
||||
|
||||
let mut manager = OtgGadgetManager::with_descriptor(
|
||||
super::configfs::DEFAULT_GADGET_NAME,
|
||||
super::endpoint::DEFAULT_MAX_ENDPOINTS,
|
||||
descriptor,
|
||||
desired.max_endpoints,
|
||||
desired.descriptor.clone(),
|
||||
);
|
||||
|
||||
let mut hid_paths = None;
|
||||
|
||||
// Add HID functions if requested
|
||||
if hid_requested {
|
||||
if hid_functions.is_empty() {
|
||||
let error = "HID functions set is empty".to_string();
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
return Err(AppError::BadRequest(error));
|
||||
}
|
||||
|
||||
let mut paths = HidDevicePaths::default();
|
||||
if let Some(hid_functions) = desired.hid_functions.clone() {
|
||||
let mut paths = HidDevicePaths {
|
||||
udc: Some(udc.clone()),
|
||||
keyboard_leds_enabled: desired.keyboard_leds,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if hid_functions.keyboard {
|
||||
match manager.add_keyboard() {
|
||||
match manager.add_keyboard(desired.keyboard_leds) {
|
||||
Ok(kb) => paths.keyboard = Some(kb),
|
||||
Err(e) => {
|
||||
let error = format!("Failed to add keyboard HID function: {}", e);
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
self.state.write().await.error = Some(error.clone());
|
||||
return Err(AppError::Internal(error));
|
||||
}
|
||||
}
|
||||
@@ -446,8 +303,7 @@ impl OtgService {
|
||||
Ok(rel) => paths.mouse_relative = Some(rel),
|
||||
Err(e) => {
|
||||
let error = format!("Failed to add relative mouse HID function: {}", e);
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
self.state.write().await.error = Some(error.clone());
|
||||
return Err(AppError::Internal(error));
|
||||
}
|
||||
}
|
||||
@@ -458,8 +314,7 @@ impl OtgService {
|
||||
Ok(abs) => paths.mouse_absolute = Some(abs),
|
||||
Err(e) => {
|
||||
let error = format!("Failed to add absolute mouse HID function: {}", e);
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
self.state.write().await.error = Some(error.clone());
|
||||
return Err(AppError::Internal(error));
|
||||
}
|
||||
}
|
||||
@@ -470,8 +325,7 @@ impl OtgService {
|
||||
Ok(consumer) => paths.consumer = Some(consumer),
|
||||
Err(e) => {
|
||||
let error = format!("Failed to add consumer HID function: {}", e);
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
self.state.write().await.error = Some(error.clone());
|
||||
return Err(AppError::Internal(error));
|
||||
}
|
||||
}
|
||||
@@ -481,8 +335,7 @@ impl OtgService {
|
||||
debug!("HID functions added to gadget");
|
||||
}
|
||||
|
||||
// Add MSD function if requested
|
||||
let msd_func = if msd_requested {
|
||||
let msd_func = if desired.msd_enabled {
|
||||
match manager.add_msd() {
|
||||
Ok(func) => {
|
||||
debug!("MSD function added to gadget");
|
||||
@@ -490,8 +343,7 @@ impl OtgService {
|
||||
}
|
||||
Err(e) => {
|
||||
let error = format!("Failed to add MSD function: {}", e);
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
self.state.write().await.error = Some(error.clone());
|
||||
return Err(AppError::Internal(error));
|
||||
}
|
||||
}
|
||||
@@ -499,25 +351,19 @@ impl OtgService {
|
||||
None
|
||||
};
|
||||
|
||||
// Setup gadget
|
||||
if let Err(e) = manager.setup() {
|
||||
let error = format!("Failed to setup gadget: {}", e);
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
self.state.write().await.error = Some(error.clone());
|
||||
return Err(AppError::Internal(error));
|
||||
}
|
||||
|
||||
// Bind to UDC
|
||||
if let Err(e) = manager.bind() {
|
||||
let error = format!("Failed to bind gadget to UDC: {}", e);
|
||||
let mut state = self.state.write().await;
|
||||
state.error = Some(error.clone());
|
||||
// Cleanup on failure
|
||||
if let Err(e) = manager.bind(&udc) {
|
||||
let error = format!("Failed to bind gadget to UDC {}: {}", udc, e);
|
||||
self.state.write().await.error = Some(error.clone());
|
||||
let _ = manager.cleanup();
|
||||
return Err(AppError::Internal(error));
|
||||
}
|
||||
|
||||
// Wait for HID devices to appear
|
||||
if let Some(ref paths) = hid_paths {
|
||||
let device_paths = paths.existing_paths();
|
||||
if !device_paths.is_empty() && !wait_for_hid_devices(&device_paths, 2000).await {
|
||||
@@ -525,103 +371,36 @@ impl OtgService {
|
||||
}
|
||||
}
|
||||
|
||||
// Store manager and update state
|
||||
{
|
||||
*self.manager.lock().await = Some(manager);
|
||||
}
|
||||
|
||||
{
|
||||
*self.msd_function.write().await = msd_func;
|
||||
}
|
||||
*self.manager.lock().await = Some(manager);
|
||||
*self.msd_function.write().await = msd_func;
|
||||
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.gadget_active = true;
|
||||
state.hid_enabled = hid_requested;
|
||||
state.msd_enabled = msd_requested;
|
||||
state.hid_enabled = desired.hid_enabled();
|
||||
state.msd_enabled = desired.msd_enabled;
|
||||
state.configured_udc = Some(udc);
|
||||
state.hid_paths = hid_paths;
|
||||
state.hid_functions = if hid_requested {
|
||||
Some(hid_functions)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
state.hid_functions = desired.hid_functions;
|
||||
state.keyboard_leds_enabled = desired.keyboard_leds;
|
||||
state.max_endpoints = desired.max_endpoints;
|
||||
state.descriptor = Some(desired.descriptor);
|
||||
state.error = None;
|
||||
}
|
||||
|
||||
info!("Gadget created successfully");
|
||||
info!("OTG gadget reconciled successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the descriptor configuration
|
||||
///
|
||||
/// This updates the stored descriptor and triggers a gadget recreation
|
||||
/// if the gadget is currently active.
|
||||
pub async fn update_descriptor(&self, config: &OtgDescriptorConfig) -> Result<()> {
|
||||
let new_descriptor = GadgetDescriptor {
|
||||
vendor_id: config.vendor_id,
|
||||
product_id: config.product_id,
|
||||
device_version: super::configfs::DEFAULT_USB_BCD_DEVICE,
|
||||
manufacturer: config.manufacturer.clone(),
|
||||
product: config.product.clone(),
|
||||
serial_number: config
|
||||
.serial_number
|
||||
.clone()
|
||||
.unwrap_or_else(|| "0123456789".to_string()),
|
||||
};
|
||||
|
||||
// Update stored descriptor
|
||||
*self.current_descriptor.write().await = new_descriptor;
|
||||
|
||||
// If gadget is active, recreate it with new descriptor
|
||||
let state = self.state.read().await;
|
||||
if state.gadget_active {
|
||||
drop(state); // Release read lock before calling recreate
|
||||
info!("Descriptor changed, recreating gadget");
|
||||
self.force_recreate_gadget().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Force recreate the gadget (used when descriptor changes)
|
||||
async fn force_recreate_gadget(&self) -> Result<()> {
|
||||
// Cleanup existing gadget
|
||||
{
|
||||
let mut manager = self.manager.lock().await;
|
||||
if let Some(mut m) = manager.take() {
|
||||
info!("Cleaning up existing gadget for descriptor change");
|
||||
if let Err(e) = m.cleanup() {
|
||||
warn!("Error cleaning up existing gadget: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear MSD function
|
||||
*self.msd_function.write().await = None;
|
||||
|
||||
// Update state to inactive
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.gadget_active = false;
|
||||
state.hid_enabled = false;
|
||||
state.msd_enabled = false;
|
||||
state.hid_paths = None;
|
||||
state.hid_functions = None;
|
||||
state.error = None;
|
||||
}
|
||||
|
||||
// Recreate with current requested functions
|
||||
self.recreate_gadget().await
|
||||
}
|
||||
|
||||
/// Shutdown the OTG service and cleanup all resources
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
info!("Shutting down OTG service");
|
||||
|
||||
// Mark nothing as requested (lock-free)
|
||||
self.requested_flags.store(0, Ordering::Release);
|
||||
{
|
||||
let mut desired = self.desired.write().await;
|
||||
*desired = OtgDesiredState::default();
|
||||
}
|
||||
|
||||
// Cleanup gadget
|
||||
let mut manager = self.manager.lock().await;
|
||||
if let Some(mut m) = manager.take() {
|
||||
if let Err(e) = m.cleanup() {
|
||||
@@ -629,7 +408,6 @@ impl OtgService {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear state
|
||||
*self.msd_function.write().await = None;
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
@@ -649,11 +427,26 @@ impl Default for OtgService {
|
||||
|
||||
impl Drop for OtgService {
|
||||
fn drop(&mut self) {
|
||||
// Gadget cleanup is handled by OtgGadgetManager's Drop
|
||||
debug!("OtgService dropping");
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OtgDescriptorConfig> for GadgetDescriptor {
|
||||
fn from(config: &OtgDescriptorConfig) -> Self {
|
||||
Self {
|
||||
vendor_id: config.vendor_id,
|
||||
product_id: config.product_id,
|
||||
device_version: super::configfs::DEFAULT_USB_BCD_DEVICE,
|
||||
manufacturer: config.manufacturer.clone(),
|
||||
product: config.product.clone(),
|
||||
serial_number: config
|
||||
.serial_number
|
||||
.clone()
|
||||
.unwrap_or_else(|| "0123456789".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -661,8 +454,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_service_creation() {
|
||||
let _service = OtgService::new();
|
||||
// Just test that creation doesn't panic
|
||||
let _ = OtgService::is_available(); // Depends on environment
|
||||
let _ = OtgService::is_available();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user