mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-31 18:11:54 +08:00
init
This commit is contained in:
138
src/otg/configfs.rs
Normal file
138
src/otg/configfs.rs
Normal 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
91
src/otg/endpoint.rs
Normal 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
42
src/otg/function.rs
Normal 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
226
src/otg/hid.rs
Normal 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
394
src/otg/manager.rs
Normal 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
35
src/otg/mod.rs
Normal 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
411
src/otg/msd.rs
Normal 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
160
src/otg/report_desc.rs
Normal 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
503
src/otg/service.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user