mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
refactor: 删除部分多余的代码和注释
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
//! ConfigFS file operations for USB Gadget
|
||||
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
@@ -7,34 +5,18 @@ use std::process::Command;
|
||||
|
||||
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) - default value
|
||||
pub const DEFAULT_USB_VENDOR_ID: u16 = 0x1d6b;
|
||||
|
||||
/// USB Product ID (Multifunction Composite Gadget) - default value
|
||||
pub const DEFAULT_USB_PRODUCT_ID: u16 = 0x0104;
|
||||
|
||||
/// USB device version - default value
|
||||
pub const DEFAULT_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()
|
||||
}
|
||||
|
||||
/// Ensure libcomposite support is available for USB gadget operations.
|
||||
///
|
||||
/// This is a best-effort runtime fallback for systems where `libcomposite`
|
||||
/// is built as a module and not loaded yet. It does not try to mount configfs;
|
||||
/// mounting remains an explicit system responsibility.
|
||||
/// Loads `libcomposite` if needed; does not mount configfs.
|
||||
pub fn ensure_libcomposite_loaded() -> Result<()> {
|
||||
if is_configfs_available() {
|
||||
return Ok(());
|
||||
@@ -66,7 +48,6 @@ pub fn ensure_libcomposite_loaded() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Find available UDC (USB Device Controller)
|
||||
pub fn find_udc() -> Option<String> {
|
||||
let udc_path = Path::new("/sys/class/udc");
|
||||
if !udc_path.exists() {
|
||||
@@ -80,40 +61,17 @@ pub fn find_udc() -> Option<String> {
|
||||
.next()
|
||||
}
|
||||
|
||||
/// Check if UDC is known to have low endpoint resources
|
||||
pub fn is_low_endpoint_udc(name: &str) -> bool {
|
||||
let name = name.to_ascii_lowercase();
|
||||
name.contains("musb") || name.contains("musb-hdrc")
|
||||
}
|
||||
|
||||
/// Resolve preferred UDC name if available, otherwise auto-detect
|
||||
pub fn resolve_udc_name(preferred: Option<&str>) -> Option<String> {
|
||||
if let Some(name) = preferred {
|
||||
let path = Path::new("/sys/class/udc").join(name);
|
||||
if path.exists() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
find_udc()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Sysfs/configfs: one write syscall with final buffer (incl. newline when needed).
|
||||
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 {
|
||||
@@ -122,9 +80,6 @@ pub fn write_file(path: &Path, content: &str) -> Result<()> {
|
||||
})
|
||||
.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 {
|
||||
@@ -136,14 +91,12 @@ pub fn write_file(path: &Path, content: &str) -> Result<()> {
|
||||
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)))?;
|
||||
@@ -154,14 +107,6 @@ pub fn write_bytes(path: &Path, data: &[u8]) -> Result<()> {
|
||||
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!(
|
||||
@@ -172,7 +117,6 @@ pub fn create_dir(path: &Path) -> Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove directory
|
||||
pub fn remove_dir(path: &Path) -> Result<()> {
|
||||
if path.exists() {
|
||||
fs::remove_dir(path).map_err(|e| {
|
||||
@@ -186,7 +130,6 @@ pub fn remove_dir(path: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove file
|
||||
pub fn remove_file(path: &Path) -> Result<()> {
|
||||
if path.exists() {
|
||||
fs::remove_file(path).map_err(|e| {
|
||||
@@ -196,7 +139,6 @@ pub fn remove_file(path: &Path) -> Result<()> {
|
||||
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!(
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
//! 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,
|
||||
@@ -13,7 +9,6 @@ pub struct EndpointAllocator {
|
||||
}
|
||||
|
||||
impl EndpointAllocator {
|
||||
/// Create a new endpoint allocator
|
||||
pub fn new(max_endpoints: u8) -> Self {
|
||||
Self {
|
||||
max_endpoints,
|
||||
@@ -21,7 +16,6 @@ impl EndpointAllocator {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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!(
|
||||
@@ -34,27 +28,22 @@ impl EndpointAllocator {
|
||||
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
|
||||
}
|
||||
@@ -82,7 +71,6 @@ mod tests {
|
||||
alloc.allocate(4).unwrap();
|
||||
assert_eq!(alloc.available(), 2);
|
||||
|
||||
// Should fail - not enough endpoints
|
||||
assert!(alloc.allocate(3).is_err());
|
||||
|
||||
alloc.release(2);
|
||||
|
||||
@@ -1,42 +1,17 @@
|
||||
//! 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<()>;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,24 @@
|
||||
//! 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::function::GadgetFunction;
|
||||
use super::report_desc::{
|
||||
CONSUMER_CONTROL, KEYBOARD, KEYBOARD_WITH_LED, MOUSE_ABSOLUTE, MOUSE_RELATIVE,
|
||||
};
|
||||
use crate::error::Result;
|
||||
|
||||
/// HID function type
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HidFunctionType {
|
||||
/// Keyboard
|
||||
Keyboard,
|
||||
/// Relative mouse (traditional mouse movement)
|
||||
/// Uses 1 endpoint: IN
|
||||
MouseRelative,
|
||||
/// Absolute mouse (touchscreen-like positioning)
|
||||
/// Uses 1 endpoint: IN
|
||||
MouseAbsolute,
|
||||
/// Consumer control (multimedia keys)
|
||||
/// Uses 1 endpoint: IN
|
||||
ConsumerControl,
|
||||
}
|
||||
|
||||
impl HidFunctionType {
|
||||
/// Get the base endpoint cost for this function type.
|
||||
pub fn endpoints(&self) -> u8 {
|
||||
match self {
|
||||
HidFunctionType::Keyboard => 1,
|
||||
@@ -39,27 +28,24 @@ impl HidFunctionType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get HID protocol
|
||||
pub fn protocol(&self) -> u8 {
|
||||
match self {
|
||||
HidFunctionType::Keyboard => 1, // Keyboard
|
||||
HidFunctionType::MouseRelative => 2, // Mouse
|
||||
HidFunctionType::MouseAbsolute => 2, // Mouse
|
||||
HidFunctionType::ConsumerControl => 0, // None
|
||||
HidFunctionType::Keyboard => 1,
|
||||
HidFunctionType::MouseRelative => 2,
|
||||
HidFunctionType::MouseAbsolute => 2,
|
||||
HidFunctionType::ConsumerControl => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
HidFunctionType::ConsumerControl => 0, // No boot interface
|
||||
HidFunctionType::Keyboard => 1,
|
||||
HidFunctionType::MouseRelative => 1,
|
||||
HidFunctionType::MouseAbsolute => 0,
|
||||
HidFunctionType::ConsumerControl => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get report length in bytes
|
||||
pub fn report_length(&self, _keyboard_leds: bool) -> u8 {
|
||||
match self {
|
||||
HidFunctionType::Keyboard => 8,
|
||||
@@ -69,7 +55,6 @@ impl HidFunctionType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get report descriptor
|
||||
pub fn report_desc(&self, keyboard_leds: bool) -> &'static [u8] {
|
||||
match self {
|
||||
HidFunctionType::Keyboard => {
|
||||
@@ -84,33 +69,17 @@ impl HidFunctionType {
|
||||
HidFunctionType::ConsumerControl => CONSUMER_CONTROL,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get description
|
||||
pub fn description(&self) -> &'static str {
|
||||
match self {
|
||||
HidFunctionType::Keyboard => "Keyboard",
|
||||
HidFunctionType::MouseRelative => "Relative Mouse",
|
||||
HidFunctionType::MouseAbsolute => "Absolute Mouse",
|
||||
HidFunctionType::ConsumerControl => "Consumer Control",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// Whether keyboard LED/status feedback is enabled.
|
||||
keyboard_leds: bool,
|
||||
}
|
||||
|
||||
impl HidFunction {
|
||||
/// Create a keyboard function
|
||||
pub fn keyboard(instance: u8, keyboard_leds: bool) -> Self {
|
||||
Self {
|
||||
instance,
|
||||
@@ -120,7 +89,6 @@ impl HidFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a relative mouse function
|
||||
pub fn mouse_relative(instance: u8) -> Self {
|
||||
Self {
|
||||
instance,
|
||||
@@ -130,7 +98,6 @@ impl HidFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an absolute mouse function
|
||||
pub fn mouse_absolute(instance: u8) -> Self {
|
||||
Self {
|
||||
instance,
|
||||
@@ -140,7 +107,6 @@ impl HidFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a consumer control function
|
||||
pub fn consumer_control(instance: u8) -> Self {
|
||||
Self {
|
||||
instance,
|
||||
@@ -150,12 +116,10 @@ impl HidFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
@@ -170,20 +134,10 @@ impl GadgetFunction for HidFunction {
|
||||
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(),
|
||||
@@ -197,7 +151,6 @@ impl GadgetFunction for HidFunction {
|
||||
&self.func_type.report_length(self.keyboard_leds).to_string(),
|
||||
)?;
|
||||
|
||||
// Write report descriptor
|
||||
write_bytes(
|
||||
&func_path.join("report_desc"),
|
||||
self.func_type.report_desc(self.keyboard_leds),
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
//! 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};
|
||||
@@ -11,14 +8,13 @@ use super::configfs::{
|
||||
DEFAULT_USB_VENDOR_ID, USB_BCD_USB,
|
||||
};
|
||||
use super::endpoint::{EndpointAllocator, DEFAULT_MAX_ENDPOINTS};
|
||||
use super::function::{FunctionMeta, GadgetFunction};
|
||||
use super::function::GadgetFunction;
|
||||
use super::hid::HidFunction;
|
||||
use super::msd::MsdFunction;
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
const REBIND_DELAY_MS: u64 = 300;
|
||||
|
||||
/// USB Gadget device descriptor configuration
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GadgetDescriptor {
|
||||
pub vendor_id: u16,
|
||||
@@ -42,44 +38,28 @@ impl Default for GadgetDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// Device descriptor
|
||||
descriptor: GadgetDescriptor,
|
||||
/// 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 {
|
||||
Self::with_descriptor(gadget_name, max_endpoints, GadgetDescriptor::default())
|
||||
}
|
||||
|
||||
/// Create a new gadget manager with custom descriptor
|
||||
pub fn with_descriptor(
|
||||
gadget_name: &str,
|
||||
max_endpoints: u8,
|
||||
@@ -96,30 +76,24 @@ impl OtgGadgetManager {
|
||||
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) {
|
||||
@@ -129,8 +103,6 @@ impl OtgGadgetManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add keyboard function
|
||||
/// Returns the expected device path (e.g., /dev/hidg0)
|
||||
pub fn add_keyboard(&mut self, keyboard_leds: bool) -> Result<PathBuf> {
|
||||
let func = HidFunction::keyboard(self.hid_instance, keyboard_leds);
|
||||
let device_path = func.device_path();
|
||||
@@ -139,7 +111,6 @@ impl OtgGadgetManager {
|
||||
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();
|
||||
@@ -148,7 +119,6 @@ impl OtgGadgetManager {
|
||||
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();
|
||||
@@ -157,7 +127,6 @@ impl OtgGadgetManager {
|
||||
Ok(device_path)
|
||||
}
|
||||
|
||||
/// Add consumer control function (multimedia keys)
|
||||
pub fn add_consumer_control(&mut self) -> Result<PathBuf> {
|
||||
let func = HidFunction::consumer_control(self.hid_instance);
|
||||
let device_path = func.device_path();
|
||||
@@ -166,7 +135,6 @@ impl OtgGadgetManager {
|
||||
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();
|
||||
@@ -175,11 +143,9 @@ impl OtgGadgetManager {
|
||||
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 {}",
|
||||
@@ -189,30 +155,22 @@ impl OtgGadgetManager {
|
||||
)));
|
||||
}
|
||||
|
||||
// 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");
|
||||
@@ -222,20 +180,15 @@ impl OtgGadgetManager {
|
||||
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)?;
|
||||
@@ -245,9 +198,7 @@ impl OtgGadgetManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bind gadget to a specific UDC
|
||||
pub fn bind(&mut self, udc: &str) -> Result<()> {
|
||||
// Recreate config symlinks before binding to avoid kernel gadget issues after rebind
|
||||
if let Err(e) = self.recreate_config_links() {
|
||||
warn!("Failed to recreate gadget config links before bind: {}", e);
|
||||
}
|
||||
@@ -260,7 +211,6 @@ impl OtgGadgetManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unbind gadget from UDC
|
||||
pub fn unbind(&mut self) -> Result<()> {
|
||||
if self.is_bound() {
|
||||
write_file(&self.gadget_path.join("UDC"), "")?;
|
||||
@@ -271,7 +221,6 @@ impl OtgGadgetManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cleanup all resources
|
||||
pub fn cleanup(&mut self) -> Result<()> {
|
||||
if !self.gadget_exists() {
|
||||
return Ok(());
|
||||
@@ -279,29 +228,23 @@ impl OtgGadgetManager {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -311,7 +254,6 @@ impl OtgGadgetManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set USB device descriptors
|
||||
fn set_device_descriptors(&self) -> Result<()> {
|
||||
write_file(
|
||||
&self.gadget_path.join("idVendor"),
|
||||
@@ -329,14 +271,13 @@ impl OtgGadgetManager {
|
||||
&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("bDeviceClass"), "0x00")?;
|
||||
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)?;
|
||||
@@ -354,41 +295,23 @@ impl OtgGadgetManager {
|
||||
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
|
||||
}
|
||||
|
||||
/// Recreate config symlinks from functions directory
|
||||
fn recreate_config_links(&self) -> Result<()> {
|
||||
let functions_path = self.gadget_path.join("functions");
|
||||
if !functions_path.exists() || !self.config_path.exists() {
|
||||
@@ -450,15 +373,10 @@ impl Drop for OtgGadgetManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
@@ -467,7 +385,6 @@ pub async fn wait_for_hid_devices(device_paths: &[PathBuf], timeout_ms: u64) ->
|
||||
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);
|
||||
|
||||
@@ -477,7 +394,6 @@ pub async fn wait_for_hid_devices(device_paths: &[PathBuf], timeout_ms: u64) ->
|
||||
|
||||
tokio::time::sleep(sleep_duration).await;
|
||||
|
||||
// Exponential backoff with cap
|
||||
delay_ms = (delay_ms * 2).min(MAX_DELAY_MS);
|
||||
}
|
||||
|
||||
@@ -492,18 +408,16 @@ mod tests {
|
||||
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
|
||||
assert!(!manager.gadget_exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_endpoint_tracking() {
|
||||
let mut manager = OtgGadgetManager::with_config("test", 8);
|
||||
|
||||
// Keyboard uses 1 endpoint
|
||||
let _ = manager.add_keyboard(false);
|
||||
assert_eq!(manager.endpoint_allocator.used(), 1);
|
||||
|
||||
// Mouse uses 1 endpoint each
|
||||
let _ = manager.add_mouse_relative();
|
||||
let _ = manager.add_mouse_absolute();
|
||||
assert_eq!(manager.endpoint_allocator.used(), 3);
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
//! 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.
|
||||
//! USB OTG composite gadget (HID + MSD).
|
||||
|
||||
pub mod configfs;
|
||||
pub mod endpoint;
|
||||
@@ -26,10 +9,6 @@ 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, MOUSE_ABSOLUTE, MOUSE_RELATIVE};
|
||||
pub use service::{HidDevicePaths, OtgDesiredState, OtgService, OtgServiceState};
|
||||
pub use service::{HidDevicePaths, OtgService};
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
//! 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 super::function::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,
|
||||
}
|
||||
|
||||
@@ -36,7 +28,6 @@ impl Default for MsdLunConfig {
|
||||
}
|
||||
|
||||
impl MsdLunConfig {
|
||||
/// Create CD-ROM configuration
|
||||
pub fn cdrom(file: PathBuf) -> Self {
|
||||
Self {
|
||||
file,
|
||||
@@ -47,7 +38,6 @@ impl MsdLunConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create disk configuration
|
||||
pub fn disk(file: PathBuf, read_only: bool) -> Self {
|
||||
Self {
|
||||
file,
|
||||
@@ -59,38 +49,26 @@ impl MsdLunConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -106,17 +84,6 @@ impl MsdFunction {
|
||||
.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);
|
||||
|
||||
@@ -124,7 +91,6 @@ impl MsdFunction {
|
||||
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()
|
||||
@@ -137,28 +103,21 @@ impl MsdFunction {
|
||||
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!(
|
||||
@@ -186,13 +145,11 @@ impl MsdFunction {
|
||||
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;
|
||||
@@ -210,7 +167,6 @@ impl MsdFunction {
|
||||
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");
|
||||
|
||||
@@ -220,7 +176,6 @@ impl MsdFunction {
|
||||
lun,
|
||||
attempt + 1
|
||||
);
|
||||
// Exponential backoff: 50, 100, 200, 400ms
|
||||
std::thread::sleep(std::time::Duration::from_millis(50 << attempt));
|
||||
last_error = Some(e);
|
||||
continue;
|
||||
@@ -231,7 +186,6 @@ impl MsdFunction {
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all retries failed
|
||||
if let Some(e) = last_error {
|
||||
return Err(e);
|
||||
}
|
||||
@@ -242,9 +196,6 @@ impl MsdFunction {
|
||||
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();
|
||||
@@ -254,17 +205,10 @@ impl MsdFunction {
|
||||
.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!(
|
||||
@@ -282,7 +226,6 @@ impl MsdFunction {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to clearing file directly
|
||||
write_file(&lun_path.join("file"), "")?;
|
||||
}
|
||||
info!("LUN {} disconnected", lun);
|
||||
@@ -291,7 +234,6 @@ impl MsdFunction {
|
||||
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");
|
||||
@@ -306,7 +248,6 @@ impl MsdFunction {
|
||||
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()
|
||||
}
|
||||
@@ -318,39 +259,23 @@ impl GadgetFunction for MsdFunction {
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
2
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -382,12 +307,10 @@ impl GadgetFunction for MsdFunction {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
//! HID Report Descriptors
|
||||
|
||||
/// Keyboard HID Report Descriptor (no LED output)
|
||||
/// Report format (8 bytes input):
|
||||
/// [0] Modifier keys (8 bits)
|
||||
/// [1] Reserved
|
||||
/// [2-7] Key codes (6 keys)
|
||||
pub const KEYBOARD: &[u8] = &[
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
@@ -34,13 +27,6 @@ pub const KEYBOARD: &[u8] = &[
|
||||
0xC0, // End Collection
|
||||
];
|
||||
|
||||
/// Keyboard HID Report Descriptor with LED output support.
|
||||
/// Input report format (8 bytes):
|
||||
/// [0] Modifier keys (8 bits)
|
||||
/// [1] Reserved
|
||||
/// [2-7] Key codes (6 keys)
|
||||
/// Output report format (1 byte):
|
||||
/// [0] Num Lock / Caps Lock / Scroll Lock / Compose / Kana
|
||||
pub const KEYBOARD_WITH_LED: &[u8] = &[
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
@@ -81,12 +67,6 @@ pub const KEYBOARD_WITH_LED: &[u8] = &[
|
||||
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)
|
||||
@@ -126,12 +106,6 @@ pub const MOUSE_RELATIVE: &[u8] = &[
|
||||
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)
|
||||
@@ -177,10 +151,6 @@ pub const MOUSE_ABSOLUTE: &[u8] = &[
|
||||
0xC0, // End Collection
|
||||
];
|
||||
|
||||
/// Consumer Control HID Report Descriptor (2 bytes report)
|
||||
/// Report format:
|
||||
/// [0-1] Consumer Control Usage (16-bit little-endian)
|
||||
/// Supports: Play/Pause, Stop, Next/Prev Track, Mute, Volume Up/Down, etc.
|
||||
pub const CONSUMER_CONTROL: &[u8] = &[
|
||||
0x05, 0x0C, // Usage Page (Consumer)
|
||||
0x09, 0x01, // Usage (Consumer Control)
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
//! OTG Service - unified gadget lifecycle management
|
||||
//!
|
||||
//! This module provides centralized management for USB OTG gadget functions.
|
||||
//! It is the single owner of the USB gadget desired state and reconciles
|
||||
//! ConfigFS to match that state.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{debug, info, warn};
|
||||
@@ -13,7 +7,6 @@ use super::msd::MsdFunction;
|
||||
use crate::config::{HidBackend, HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// HID device paths
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HidDevicePaths {
|
||||
pub keyboard: Option<PathBuf>,
|
||||
@@ -26,26 +19,20 @@ pub struct HidDevicePaths {
|
||||
|
||||
impl HidDevicePaths {
|
||||
pub fn existing_paths(&self) -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
if let Some(ref p) = self.keyboard {
|
||||
paths.push(p.clone());
|
||||
}
|
||||
if let Some(ref p) = self.mouse_relative {
|
||||
paths.push(p.clone());
|
||||
}
|
||||
if let Some(ref p) = self.mouse_absolute {
|
||||
paths.push(p.clone());
|
||||
}
|
||||
if let Some(ref p) = self.consumer {
|
||||
paths.push(p.clone());
|
||||
}
|
||||
paths
|
||||
[
|
||||
&self.keyboard,
|
||||
&self.mouse_relative,
|
||||
&self.mouse_absolute,
|
||||
&self.consumer,
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|p| p.as_ref().cloned())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Desired OTG gadget state derived from configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OtgDesiredState {
|
||||
pub(crate) struct OtgDesiredState {
|
||||
pub udc: Option<String>,
|
||||
pub descriptor: GadgetDescriptor,
|
||||
pub hid_functions: Option<OtgHidFunctions>,
|
||||
@@ -68,7 +55,7 @@ impl Default for OtgDesiredState {
|
||||
}
|
||||
|
||||
impl OtgDesiredState {
|
||||
pub fn from_config(hid: &HidConfig, msd: &MsdConfig) -> Result<Self> {
|
||||
pub(crate) fn from_config(hid: &HidConfig, msd: &MsdConfig) -> Result<Self> {
|
||||
let hid_functions = if hid.backend == HidBackend::Otg {
|
||||
let functions = hid.constrained_otg_functions();
|
||||
Some(functions)
|
||||
@@ -96,45 +83,28 @@ impl OtgDesiredState {
|
||||
}
|
||||
}
|
||||
|
||||
/// OTG Service state
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct OtgServiceState {
|
||||
/// Whether the gadget is created and bound
|
||||
struct OtgServiceState {
|
||||
pub gadget_active: bool,
|
||||
/// Whether HID functions are enabled
|
||||
pub hid_enabled: bool,
|
||||
/// Whether MSD function is enabled
|
||||
pub msd_enabled: bool,
|
||||
/// Bound UDC name
|
||||
pub configured_udc: Option<String>,
|
||||
/// HID device paths (set after gadget setup)
|
||||
pub hid_paths: Option<HidDevicePaths>,
|
||||
/// HID function selection (set after gadget setup)
|
||||
pub hid_functions: Option<OtgHidFunctions>,
|
||||
/// Whether keyboard LED/status feedback is enabled.
|
||||
pub keyboard_leds_enabled: bool,
|
||||
/// Applied endpoint budget.
|
||||
pub max_endpoints: u8,
|
||||
/// Applied descriptor configuration
|
||||
pub descriptor: Option<GadgetDescriptor>,
|
||||
/// Error message if setup failed
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// OTG Service - unified gadget lifecycle management
|
||||
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>>,
|
||||
/// Desired OTG state
|
||||
desired: RwLock<OtgDesiredState>,
|
||||
}
|
||||
|
||||
impl OtgService {
|
||||
/// Create a new OTG service
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
manager: Mutex::new(None),
|
||||
@@ -144,55 +114,29 @@ impl OtgService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Apply desired OTG state derived from the current application config.
|
||||
pub async fn apply_config(&self, hid: &HidConfig, msd: &MsdConfig) -> Result<()> {
|
||||
let desired = OtgDesiredState::from_config(hid, msd)?;
|
||||
self.apply_desired_state(desired).await
|
||||
}
|
||||
|
||||
/// Apply a fully materialized desired OTG state.
|
||||
pub async fn apply_desired_state(&self, desired: OtgDesiredState) -> Result<()> {
|
||||
pub(crate) async fn apply_desired_state(&self, desired: OtgDesiredState) -> Result<()> {
|
||||
{
|
||||
let mut current = self.desired.write().await;
|
||||
*current = desired;
|
||||
@@ -392,7 +336,6 @@ impl OtgService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown the OTG service and cleanup all resources
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
info!("Shutting down OTG service");
|
||||
|
||||
@@ -425,12 +368,6 @@ impl Default for OtgService {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OtgService {
|
||||
fn drop(&mut self) {
|
||||
debug!("OtgService dropping");
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OtgDescriptorConfig> for GadgetDescriptor {
|
||||
fn from(config: &OtgDescriptorConfig) -> Self {
|
||||
Self {
|
||||
@@ -452,17 +389,8 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_service_creation() {
|
||||
fn service_new_and_availability_probe() {
|
||||
let _service = OtgService::new();
|
||||
let _ = OtgService::is_available();
|
||||
}
|
||||
|
||||
#[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