feat: 初步支持 USB 网卡桥接;删除 OTG 端点预算管理

This commit is contained in:
mofeng-git
2026-07-16 19:19:24 +08:00
parent 63c2bb4ca2
commit 213359b6c8
35 changed files with 2367 additions and 727 deletions

View File

@@ -81,29 +81,6 @@ pub enum OtgHidProfile {
Custom,
}
#[typeshare]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum OtgEndpointBudget {
#[default]
Auto,
Five,
Six,
Unlimited,
}
impl OtgEndpointBudget {
pub fn endpoint_limit_raw(&self) -> Option<u8> {
match self {
Self::Five => Some(5),
Self::Six => Some(6),
Self::Unlimited => None,
Self::Auto => None,
}
}
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
@@ -154,26 +131,6 @@ impl OtgHidFunctions {
pub fn is_empty(&self) -> bool {
!self.keyboard && !self.mouse_relative && !self.mouse_absolute && !self.consumer
}
pub fn endpoint_cost(&self, keyboard_leds: bool) -> u8 {
let mut endpoints = 0;
if self.keyboard {
endpoints += 1;
if keyboard_leds {
endpoints += 1;
}
}
if self.mouse_relative {
endpoints += 1;
}
if self.mouse_absolute {
endpoints += 1;
}
if self.consumer {
endpoints += 1;
}
endpoints
}
}
impl Default for OtgHidFunctions {
@@ -216,8 +173,6 @@ pub struct HidConfig {
#[serde(default)]
pub otg_profile: OtgHidProfile,
#[serde(default)]
pub otg_endpoint_budget: OtgEndpointBudget,
#[serde(default)]
pub otg_functions: OtgHidFunctions,
#[serde(default)]
pub otg_keyboard_leds: bool,
@@ -237,7 +192,6 @@ impl Default for HidConfig {
otg_udc: None,
otg_descriptor: OtgDescriptorConfig::default(),
otg_profile: OtgHidProfile::default(),
otg_endpoint_budget: OtgEndpointBudget::default(),
otg_functions: OtgHidFunctions::default(),
otg_keyboard_leds: false,
ch9329_port: "/dev/ttyUSB0".to_string(),
@@ -262,16 +216,7 @@ impl HidConfig {
self.effective_otg_functions()
}
pub fn effective_otg_required_endpoints(&self, msd_enabled: bool) -> u8 {
let functions = self.effective_otg_functions();
let mut endpoints = functions.endpoint_cost(self.effective_otg_keyboard_leds());
if msd_enabled {
endpoints += 2;
}
endpoints
}
pub fn validate_otg_endpoint_budget(&self, msd_enabled: bool) -> crate::error::Result<()> {
pub fn validate_otg_functions(&self) -> crate::error::Result<()> {
if self.backend != HidBackend::Otg {
return Ok(());
}
@@ -283,17 +228,6 @@ impl HidConfig {
));
}
let resolved_limit = self.resolved_otg_endpoint_limit();
let required = self.effective_otg_required_endpoints(msd_enabled);
if let Some(limit) = resolved_limit {
if required > limit {
return Err(crate::error::AppError::BadRequest(format!(
"OTG selection requires {} endpoints, but the configured limit is {}",
required, limit
)));
}
}
Ok(())
}
@@ -317,30 +251,4 @@ impl HidConfig {
}
})
}
#[inline]
pub fn resolved_otg_endpoint_limit(&self) -> Option<u8> {
if self.backend != HidBackend::Otg {
return None;
}
match self.otg_endpoint_budget {
OtgEndpointBudget::Five => Some(5),
OtgEndpointBudget::Six => Some(6),
OtgEndpointBudget::Unlimited => None,
OtgEndpointBudget::Auto => {
#[cfg(unix)]
let udc = self.resolved_otg_udc().unwrap_or_default();
#[cfg(unix)]
if crate::otg::configfs::is_low_endpoint_udc(&udc) {
Some(5)
} else {
Some(6)
}
#[cfg(not(unix))]
{
Some(6)
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ mod atx;
mod common;
mod computer_use;
mod hid;
mod otg_network;
mod stream;
mod web;
@@ -15,6 +16,7 @@ pub use atx::*;
pub use common::*;
pub use computer_use::*;
pub use hid::*;
pub use otg_network::*;
pub use stream::*;
pub use web::*;
@@ -27,6 +29,7 @@ pub struct AppConfig {
pub auth: AuthConfig,
pub video: VideoConfig,
pub hid: HidConfig,
pub otg_network: OtgNetworkConfig,
pub msd: MsdConfig,
pub atx: AtxConfig,
pub audio: AudioConfig,
@@ -44,6 +47,7 @@ impl AppConfig {
pub fn enforce_invariants(&mut self) {
if self.hid.backend != HidBackend::Otg {
self.msd.enabled = false;
self.otg_network.enabled = false;
}
self.atx.normalize();
}

View File

@@ -0,0 +1,85 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
#[typeshare]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum OtgNetworkDriverMode {
#[default]
Ncm,
Ecm,
Rndis,
}
impl OtgNetworkDriverMode {
pub fn function_name(self) -> &'static str {
match self {
Self::Ncm => "ncm",
Self::Ecm => "ecm",
Self::Rndis => "rndis",
}
}
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(default)]
pub struct OtgNetworkConfig {
pub enabled: bool,
pub driver_mode: OtgNetworkDriverMode,
/// Empty means select the connected NetworkManager Ethernet interface.
pub bridge_interface: String,
/// Empty values are resolved from the machine identity at runtime.
pub host_mac: String,
pub device_mac: String,
}
impl OtgNetworkConfig {
pub fn validate(&self) -> crate::error::Result<()> {
for (name, value) in [
("host_mac", self.host_mac.as_str()),
("device_mac", self.device_mac.as_str()),
] {
if !value.is_empty() && !is_valid_unicast_mac(value) {
return Err(crate::error::AppError::BadRequest(format!(
"OTG network {name} must be a locally administered unicast MAC address"
)));
}
}
if !self.host_mac.is_empty()
&& !self.device_mac.is_empty()
&& self.host_mac.eq_ignore_ascii_case(&self.device_mac)
{
return Err(crate::error::AppError::BadRequest(
"OTG network host_mac and device_mac must be different".to_string(),
));
}
if self.bridge_interface.contains('/') || self.bridge_interface.contains('\0') {
return Err(crate::error::AppError::BadRequest(
"Invalid OTG network bridge interface".to_string(),
));
}
Ok(())
}
}
fn is_valid_unicast_mac(value: &str) -> bool {
let bytes = value
.split(':')
.map(|part| u8::from_str_radix(part, 16))
.collect::<Result<Vec<_>, _>>();
matches!(bytes, Ok(ref bytes) if bytes.len() == 6 && bytes[0] & 0x03 == 0x02)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validates_local_unicast_mac_addresses() {
assert!(is_valid_unicast_mac("02:00:00:00:10:01"));
assert!(!is_valid_unicast_mac("01:00:00:00:10:01"));
assert!(!is_valid_unicast_mac("00:00:00:00:10:01"));
assert!(!is_valid_unicast_mac("bad"));
}
}