feat(hid): 支持 CH9329 + OTG 组合模式

CH9329 负责键鼠,OTG 提供虚拟媒体、USB 网络和音频;分别配置设备、报告运行状态,并在 OTG 配置变更和回滚时保留未变化的 CH9329 实例。

补齐双设备校验、串口冲突检查、初始化引导和功能入口,修复组合模式下鼠标兼容选项缺失。

验证:独立功能版本的 591 项 Rust 库测试、前端生产构建、3 项引导逻辑测试及 2 组浏览器回归通过。未进行目标 KVM 硬件验证。
This commit is contained in:
mofeng-git
2026-10-26 14:33:38 +08:00
parent b4161f3534
commit f303a8cb2f
30 changed files with 718 additions and 225 deletions

View File

@@ -149,8 +149,11 @@ fn detect_libcomposite_available(gadget_root: &std::path::Path) -> bool {
/// OTG self-check status for troubleshooting USB gadget issues
pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
let hid_backend_is_otg = matches!(config.hid.backend, crate::config::HidBackend::Otg);
let gadget_expected = hid_backend_is_otg || config.msd.enabled || config.otg_network.enabled;
let hid_backend_is_otg = config.hid.backend.uses_otg_hid();
let gadget_expected = hid_backend_is_otg
|| config.msd.enabled
|| config.otg_network.enabled
|| config.uac.enabled;
let mut checks = Vec::new();
let build_response = |checks: Vec<OtgSelfCheckItem>,
@@ -173,7 +176,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
overall_ok: error_count == 0,
error_count,
warning_count,
hid_backend: format!("{:?}", config.hid.backend).to_lowercase(),
hid_backend: config.hid.backend.as_str().to_string(),
selected_udc,
bound_udc,
udc_state,
@@ -415,7 +418,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
OtgSelfCheckLevel::Warn
},
"Check one-kvm gadget presence",
Some("Enable OTG HID, MSD, or USB Ethernet to create the one-kvm gadget"),
Some("Enable OTG HID, MSD, USB Ethernet, or USB audio to create the one-kvm gadget"),
Some(one_kvm_path.display().to_string()),
);
}
@@ -459,7 +462,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
false,
OtgSelfCheckLevel::Warn,
"Check one-kvm UDC binding",
Some("Ensure HID/MSD is enabled and initialized successfully"),
Some("Ensure an OTG function is enabled and initialized successfully"),
Some(one_kvm_udc_path.display().to_string()),
);
} else {
@@ -494,18 +497,19 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
})
.cloned()
.collect::<Vec<_>>();
if hid_functions.is_empty() {
let hid_functions_ok = hid_backend_is_otg != hid_functions.is_empty();
if !hid_functions_ok {
push_otg_check(
&mut checks,
"hid_functions_present",
false,
if hid_backend_is_otg {
OtgSelfCheckLevel::Error
OtgSelfCheckLevel::Error,
"Check OTG HID function selection",
Some(if hid_backend_is_otg {
"Check OTG HID config and enable at least one HID function"
} else {
OtgSelfCheckLevel::Warn
},
"Check HID function creation",
Some("Check OTG HID config and enable at least one HID function"),
"OTG HID must be disabled when input is handled by another backend"
}),
Some(functions_path.display().to_string()),
);
} else {
@@ -514,7 +518,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
"hid_functions_present",
true,
OtgSelfCheckLevel::Info,
"Check HID function creation",
"Check OTG HID function selection",
None::<String>,
Some(functions_path.display().to_string()),
);
@@ -569,9 +573,13 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
.map(|name| name.to_string_lossy().to_string())
})
.is_some_and(|name| {
name == "okvm-br0" || name == config.otg_network.bridge_interface
name == "okvm-br0"
|| name == config.otg_network.bridge_interface
|| (config.otg_network.bridge_interface.is_empty()
&& std::path::Path::new("/sys/class/net").join(name).join("bridge").is_dir())
&& std::path::Path::new("/sys/class/net")
.join(name)
.join("bridge")
.is_dir())
});
push_otg_check(
&mut checks,
@@ -843,7 +851,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
overall_ok: error_count == 0,
error_count,
warning_count,
hid_backend: format!("{:?}", config.hid.backend).to_lowercase(),
hid_backend: config.hid.backend.as_str().to_string(),
selected_udc,
bound_udc,
udc_state,

View File

@@ -8,8 +8,7 @@ use super::bridge::NetworkBridgeRuntime;
use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager};
use super::msd::{MsdFunction, MsdInquiryStrings, MsdLunConfig};
use crate::config::{
HidBackend, HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions, OtgNetworkConfig,
UacConfig,
HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions, OtgNetworkConfig, UacConfig,
};
use crate::error::{AppError, Result};
@@ -40,6 +39,17 @@ pub struct OtgNetworkStatus {
pub error: Option<String>,
}
#[typeshare]
#[derive(Debug, Clone, serde::Serialize)]
pub struct OtgRuntimeStatus {
pub enabled: bool,
pub health: OtgRuntimeHealth,
pub gadget_active: bool,
pub udc: Option<String>,
pub udc_state: Option<String>,
pub error: Option<String>,
}
impl HidDevicePaths {
pub fn existing_paths(&self) -> Vec<PathBuf> {
[
@@ -93,7 +103,7 @@ impl OtgDesiredState {
network.validate()?;
uac.validate()?;
msd.validate()?;
let hid_functions = if hid.backend == HidBackend::Otg {
let hid_functions = if hid.backend.uses_otg_hid() {
let functions = hid.constrained_otg_functions();
Some(functions)
} else {
@@ -107,7 +117,13 @@ impl OtgDesiredState {
.as_ref()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.or_else(OtgGadgetManager::find_udc)
.or_else(|| {
if hid.backend.uses_ch9329() {
None
} else {
OtgGadgetManager::find_udc()
}
})
} else {
None
};
@@ -249,6 +265,25 @@ impl OtgService {
}
}
pub async fn runtime_status(&self, enabled: bool) -> OtgRuntimeStatus {
let state = self.state.read().await;
let udc_state = state.configured_udc.as_ref().and_then(|udc| {
// Only UDC names, never a path supplied through a configuration file.
if udc.contains('/') || udc == "." || udc == ".." {
return None;
}
crate::utils::read_trimmed(&PathBuf::from(format!("/sys/class/udc/{udc}/state")))
});
OtgRuntimeStatus {
enabled,
health: state.health,
gadget_active: state.gadget_active,
udc: state.configured_udc.clone(),
udc_state,
error: state.error.clone(),
}
}
pub async fn apply_config(
&self,
hid: &HidConfig,
@@ -687,6 +722,38 @@ impl From<&OtgDescriptorConfig> for GadgetDescriptor {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::HidBackend;
#[test]
fn composite_gadget_supports_every_advanced_function_combination_without_hid() {
let hid = HidConfig {
backend: HidBackend::Ch9329Otg,
otg_udc: Some("test.udc".into()),
otg_keyboard_leds: true,
..Default::default()
};
for mask in 0..8 {
let msd = MsdConfig {
enabled: mask & 1 != 0,
..Default::default()
};
let network = OtgNetworkConfig {
enabled: mask & 2 != 0,
..Default::default()
};
let uac = UacConfig {
enabled: mask & 4 != 0,
..Default::default()
};
let desired = OtgDesiredState::from_config(&hid, &msd, &network, &uac).unwrap();
assert!(!desired.hid_enabled());
assert!(!desired.keyboard_leds);
assert_eq!(desired.msd_enabled, msd.enabled);
assert_eq!(desired.network_enabled(), network.enabled);
assert_eq!(desired.uac.enabled, uac.enabled);
assert_eq!(desired.udc.is_some(), mask != 0);
}
}
#[test]
fn service_new_and_availability_probe() {