feat: 新增 Linux 绝对鼠标兼容模式 #266;新增 CH9329 描述符设置

This commit is contained in:
mofeng-git
2026-06-14 20:59:23 +08:00
parent da61644dbc
commit 5c98aea7e3
21 changed files with 1403 additions and 105 deletions

View File

@@ -33,6 +33,7 @@ fn hid_backend_type(config: &HidConfig) -> crate::hid::HidBackendType {
HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
port: config.ch9329_port.clone(),
baud_rate: config.ch9329_baudrate,
hybrid_mouse: config.ch9329_hybrid_mouse,
},
HidBackend::None => crate::hid::HidBackendType::None,
}
@@ -179,10 +180,12 @@ pub async fn apply_hid_config(
old_config.effective_otg_keyboard_leds() != new_config.effective_otg_keyboard_leds();
let endpoint_budget_changed =
old_config.resolved_otg_endpoint_limit() != new_config.resolved_otg_endpoint_limit();
let ch9329_runtime_changed = old_config.ch9329_hybrid_mouse != new_config.ch9329_hybrid_mouse;
if old_config.backend == new_config.backend
&& old_config.ch9329_port == new_config.ch9329_port
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
&& !ch9329_runtime_changed
&& old_config.otg_udc == new_config.otg_udc
&& !descriptor_changed
&& !hid_functions_changed

View File

@@ -1,7 +1,7 @@
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::HidConfig;
use crate::config::{HidBackend, HidConfig};
use crate::error::Result;
use crate::state::AppState;
@@ -21,10 +21,20 @@ pub async fn update_hid_config(
let _apply_guard = try_apply_lock(&state.config_apply_locks.otg, "otg")?;
let old_hid_config = state.config.get().hid.clone();
let mut staged_hid_config = old_hid_config.clone();
req.apply_to(&mut staged_hid_config);
let descriptor_update = req
.ch9329_descriptor
.as_ref()
.map(|_| staged_hid_config.ch9329_descriptor.clone());
if descriptor_update.is_some() {
staged_hid_config.ch9329_descriptor = old_hid_config.ch9329_descriptor.clone();
}
state
.config
.update(|config| {
req.apply_to(&mut config.hid);
config.hid = staged_hid_config.clone();
config.enforce_invariants();
})
.await?;
@@ -39,5 +49,21 @@ pub async fn update_hid_config(
)
.await?;
if let Some(descriptor) = descriptor_update {
if new_hid_config.backend != HidBackend::Ch9329 {
return Ok(Json(new_hid_config));
}
let actual = state.hid.apply_ch9329_descriptor(&descriptor).await?;
state
.config
.update(|config| {
config.hid.ch9329_descriptor = actual.descriptor.clone();
config.enforce_invariants();
})
.await?;
return Ok(Json(state.config.get().hid.clone()));
}
Ok(Json(new_hid_config))
}

View File

@@ -292,12 +292,63 @@ impl OtgHidFunctionsUpdate {
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct Ch9329DescriptorConfigUpdate {
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
pub manufacturer: Option<String>,
pub product: Option<String>,
pub serial_number: Option<String>,
}
impl Ch9329DescriptorConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
Self::validate_optional_string("Manufacturer", self.manufacturer.as_deref())?;
Self::validate_optional_string("Product", self.product.as_deref())?;
Self::validate_optional_string("Serial number", self.serial_number.as_deref())?;
Ok(())
}
fn validate_optional_string(label: &str, value: Option<&str>) -> crate::error::Result<()> {
if let Some(value) = value {
if value.as_bytes().len() > 23 {
return Err(AppError::BadRequest(format!(
"{} string too long (max 23 bytes for CH9329)",
label
)));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut Ch9329DescriptorConfig) {
if let Some(v) = self.vendor_id {
config.vendor_id = v;
}
if let Some(v) = self.product_id {
config.product_id = v;
}
if let Some(ref v) = self.manufacturer {
config.manufacturer = v.clone();
}
if let Some(ref v) = self.product {
config.product = v.clone();
}
if let Some(ref v) = self.serial_number {
config.serial_number = if v.is_empty() { None } else { Some(v.clone()) };
}
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct HidConfigUpdate {
pub backend: Option<HidBackend>,
pub ch9329_port: Option<String>,
pub ch9329_baudrate: Option<u32>,
pub ch9329_hybrid_mouse: Option<bool>,
pub ch9329_descriptor: Option<Ch9329DescriptorConfigUpdate>,
pub otg_udc: Option<String>,
pub otg_descriptor: Option<OtgDescriptorConfigUpdate>,
pub otg_profile: Option<OtgHidProfile>,
@@ -320,6 +371,9 @@ impl HidConfigUpdate {
if let Some(ref desc) = self.otg_descriptor {
desc.validate()?;
}
if let Some(ref desc) = self.ch9329_descriptor {
desc.validate()?;
}
Ok(())
}
@@ -333,6 +387,12 @@ impl HidConfigUpdate {
if let Some(baudrate) = self.ch9329_baudrate {
config.ch9329_baudrate = baudrate;
}
if let Some(enabled) = self.ch9329_hybrid_mouse {
config.ch9329_hybrid_mouse = enabled;
}
if let Some(ref desc) = self.ch9329_descriptor {
desc.apply_to(&mut config.ch9329_descriptor);
}
if let Some(ref udc) = self.otg_udc {
config.otg_udc = Some(udc.clone());
}

View File

@@ -1,4 +1,11 @@
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 {
@@ -51,3 +58,57 @@ pub async fn hid_reset(State(state): State<Arc<AppState>>) -> Result<Json<LoginR
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,
}
}

View File

@@ -170,6 +170,7 @@ pub async fn setup_init(
crate::config::HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
port: new_config.hid.ch9329_port.clone(),
baud_rate: new_config.hid.ch9329_baudrate,
hybrid_mouse: new_config.hid.ch9329_hybrid_mouse,
},
crate::config::HidBackend::None => crate::hid::HidBackendType::None,
};

View File

@@ -73,6 +73,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/webrtc/close", post(handlers::webrtc_close_session))
// HID endpoints
.route("/hid/status", get(handlers::hid_status))
.route(
"/hid/ch9329/descriptor",
get(handlers::hid_ch9329_descriptor),
)
.route("/hid/reset", post(handlers::hid_reset))
// WebSocket HID endpoint (for MJPEG mode)
.route("/ws/hid", any(ws_hid_handler))