This commit is contained in:
mofeng-git
2025-12-28 18:19:16 +08:00
commit d143d158e4
771 changed files with 220548 additions and 0 deletions

138
src/otg/configfs.rs Normal file
View File

@@ -0,0 +1,138 @@
//! ConfigFS file operations for USB Gadget
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::Path;
use crate::error::{AppError, Result};
/// ConfigFS base path for USB gadgets
pub const CONFIGFS_PATH: &str = "/sys/kernel/config/usb_gadget";
/// Default gadget name
pub const DEFAULT_GADGET_NAME: &str = "one-kvm";
/// USB Vendor ID (Linux Foundation)
pub const USB_VENDOR_ID: u16 = 0x1d6b;
/// USB Product ID (Multifunction Composite Gadget)
pub const USB_PRODUCT_ID: u16 = 0x0104;
/// USB device version
pub const USB_BCD_DEVICE: u16 = 0x0100;
/// USB spec version (USB 2.0)
pub const USB_BCD_USB: u16 = 0x0200;
/// Check if ConfigFS is available
pub fn is_configfs_available() -> bool {
Path::new(CONFIGFS_PATH).exists()
}
/// Find available UDC (USB Device Controller)
pub fn find_udc() -> Option<String> {
let udc_path = Path::new("/sys/class/udc");
if !udc_path.exists() {
return None;
}
fs::read_dir(udc_path)
.ok()?
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.next()
}
/// Write string content to a file
///
/// For sysfs files, this function appends a newline and flushes
/// to ensure the kernel processes the write immediately.
///
/// IMPORTANT: sysfs attributes require a single atomic write() syscall.
/// The kernel processes the value on the first write(), so we must
/// build the complete buffer (including newline) before writing.
pub fn write_file(path: &Path, content: &str) -> Result<()> {
// For sysfs files (especially write-only ones like forced_eject),
// we need to use simple O_WRONLY without O_TRUNC
// O_TRUNC may fail on special files or require read permission
let mut file = OpenOptions::new()
.write(true)
.open(path)
.or_else(|e| {
// If open fails, try create (for regular files)
if path.exists() {
Err(e)
} else {
File::create(path)
}
})
.map_err(|e| AppError::Internal(format!("Failed to open {}: {}", path.display(), e)))?;
// Build complete buffer with newline, then write in single syscall.
// This is critical for sysfs - multiple write() calls may cause
// the kernel to only process partial data or return EINVAL.
let data: std::borrow::Cow<[u8]> = if content.ends_with('\n') {
content.as_bytes().into()
} else {
let mut buf = content.as_bytes().to_vec();
buf.push(b'\n');
buf.into()
};
file.write_all(&data)
.map_err(|e| AppError::Internal(format!("Failed to write to {}: {}", path.display(), e)))?;
// Explicitly flush to ensure sysfs processes the write
file.flush()
.map_err(|e| AppError::Internal(format!("Failed to flush {}: {}", path.display(), e)))?;
Ok(())
}
/// Write binary content to a file
pub fn write_bytes(path: &Path, data: &[u8]) -> Result<()> {
let mut file = File::create(path)
.map_err(|e| AppError::Internal(format!("Failed to create {}: {}", path.display(), e)))?;
file.write_all(data)
.map_err(|e| AppError::Internal(format!("Failed to write to {}: {}", path.display(), e)))?;
Ok(())
}
/// Read string content from a file
pub fn read_file(path: &Path) -> Result<String> {
fs::read_to_string(path)
.map(|s| s.trim().to_string())
.map_err(|e| AppError::Internal(format!("Failed to read {}: {}", path.display(), e)))
}
/// Create directory if not exists
pub fn create_dir(path: &Path) -> Result<()> {
fs::create_dir_all(path)
.map_err(|e| AppError::Internal(format!("Failed to create directory {}: {}", path.display(), e)))
}
/// Remove directory
pub fn remove_dir(path: &Path) -> Result<()> {
if path.exists() {
fs::remove_dir(path)
.map_err(|e| AppError::Internal(format!("Failed to remove directory {}: {}", path.display(), e)))?;
}
Ok(())
}
/// Remove file
pub fn remove_file(path: &Path) -> Result<()> {
if path.exists() {
fs::remove_file(path)
.map_err(|e| AppError::Internal(format!("Failed to remove file {}: {}", path.display(), e)))?;
}
Ok(())
}
/// Create symlink
pub fn create_symlink(src: &Path, dest: &Path) -> Result<()> {
std::os::unix::fs::symlink(src, dest)
.map_err(|e| AppError::Internal(format!("Failed to create symlink {} -> {}: {}", dest.display(), src.display(), e)))
}

91
src/otg/endpoint.rs Normal file
View File

@@ -0,0 +1,91 @@
//! USB Endpoint allocation management
use crate::error::{AppError, Result};
/// Default maximum endpoints for typical UDC
pub const DEFAULT_MAX_ENDPOINTS: u8 = 16;
/// Endpoint allocator - manages UDC endpoint resources
#[derive(Debug, Clone)]
pub struct EndpointAllocator {
max_endpoints: u8,
used_endpoints: u8,
}
impl EndpointAllocator {
/// Create a new endpoint allocator
pub fn new(max_endpoints: u8) -> Self {
Self {
max_endpoints,
used_endpoints: 0,
}
}
/// Allocate endpoints for a function
pub fn allocate(&mut self, count: u8) -> Result<()> {
if self.used_endpoints + count > self.max_endpoints {
return Err(AppError::Internal(format!(
"Not enough endpoints: need {}, available {}",
count,
self.available()
)));
}
self.used_endpoints += count;
Ok(())
}
/// Release endpoints
pub fn release(&mut self, count: u8) {
self.used_endpoints = self.used_endpoints.saturating_sub(count);
}
/// Get available endpoint count
pub fn available(&self) -> u8 {
self.max_endpoints.saturating_sub(self.used_endpoints)
}
/// Get used endpoint count
pub fn used(&self) -> u8 {
self.used_endpoints
}
/// Get maximum endpoint count
pub fn max(&self) -> u8 {
self.max_endpoints
}
/// Check if can allocate
pub fn can_allocate(&self, count: u8) -> bool {
self.available() >= count
}
}
impl Default for EndpointAllocator {
fn default() -> Self {
Self::new(DEFAULT_MAX_ENDPOINTS)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allocator() {
let mut alloc = EndpointAllocator::new(8);
assert_eq!(alloc.available(), 8);
alloc.allocate(2).unwrap();
assert_eq!(alloc.available(), 6);
assert_eq!(alloc.used(), 2);
alloc.allocate(4).unwrap();
assert_eq!(alloc.available(), 2);
// Should fail - not enough endpoints
assert!(alloc.allocate(3).is_err());
alloc.release(2);
assert_eq!(alloc.available(), 4);
}
}

42
src/otg/function.rs Normal file
View File

@@ -0,0 +1,42 @@
//! USB Gadget Function trait definition
use std::path::Path;
use crate::error::Result;
/// Function metadata
#[derive(Debug, Clone)]
pub struct FunctionMeta {
/// Function name (e.g., "hid.usb0")
pub name: String,
/// Human-readable description
pub description: String,
/// Number of endpoints used
pub endpoints: u8,
/// Whether the function is enabled
pub enabled: bool,
}
/// USB Gadget Function trait
pub trait GadgetFunction: Send + Sync {
/// Get function name (e.g., "hid.usb0", "mass_storage.usb0")
fn name(&self) -> &str;
/// Get number of endpoints required
fn endpoints_required(&self) -> u8;
/// Get function metadata
fn meta(&self) -> FunctionMeta;
/// Create function directory and configuration in ConfigFS
fn create(&self, gadget_path: &Path) -> Result<()>;
/// Link function to configuration
fn link(&self, config_path: &Path, gadget_path: &Path) -> Result<()>;
/// Unlink function from configuration
fn unlink(&self, config_path: &Path) -> Result<()>;
/// Cleanup function directory
fn cleanup(&self, gadget_path: &Path) -> Result<()>;
}

226
src/otg/hid.rs Normal file
View File

@@ -0,0 +1,226 @@
//! HID Function implementation for USB Gadget
use std::path::{Path, PathBuf};
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 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,
/// Relative mouse (traditional mouse movement)
/// Uses 1 endpoint: IN
MouseRelative,
/// Absolute mouse (touchscreen-like positioning)
/// Uses 1 endpoint: IN
MouseAbsolute,
}
impl HidFunctionType {
/// Get endpoints required for this function type
pub fn endpoints(&self) -> u8 {
match self {
HidFunctionType::Keyboard => 2, // IN + OUT for LED
HidFunctionType::MouseRelative => 1,
HidFunctionType::MouseAbsolute => 1,
}
}
/// Get HID protocol
pub fn protocol(&self) -> u8 {
match self {
HidFunctionType::Keyboard => 1, // Keyboard
HidFunctionType::MouseRelative => 2, // Mouse
HidFunctionType::MouseAbsolute => 2, // Mouse
}
}
/// 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)
}
}
/// Get report length in bytes
pub fn report_length(&self) -> u8 {
match self {
HidFunctionType::Keyboard => 8,
HidFunctionType::MouseRelative => 4,
HidFunctionType::MouseAbsolute => 6,
}
}
/// Get report descriptor
pub fn report_desc(&self) -> &'static [u8] {
match self {
HidFunctionType::Keyboard => KEYBOARD_WITH_LED,
HidFunctionType::MouseRelative => MOUSE_RELATIVE,
HidFunctionType::MouseAbsolute => MOUSE_ABSOLUTE,
}
}
/// Get description
pub fn description(&self) -> &'static str {
match self {
HidFunctionType::Keyboard => "Keyboard",
HidFunctionType::MouseRelative => "Relative Mouse",
HidFunctionType::MouseAbsolute => "Absolute Mouse",
}
}
}
/// HID Function for USB Gadget
#[derive(Debug, Clone)]
pub struct HidFunction {
/// Instance number (usb0, usb1, ...)
instance: u8,
/// Function type
func_type: HidFunctionType,
/// Cached function name (avoids repeated allocation)
name: String,
}
impl HidFunction {
/// Create a keyboard function
pub fn keyboard(instance: u8) -> Self {
Self {
instance,
func_type: HidFunctionType::Keyboard,
name: format!("hid.usb{}", instance),
}
}
/// Create a relative mouse function
pub fn mouse_relative(instance: u8) -> Self {
Self {
instance,
func_type: HidFunctionType::MouseRelative,
name: format!("hid.usb{}", instance),
}
}
/// Create an absolute mouse function
pub fn mouse_absolute(instance: u8) -> Self {
Self {
instance,
func_type: HidFunctionType::MouseAbsolute,
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())
}
/// Get expected device path (e.g., /dev/hidg0)
pub fn device_path(&self) -> PathBuf {
PathBuf::from(format!("/dev/hidg{}", self.instance))
}
}
impl GadgetFunction for HidFunction {
fn name(&self) -> &str {
&self.name
}
fn endpoints_required(&self) -> u8 {
self.func_type.endpoints()
}
fn meta(&self) -> FunctionMeta {
FunctionMeta {
name: self.name().to_string(),
description: self.func_type.description().to_string(),
endpoints: self.endpoints_required(),
enabled: true,
}
}
fn create(&self, gadget_path: &Path) -> Result<()> {
let func_path = self.function_path(gadget_path);
create_dir(&func_path)?;
// Set HID parameters
write_file(&func_path.join("protocol"), &self.func_type.protocol().to_string())?;
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())?;
debug!("Created HID function: {} at {}", self.name(), func_path.display());
Ok(())
}
fn link(&self, config_path: &Path, gadget_path: &Path) -> Result<()> {
let func_path = self.function_path(gadget_path);
let link_path = config_path.join(self.name());
if !link_path.exists() {
create_symlink(&func_path, &link_path)?;
debug!("Linked HID function {} to config", self.name());
}
Ok(())
}
fn unlink(&self, config_path: &Path) -> Result<()> {
let link_path = config_path.join(self.name());
remove_file(&link_path)?;
debug!("Unlinked HID function {}", self.name());
Ok(())
}
fn cleanup(&self, gadget_path: &Path) -> Result<()> {
let func_path = self.function_path(gadget_path);
remove_dir(&func_path)?;
debug!("Cleaned up HID function {}", self.name());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hid_function_types() {
assert_eq!(HidFunctionType::Keyboard.endpoints(), 2);
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);
}
#[test]
fn test_hid_function_names() {
let kb = HidFunction::keyboard(0);
assert_eq!(kb.name(), "hid.usb0");
assert_eq!(kb.device_path(), PathBuf::from("/dev/hidg0"));
let mouse = HidFunction::mouse_relative(1);
assert_eq!(mouse.name(), "hid.usb1");
}
}

394
src/otg/manager.rs Normal file
View File

@@ -0,0 +1,394 @@
//! OTG Gadget Manager - unified management for USB Gadget functions
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tracing::{debug, error, info, warn};
use super::configfs::{
create_dir, find_udc, is_configfs_available, remove_dir, write_file, CONFIGFS_PATH,
DEFAULT_GADGET_NAME, USB_BCD_DEVICE, USB_BCD_USB, USB_PRODUCT_ID, USB_VENDOR_ID,
};
use super::endpoint::{EndpointAllocator, DEFAULT_MAX_ENDPOINTS};
use super::function::{FunctionMeta, GadgetFunction};
use super::hid::HidFunction;
use super::msd::MsdFunction;
use crate::error::{AppError, Result};
/// OTG Gadget Manager - unified management for HID and MSD
pub struct OtgGadgetManager {
/// Gadget name
gadget_name: String,
/// Gadget path in ConfigFS
gadget_path: PathBuf,
/// Configuration path
config_path: PathBuf,
/// Endpoint allocator
endpoint_allocator: EndpointAllocator,
/// HID instance counter
hid_instance: u8,
/// MSD instance counter
msd_instance: u8,
/// Registered functions
functions: Vec<Box<dyn GadgetFunction>>,
/// Function metadata
meta: HashMap<String, FunctionMeta>,
/// Bound UDC name
bound_udc: Option<String>,
/// Whether gadget was created by us
created_by_us: bool,
}
impl OtgGadgetManager {
/// Create a new gadget manager with default settings
pub fn new() -> Self {
Self::with_config(DEFAULT_GADGET_NAME, DEFAULT_MAX_ENDPOINTS)
}
/// Create a new gadget manager with custom configuration
pub fn with_config(gadget_name: &str, max_endpoints: u8) -> Self {
let gadget_path = PathBuf::from(CONFIGFS_PATH).join(gadget_name);
let config_path = gadget_path.join("configs/c.1");
Self {
gadget_name: gadget_name.to_string(),
gadget_path,
config_path,
endpoint_allocator: EndpointAllocator::new(max_endpoints),
hid_instance: 0,
msd_instance: 0,
// Pre-allocate for typical use: 3 HID (keyboard, rel mouse, abs mouse) + 1 MSD
functions: Vec::with_capacity(4),
meta: HashMap::with_capacity(4),
bound_udc: None,
created_by_us: false,
}
}
/// Check if ConfigFS is available
pub fn is_available() -> bool {
is_configfs_available()
}
/// Find available UDC
pub fn find_udc() -> Option<String> {
find_udc()
}
/// Check if gadget exists
pub fn gadget_exists(&self) -> bool {
self.gadget_path.exists()
}
/// Check if gadget is bound to UDC
pub fn is_bound(&self) -> bool {
let udc_file = self.gadget_path.join("UDC");
if let Ok(content) = fs::read_to_string(&udc_file) {
!content.trim().is_empty()
} else {
false
}
}
/// 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);
let device_path = func.device_path();
self.add_function(Box::new(func))?;
self.hid_instance += 1;
Ok(device_path)
}
/// Add relative mouse function
pub fn add_mouse_relative(&mut self) -> Result<PathBuf> {
let func = HidFunction::mouse_relative(self.hid_instance);
let device_path = func.device_path();
self.add_function(Box::new(func))?;
self.hid_instance += 1;
Ok(device_path)
}
/// Add absolute mouse function
pub fn add_mouse_absolute(&mut self) -> Result<PathBuf> {
let func = HidFunction::mouse_absolute(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);
let func_clone = func.clone();
self.add_function(Box::new(func))?;
self.msd_instance += 1;
Ok(func_clone)
}
/// Add a generic function
fn add_function(&mut self, func: Box<dyn GadgetFunction>) -> Result<()> {
let endpoints = func.endpoints_required();
// Check endpoint availability
if !self.endpoint_allocator.can_allocate(endpoints) {
return Err(AppError::Internal(format!(
"Not enough endpoints for function {}: need {}, available {}",
func.name(),
endpoints,
self.endpoint_allocator.available()
)));
}
// Allocate endpoints
self.endpoint_allocator.allocate(endpoints)?;
// Store metadata
self.meta.insert(func.name().to_string(), func.meta());
// Store function
self.functions.push(func);
Ok(())
}
/// Setup the gadget (create directories and configure)
pub fn setup(&mut self) -> Result<()> {
info!("Setting up OTG USB Gadget: {}", self.gadget_name);
// Check ConfigFS availability
if !Self::is_available() {
return Err(AppError::Internal(
"ConfigFS not available. Is it mounted at /sys/kernel/config?".to_string(),
));
}
// Check if gadget already exists and is bound
if self.gadget_exists() {
if self.is_bound() {
info!("Gadget already exists and is bound, skipping setup");
return Ok(());
}
warn!("Gadget exists but not bound, will reconfigure");
self.cleanup()?;
}
// Create gadget directory
create_dir(&self.gadget_path)?;
self.created_by_us = true;
// Set device descriptors
self.set_device_descriptors()?;
// Create strings
self.create_strings()?;
// Create configuration
self.create_configuration()?;
// Create and link all functions
for func in &self.functions {
func.create(&self.gadget_path)?;
func.link(&self.config_path, &self.gadget_path)?;
}
info!("OTG USB Gadget setup complete");
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())
})?;
info!("Binding gadget to UDC: {}", udc);
write_file(&self.gadget_path.join("UDC"), &udc)?;
self.bound_udc = Some(udc);
Ok(())
}
/// Unbind gadget from UDC
pub fn unbind(&mut self) -> Result<()> {
if self.is_bound() {
write_file(&self.gadget_path.join("UDC"), "")?;
self.bound_udc = None;
info!("Unbound gadget from UDC");
}
Ok(())
}
/// Cleanup all resources
pub fn cleanup(&mut self) -> Result<()> {
if !self.gadget_exists() {
return Ok(());
}
info!("Cleaning up OTG USB Gadget: {}", self.gadget_name);
// Unbind from UDC first
let _ = self.unbind();
// Unlink and cleanup functions
for func in self.functions.iter().rev() {
let _ = func.unlink(&self.config_path);
}
// Remove config strings
let config_strings = self.config_path.join("strings/0x409");
let _ = remove_dir(&config_strings);
let _ = remove_dir(&self.config_path);
// Cleanup functions
for func in self.functions.iter().rev() {
let _ = func.cleanup(&self.gadget_path);
}
// Remove gadget strings
let gadget_strings = self.gadget_path.join("strings/0x409");
let _ = remove_dir(&gadget_strings);
// Remove gadget directory
if let Err(e) = remove_dir(&self.gadget_path) {
warn!("Could not remove gadget directory: {}", e);
}
self.created_by_us = false;
info!("OTG USB Gadget cleanup complete");
Ok(())
}
/// Set USB device descriptors
fn set_device_descriptors(&self) -> Result<()> {
write_file(&self.gadget_path.join("idVendor"), &format!("0x{:04x}", USB_VENDOR_ID))?;
write_file(&self.gadget_path.join("idProduct"), &format!("0x{:04x}", USB_PRODUCT_ID))?;
write_file(&self.gadget_path.join("bcdDevice"), &format!("0x{:04x}", USB_BCD_DEVICE))?;
write_file(&self.gadget_path.join("bcdUSB"), &format!("0x{:04x}", USB_BCD_USB))?;
write_file(&self.gadget_path.join("bDeviceClass"), "0x00")?; // Composite device
write_file(&self.gadget_path.join("bDeviceSubClass"), "0x00")?;
write_file(&self.gadget_path.join("bDeviceProtocol"), "0x00")?;
debug!("Set device descriptors");
Ok(())
}
/// Create USB strings
fn create_strings(&self) -> Result<()> {
let strings_path = self.gadget_path.join("strings/0x409");
create_dir(&strings_path)?;
write_file(&strings_path.join("serialnumber"), "0123456789")?;
write_file(&strings_path.join("manufacturer"), "One-KVM")?;
write_file(&strings_path.join("product"), "One-KVM HID Device")?;
debug!("Created USB strings");
Ok(())
}
/// Create configuration
fn create_configuration(&self) -> Result<()> {
create_dir(&self.config_path)?;
// Create config strings
let strings_path = self.config_path.join("strings/0x409");
create_dir(&strings_path)?;
write_file(&strings_path.join("configuration"), "Config 1: HID + MSD")?;
// Set max power (500mA)
write_file(&self.config_path.join("MaxPower"), "500")?;
debug!("Created configuration c.1");
Ok(())
}
/// Get function metadata
pub fn get_meta(&self) -> &HashMap<String, FunctionMeta> {
&self.meta
}
/// Get endpoint usage info
pub fn endpoint_info(&self) -> (u8, u8) {
(self.endpoint_allocator.used(), self.endpoint_allocator.max())
}
/// Get gadget path
pub fn gadget_path(&self) -> &PathBuf {
&self.gadget_path
}
}
impl Default for OtgGadgetManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for OtgGadgetManager {
fn drop(&mut self) {
if self.created_by_us {
if let Err(e) = self.cleanup() {
error!("Failed to cleanup OTG gadget on drop: {}", e);
}
}
}
}
/// Wait for HID devices to become available
///
/// Uses exponential backoff starting from 10ms, capped at 100ms,
/// to reduce CPU usage while still providing fast response.
pub async fn wait_for_hid_devices(device_paths: &[PathBuf], timeout_ms: u64) -> bool {
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_millis(timeout_ms);
// Exponential backoff: start at 10ms, double each time, cap at 100ms
let mut delay_ms = 10u64;
const MAX_DELAY_MS: u64 = 100;
while start.elapsed() < timeout {
if device_paths.iter().all(|p| p.exists()) {
return true;
}
// Calculate remaining time to avoid overshooting timeout
let remaining = timeout.saturating_sub(start.elapsed());
let sleep_duration = std::time::Duration::from_millis(delay_ms).min(remaining);
if sleep_duration.is_zero() {
break;
}
tokio::time::sleep(sleep_duration).await;
// Exponential backoff with cap
delay_ms = (delay_ms * 2).min(MAX_DELAY_MS);
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manager_creation() {
let manager = OtgGadgetManager::new();
assert_eq!(manager.gadget_name, DEFAULT_GADGET_NAME);
assert!(!manager.gadget_exists()); // Won't exist in test environment
}
#[test]
fn test_endpoint_tracking() {
let mut manager = OtgGadgetManager::with_config("test", 8);
// Keyboard uses 2 endpoints
let _ = manager.add_keyboard();
assert_eq!(manager.endpoint_allocator.used(), 2);
// Mouse uses 1 endpoint each
let _ = manager.add_mouse_relative();
let _ = manager.add_mouse_absolute();
assert_eq!(manager.endpoint_allocator.used(), 4);
}
}

35
src/otg/mod.rs Normal file
View File

@@ -0,0 +1,35 @@
//! OTG USB Gadget unified management module
//!
//! This module provides unified management for USB Gadget functions:
//! - HID (Keyboard, Mouse)
//! - MSD (Mass Storage Device)
//!
//! Architecture:
//! ```text
//! OtgService (high-level coordination)
//! └── OtgGadgetManager (gadget lifecycle)
//! ├── EndpointAllocator (manages UDC endpoints)
//! ├── HidFunction (keyboard, mouse_rel, mouse_abs)
//! └── MsdFunction (mass storage)
//! ```
//!
//! The recommended way to use this module is through `OtgService`, which provides
//! a high-level interface for enabling/disabling HID and MSD functions independently.
//! Both `HidController` and `MsdController` should share the same `OtgService` instance.
pub mod configfs;
pub mod endpoint;
pub mod function;
pub mod hid;
pub mod manager;
pub mod msd;
pub mod report_desc;
pub mod service;
pub use endpoint::EndpointAllocator;
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 service::{HidDevicePaths, OtgService, OtgServiceState};

411
src/otg/msd.rs Normal file
View File

@@ -0,0 +1,411 @@
//! MSD (Mass Storage Device) Function implementation for USB Gadget
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};
use super::configfs::{create_dir, create_symlink, remove_dir, remove_file, write_file};
use super::function::{FunctionMeta, GadgetFunction};
use crate::error::{AppError, Result};
/// MSD LUN configuration
#[derive(Debug, Clone)]
pub struct MsdLunConfig {
/// File/image path to expose
pub file: PathBuf,
/// Mount as CD-ROM
pub cdrom: bool,
/// Read-only mode
pub ro: bool,
/// Removable media
pub removable: bool,
/// Disable Force Unit Access
pub nofua: bool,
}
impl Default for MsdLunConfig {
fn default() -> Self {
Self {
file: PathBuf::new(),
cdrom: false,
ro: false,
removable: true,
nofua: true,
}
}
}
impl MsdLunConfig {
/// Create CD-ROM configuration
pub fn cdrom(file: PathBuf) -> Self {
Self {
file,
cdrom: true,
ro: true,
removable: true,
nofua: true,
}
}
/// Create disk configuration
pub fn disk(file: PathBuf, read_only: bool) -> Self {
Self {
file,
cdrom: false,
ro: read_only,
removable: true,
nofua: true,
}
}
}
/// MSD Function for USB Gadget
#[derive(Debug, Clone)]
pub struct MsdFunction {
/// Instance number (usb0, usb1, ...)
instance: u8,
/// Cached function name (avoids repeated allocation)
name: String,
}
impl MsdFunction {
/// Create a new MSD function
pub fn new(instance: u8) -> Self {
Self {
instance,
name: format!("mass_storage.usb{}", instance),
}
}
/// Get function path in gadget
fn function_path(&self, gadget_path: &Path) -> PathBuf {
gadget_path.join("functions").join(self.name())
}
/// Get LUN path
fn lun_path(&self, gadget_path: &Path, lun: u8) -> PathBuf {
self.function_path(gadget_path).join(format!("lun.{}", lun))
}
/// Configure a LUN with specified settings (async version)
///
/// This is the preferred method for async contexts. It runs the blocking
/// file I/O and USB timing operations in a separate thread pool.
pub async fn configure_lun_async(
&self,
gadget_path: &Path,
lun: u8,
config: &MsdLunConfig,
) -> Result<()> {
let gadget_path = gadget_path.to_path_buf();
let config = config.clone();
let this = self.clone();
tokio::task::spawn_blocking(move || this.configure_lun(&gadget_path, lun, &config))
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
}
/// Configure a LUN with specified settings
/// Note: This should be called after the gadget is set up
///
/// This implementation is based on PiKVM's MSD drive configuration.
/// Key improvements:
/// - Uses forced_eject when available (safer than clearing file directly)
/// - Reduced sleep times to minimize HID interference
/// - Better retry logic for EBUSY errors
///
/// **Note**: This is a blocking function. In async contexts, prefer
/// `configure_lun_async` to avoid blocking the runtime.
pub fn configure_lun(&self, gadget_path: &Path, lun: u8, config: &MsdLunConfig) -> Result<()> {
let lun_path = self.lun_path(gadget_path, lun);
if !lun_path.exists() {
create_dir(&lun_path)?;
}
// Batch read all current values to minimize syscalls
let read_attr = |attr: &str| -> String {
fs::read_to_string(lun_path.join(attr))
.unwrap_or_default()
.trim()
.to_string()
};
let current_cdrom = read_attr("cdrom");
let current_ro = read_attr("ro");
let current_removable = read_attr("removable");
let current_nofua = read_attr("nofua");
// Prepare new values
let new_cdrom = if config.cdrom { "1" } else { "0" };
let new_ro = if config.ro { "1" } else { "0" };
let new_removable = if config.removable { "1" } else { "0" };
let new_nofua = if config.nofua { "1" } else { "0" };
// Disconnect current file first using forced_eject if available (PiKVM approach)
let forced_eject_path = lun_path.join("forced_eject");
if forced_eject_path.exists() {
// forced_eject is safer - it forcibly detaches regardless of host state
debug!("Using forced_eject to clear LUN {}", lun);
let _ = write_file(&forced_eject_path, "1");
} else {
// Fallback to clearing file directly
let _ = write_file(&lun_path.join("file"), "");
}
// Brief yield to allow USB stack to process the disconnect
// Reduced from 200ms to 50ms - let USB protocol handle timing
std::thread::sleep(std::time::Duration::from_millis(50));
// Write only changed attributes
let cdrom_changed = current_cdrom != new_cdrom;
if cdrom_changed {
debug!("Updating LUN {} cdrom: {} -> {}", lun, current_cdrom, new_cdrom);
write_file(&lun_path.join("cdrom"), new_cdrom)?;
}
if current_ro != new_ro {
debug!("Updating LUN {} ro: {} -> {}", lun, current_ro, new_ro);
write_file(&lun_path.join("ro"), new_ro)?;
}
if current_removable != new_removable {
debug!("Updating LUN {} removable: {} -> {}", lun, current_removable, new_removable);
write_file(&lun_path.join("removable"), new_removable)?;
}
if current_nofua != new_nofua {
debug!("Updating LUN {} nofua: {} -> {}", lun, current_nofua, new_nofua);
write_file(&lun_path.join("nofua"), new_nofua)?;
}
// If cdrom mode changed, brief yield for USB host
if cdrom_changed {
debug!("CDROM mode changed, brief yield for USB host");
std::thread::sleep(std::time::Duration::from_millis(50));
}
// Set file path (this triggers the actual mount) - with retry on EBUSY
if config.file.exists() {
let file_path = config.file.to_string_lossy();
let mut last_error = None;
for attempt in 0..5 {
match write_file(&lun_path.join("file"), file_path.as_ref()) {
Ok(_) => {
info!(
"LUN {} configured with file: {} (cdrom={}, ro={})",
lun,
config.file.display(),
config.cdrom,
config.ro
);
return Ok(());
}
Err(e) => {
// Check if it's EBUSY (error code 16)
let is_busy = e.to_string().contains("Device or resource busy")
|| e.to_string().contains("os error 16");
if is_busy && attempt < 4 {
warn!(
"LUN {} file write busy, retrying (attempt {}/5)",
lun,
attempt + 1
);
// Exponential backoff: 50, 100, 200, 400ms
std::thread::sleep(std::time::Duration::from_millis(50 << attempt));
last_error = Some(e);
continue;
}
return Err(e);
}
}
}
// If we get here, all retries failed
if let Some(e) = last_error {
return Err(e);
}
} else if !config.file.as_os_str().is_empty() {
warn!("LUN {} file does not exist: {}", lun, config.file.display());
}
Ok(())
}
/// Disconnect LUN (async version)
///
/// Preferred for async contexts.
pub async fn disconnect_lun_async(&self, gadget_path: &Path, lun: u8) -> Result<()> {
let gadget_path = gadget_path.to_path_buf();
let this = self.clone();
tokio::task::spawn_blocking(move || this.disconnect_lun(&gadget_path, lun))
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
}
/// Disconnect LUN (clear file)
///
/// This method uses forced_eject when available, which is safer than
/// directly clearing the file path. Based on PiKVM's implementation.
/// See: https://docs.kernel.org/usb/mass-storage.html
pub fn disconnect_lun(&self, gadget_path: &Path, lun: u8) -> Result<()> {
let lun_path = self.lun_path(gadget_path, lun);
if lun_path.exists() {
// Prefer forced_eject if available (PiKVM approach)
// forced_eject forcibly detaches the backing file regardless of host state
let forced_eject_path = lun_path.join("forced_eject");
if forced_eject_path.exists() {
debug!("Using forced_eject to disconnect LUN {} at {:?}", lun, forced_eject_path);
match write_file(&forced_eject_path, "1") {
Ok(_) => debug!("forced_eject write succeeded"),
Err(e) => {
warn!("forced_eject write failed: {}, falling back to clearing file", e);
write_file(&lun_path.join("file"), "")?;
}
}
} else {
// Fallback to clearing file directly
write_file(&lun_path.join("file"), "")?;
}
info!("LUN {} disconnected", lun);
}
Ok(())
}
/// Get current LUN file path
pub fn get_lun_file(&self, gadget_path: &Path, lun: u8) -> Option<PathBuf> {
let lun_path = self.lun_path(gadget_path, lun);
let file_path = lun_path.join("file");
if let Ok(content) = fs::read_to_string(&file_path) {
let content = content.trim();
if !content.is_empty() {
return Some(PathBuf::from(content));
}
}
None
}
/// Check if LUN is connected
pub fn is_lun_connected(&self, gadget_path: &Path, lun: u8) -> bool {
self.get_lun_file(gadget_path, lun).is_some()
}
}
impl GadgetFunction for MsdFunction {
fn name(&self) -> &str {
&self.name
}
fn endpoints_required(&self) -> u8 {
2 // IN + OUT for bulk transfers
}
fn meta(&self) -> FunctionMeta {
FunctionMeta {
name: self.name().to_string(),
description: if self.instance == 0 {
"Mass Storage Drive".to_string()
} else {
format!("Extra Drive #{}", self.instance)
},
endpoints: self.endpoints_required(),
enabled: true,
}
}
fn create(&self, gadget_path: &Path) -> Result<()> {
let func_path = self.function_path(gadget_path);
create_dir(&func_path)?;
// Set stall to 0 (workaround for some hosts)
let stall_path = func_path.join("stall");
if stall_path.exists() {
let _ = write_file(&stall_path, "0");
}
// LUN 0 is created automatically, but ensure it exists
let lun0_path = func_path.join("lun.0");
if !lun0_path.exists() {
create_dir(&lun0_path)?;
}
// Set default LUN 0 parameters
let _ = write_file(&lun0_path.join("cdrom"), "0");
let _ = write_file(&lun0_path.join("ro"), "0");
let _ = write_file(&lun0_path.join("removable"), "1");
let _ = write_file(&lun0_path.join("nofua"), "1");
debug!("Created MSD function: {}", self.name());
Ok(())
}
fn link(&self, config_path: &Path, gadget_path: &Path) -> Result<()> {
let func_path = self.function_path(gadget_path);
let link_path = config_path.join(self.name());
if !link_path.exists() {
create_symlink(&func_path, &link_path)?;
debug!("Linked MSD function {} to config", self.name());
}
Ok(())
}
fn unlink(&self, config_path: &Path) -> Result<()> {
let link_path = config_path.join(self.name());
remove_file(&link_path)?;
debug!("Unlinked MSD function {}", self.name());
Ok(())
}
fn cleanup(&self, gadget_path: &Path) -> Result<()> {
let func_path = self.function_path(gadget_path);
// Disconnect all LUNs first
for lun in 0..8 {
let _ = self.disconnect_lun(gadget_path, lun);
}
// Remove function directory
if let Err(e) = remove_dir(&func_path) {
warn!("Could not remove MSD function directory: {}", e);
}
debug!("Cleaned up MSD function {}", self.name());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lun_config_cdrom() {
let config = MsdLunConfig::cdrom(PathBuf::from("/tmp/test.iso"));
assert!(config.cdrom);
assert!(config.ro);
assert!(config.removable);
}
#[test]
fn test_lun_config_disk() {
let config = MsdLunConfig::disk(PathBuf::from("/tmp/test.img"), false);
assert!(!config.cdrom);
assert!(!config.ro);
assert!(config.removable);
}
#[test]
fn test_msd_function_name() {
let msd = MsdFunction::new(0);
assert_eq!(msd.name(), "mass_storage.usb0");
assert_eq!(msd.endpoints_required(), 2);
}
}

160
src/otg/report_desc.rs Normal file
View File

@@ -0,0 +1,160 @@
//! HID Report Descriptors
/// Keyboard HID Report Descriptor with LED output support
/// 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] = &[
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 (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)
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)
/// [1] X movement (signed 8-bit)
/// [2] Y movement (signed 8-bit)
/// [3] Wheel (signed 8-bit)
pub const MOUSE_RELATIVE: &[u8] = &[
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
// Buttons (5 bits)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5) - 5 buttons
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data, Variable, Absolute) - Button bits
// Padding (3 bits)
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x81, 0x01, // Input (Constant) - Padding
// X, Y movement
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x06, // Input (Data, Variable, Relative) - X, Y
// Wheel
0x09, 0x38, // Usage (Wheel)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x06, // Input (Data, Variable, Relative) - Wheel
0xC0, // End Collection
0xC0, // End Collection
];
/// Absolute Mouse HID Report Descriptor (6 bytes report)
/// Report format:
/// [0] Buttons (5 bits) + padding (3 bits)
/// [1-2] X position (16-bit, 0-32767)
/// [3-4] Y position (16-bit, 0-32767)
/// [5] Wheel (signed 8-bit)
pub const MOUSE_ABSOLUTE: &[u8] = &[
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
// Buttons (5 bits)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5) - 5 buttons
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data, Variable, Absolute) - Button bits
// Padding (3 bits)
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x81, 0x01, // Input (Constant) - Padding
// X position (16-bit absolute)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x16, 0x00, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x75, 0x10, // Report Size (16)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data, Variable, Absolute) - X
// Y position (16-bit absolute)
0x09, 0x31, // Usage (Y)
0x16, 0x00, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x75, 0x10, // Report Size (16)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data, Variable, Absolute) - Y
// Wheel
0x09, 0x38, // Usage (Wheel)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x06, // Input (Data, Variable, Relative) - Wheel
0xC0, // End Collection
0xC0, // End Collection
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_report_descriptor_sizes() {
assert!(!KEYBOARD_WITH_LED.is_empty());
assert!(!MOUSE_RELATIVE.is_empty());
assert!(!MOUSE_ABSOLUTE.is_empty());
}
}

503
src/otg/service.rs Normal file
View File

@@ -0,0 +1,503 @@
//! 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
//! ```
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, OtgGadgetManager};
use super::msd::MsdFunction;
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)]
pub struct HidDevicePaths {
pub keyboard: PathBuf,
pub mouse_relative: PathBuf,
pub mouse_absolute: PathBuf,
}
impl Default for HidDevicePaths {
fn default() -> Self {
Self {
keyboard: PathBuf::from("/dev/hidg0"),
mouse_relative: PathBuf::from("/dev/hidg1"),
mouse_absolute: PathBuf::from("/dev/hidg2"),
}
}
}
/// OTG Service state
#[derive(Debug, Clone, Default)]
pub struct OtgServiceState {
/// Whether the gadget is created and bound
pub gadget_active: bool,
/// Whether HID functions are enabled
pub hid_enabled: bool,
/// Whether MSD function is enabled
pub msd_enabled: bool,
/// HID device paths (set after gadget setup)
pub hid_paths: Option<HidDevicePaths>,
/// 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>>,
/// Current state
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,
}
impl OtgService {
/// Create a new OTG service
pub fn new() -> Self {
Self {
manager: Mutex::new(None),
state: RwLock::new(OtgServiceState::default()),
msd_function: RwLock::new(None),
requested_flags: AtomicU8::new(0),
}
}
/// 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);
}
}
/// Check if OTG is available on this system
pub fn is_available() -> bool {
OtgGadgetManager::is_available() && OtgGadgetManager::find_udc().is_some()
}
/// Get current service state
pub async fn state(&self) -> OtgServiceState {
self.state.read().await.clone()
}
/// Check if gadget is active
pub async fn is_gadget_active(&self) -> bool {
self.state.read().await.gadget_active
}
/// Check if HID is enabled
pub async fn is_hid_enabled(&self) -> bool {
self.state.read().await.hid_enabled
}
/// Check if MSD is enabled
pub async fn is_msd_enabled(&self) -> bool {
self.state.read().await.msd_enabled
}
/// Get gadget path (for MSD LUN configuration)
pub async fn gadget_path(&self) -> Option<PathBuf> {
let manager = self.manager.lock().await;
manager.as_ref().map(|m| m.gadget_path().clone())
}
/// Get HID device paths
pub async fn hid_device_paths(&self) -> Option<HidDevicePaths> {
self.state.read().await.hid_paths.clone()
}
/// 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
{
let state = self.state.read().await;
if state.hid_enabled {
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()))
}
/// 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
{
let state = self.state.read().await;
if !state.hid_enabled {
info!("HID already disabled");
return Ok(());
}
}
// Recreate gadget without HID (or destroy if MSD also disabled)
self.recreate_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();
info!(
"Recreating gadget with: HID={}, MSD={}",
hid_requested, msd_requested
);
// Check if gadget already matches requested state
{
let state = self.state.read().await;
if state.gadget_active
&& state.hid_enabled == hid_requested
&& state.msd_enabled == msd_requested
{
info!("Gadget already has requested functions, skipping recreate");
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");
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.error = None;
}
// If nothing requested, we're done
if !hid_requested && !msd_requested {
info!("No functions requested, gadget destroyed");
return Ok(());
}
// 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());
return Err(AppError::Internal(error));
}
// Create new gadget manager
let mut manager = OtgGadgetManager::new();
let mut hid_paths = None;
// Add HID functions if requested
if hid_requested {
match (
manager.add_keyboard(),
manager.add_mouse_relative(),
manager.add_mouse_absolute(),
) {
(Ok(kb), Ok(rel), Ok(abs)) => {
hid_paths = Some(HidDevicePaths {
keyboard: kb,
mouse_relative: rel,
mouse_absolute: abs,
});
debug!("HID functions added to gadget");
}
(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());
return Err(AppError::Internal(error));
}
}
}
// Add MSD function if requested
let msd_func = if msd_requested {
match manager.add_msd() {
Ok(func) => {
debug!("MSD function added to gadget");
Some(func)
}
Err(e) => {
let error = format!("Failed to add MSD function: {}", e);
let mut state = self.state.write().await;
state.error = Some(error.clone());
return Err(AppError::Internal(error));
}
}
} else {
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());
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
let _ = manager.cleanup();
return Err(AppError::Internal(error));
}
// Wait for HID devices to appear
if let Some(ref paths) = hid_paths {
let device_paths = vec![
paths.keyboard.clone(),
paths.mouse_relative.clone(),
paths.mouse_absolute.clone(),
];
if !wait_for_hid_devices(&device_paths, 2000).await {
warn!("HID devices did not appear after gadget setup");
}
}
// Store manager and update state
{
*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_paths = hid_paths;
state.error = None;
}
info!("Gadget created successfully");
Ok(())
}
/// 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);
// Cleanup gadget
let mut manager = self.manager.lock().await;
if let Some(mut m) = manager.take() {
if let Err(e) = m.cleanup() {
warn!("Error cleaning up gadget during shutdown: {}", e);
}
}
// Clear state
*self.msd_function.write().await = None;
{
let mut state = self.state.write().await;
*state = OtgServiceState::default();
}
info!("OTG service shutdown complete");
Ok(())
}
}
impl Default for OtgService {
fn default() -> Self {
Self::new()
}
}
impl Drop for OtgService {
fn drop(&mut self) {
// Gadget cleanup is handled by OtgGadgetManager's Drop
debug!("OtgService dropping");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_creation() {
let service = OtgService::new();
// Just test that creation doesn't panic
assert!(!OtgService::is_available() || true); // Depends on environment
}
#[tokio::test]
async fn test_initial_state() {
let service = OtgService::new();
let state = service.state().await;
assert!(!state.gadget_active);
assert!(!state.hid_enabled);
assert!(!state.msd_enabled);
}
}