Files
One-KVM/src/web/handlers/hid_api.rs
mofeng-git f303a8cb2f feat(hid): 支持 CH9329 + OTG 组合模式
CH9329 负责键鼠,OTG 提供虚拟媒体、USB 网络和音频;分别配置设备、报告运行状态,并在 OTG 配置变更和回滚时保留未变化的 CH9329 实例。

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

验证:独立功能版本的 591 项 Rust 库测试、前端生产构建、3 项引导逻辑测试及 2 组浏览器回归通过。未进行目标 KVM 硬件验证。
2026-10-26 14:33:38 +08:00

170 lines
5.2 KiB
Rust

use super::*;
use crate::error::AppError;
#[derive(Deserialize)]
pub struct Ch9329DescriptorQuery {
pub port: Option<String>,
pub baud_rate: Option<u32>,
}
#[derive(Serialize)]
pub struct HidStatus {
pub available: bool,
pub backend: String,
pub configured_backend: String,
#[cfg(unix)]
pub otg: crate::otg::service::OtgRuntimeStatus,
pub initialized: bool,
pub online: bool,
pub supports_absolute_mouse: bool,
pub keyboard_leds_enabled: bool,
pub led_state: crate::hid::LedState,
pub screen_resolution: Option<(u32, u32)>,
pub device: Option<String>,
pub error: Option<String>,
pub error_code: Option<String>,
}
/// OTG self-check status for troubleshooting USB gadget issues
#[cfg(unix)]
pub async fn hid_otg_self_check(
State(state): State<Arc<AppState>>,
) -> Json<crate::otg::self_check::OtgSelfCheckResponse> {
let config = state.config.get();
Json(crate::otg::self_check::run(config.as_ref()))
}
/// Get HID status
pub async fn hid_status(State(state): State<Arc<AppState>>) -> Json<HidStatus> {
let hid = state.hid.snapshot().await;
let config = state.config.get();
Json(HidStatus {
available: hid.available,
backend: hid.backend,
configured_backend: config.hid.backend.as_str().to_string(),
#[cfg(unix)]
otg: state
.otg_service
.runtime_status(
config.hid.backend.uses_otg_hid()
|| config.msd.enabled
|| config.otg_network.enabled
|| config.uac.enabled,
)
.await,
initialized: hid.initialized,
online: hid.online,
supports_absolute_mouse: hid.supports_absolute_mouse,
keyboard_leds_enabled: hid.keyboard_leds_enabled,
led_state: hid.led_state,
screen_resolution: hid.screen_resolution,
device: hid.device,
error: hid.error,
error_code: hid.error_code,
})
}
/// Reset HID state
pub async fn hid_reset(State(state): State<Arc<AppState>>) -> Result<Json<LoginResponse>> {
state.hid.reset().await?;
Ok(Json(LoginResponse {
success: true,
message: Some("HID state reset".to_string()),
}))
}
/// Read the CH9329 USB descriptor, falling back to the saved config when SET is not low.
pub async fn hid_ch9329_descriptor(
State(state): State<Arc<AppState>>,
Query(query): Query<Ch9329DescriptorQuery>,
) -> Result<Json<crate::config::Ch9329DescriptorState>> {
let config = state.config.get();
let hid = &config.hid;
let port = query.port.as_deref().filter(|port| !port.trim().is_empty());
let baud_rate = query.baud_rate;
let descriptor_result = match (port, baud_rate) {
(Some(port), Some(baud_rate))
if port != hid.ch9329_port || baud_rate != hid.ch9329_baudrate =>
{
crate::hid::ch9329::Ch9329Backend::read_device_descriptor(port, baud_rate)
}
_ => state.hid.read_ch9329_descriptor().await,
};
let descriptor = match descriptor_result {
Ok(descriptor) => descriptor,
Err(err) if is_ch9329_config_mode_unavailable(&err) => cached_ch9329_descriptor(hid),
Err(err) => return Err(err),
};
Ok(Json(descriptor))
}
fn is_ch9329_config_mode_unavailable(err: &AppError) -> bool {
matches!(
err,
AppError::HidError {
backend,
error_code,
..
} if backend == "ch9329" && error_code == "invalid_response"
)
}
fn cached_ch9329_descriptor(
hid: &crate::config::HidConfig,
) -> crate::config::Ch9329DescriptorState {
let descriptor = hid.ch9329_descriptor.clone();
crate::config::Ch9329DescriptorState {
manufacturer_enabled: !descriptor.manufacturer.is_empty(),
product_enabled: !descriptor.product.is_empty(),
serial_enabled: descriptor
.serial_number
.as_ref()
.is_some_and(|value| !value.is_empty()),
config_mode_available: false,
descriptor,
}
}
#[derive(Deserialize)]
pub struct BluetoothAction {
pub action: String,
pub seconds: Option<u32>,
}
pub async fn hid_bluetooth_status(
State(state): State<Arc<AppState>>,
) -> Result<Json<serde_json::Value>> {
Ok(Json(state.hid.bluetooth_status().await?))
}
pub async fn hid_bluetooth_action(
State(state): State<Arc<AppState>>,
Json(req): Json<BluetoothAction>,
) -> Result<Json<serde_json::Value>> {
let _guard = crate::runtime::try_apply_lock(&state.config_apply_locks.otg, "bluetooth")?;
state
.hid
.bluetooth_action(&req.action, req.seconds.unwrap_or(120))
.await?;
Ok(Json(serde_json::json!({"success": true})))
}
pub async fn hid_bluetooth_adapters() -> Result<Json<serde_json::Value>> {
#[cfg(target_os = "linux")]
{
Ok(Json(
serde_json::to_value(
one_kvm_bluetooth_hid::adapters()
.await
.map_err(AppError::ServiceUnavailable)?,
)
.map_err(|e| AppError::Internal(e.to_string()))?,
))
}
#[cfg(not(target_os = "linux"))]
{
Err(AppError::BadRequest("Bluetooth HID requires Linux".into()))
}
}