feat: 初步增加 Windows 支持

This commit is contained in:
mofeng-git
2026-05-18 22:43:28 +08:00
parent 0b9d94f53f
commit 935fa823f2
163 changed files with 11419 additions and 7581 deletions

View File

@@ -0,0 +1,89 @@
//! Runtime platform mode and feature capability reporting.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformMode {
Linux,
Windows,
}
impl PlatformMode {
pub const fn current() -> Self {
if cfg!(windows) {
Self::Windows
} else {
Self::Linux
}
}
pub const fn label(self) -> &'static str {
match self {
Self::Linux => "Linux",
Self::Windows => "Windows",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureCapability {
pub available: bool,
pub backends: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_backend: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl FeatureCapability {
pub fn available(backends: impl IntoIterator<Item = impl Into<String>>) -> Self {
let backends = backends.into_iter().map(Into::into).collect();
Self {
available: true,
backends,
selected_backend: None,
reason: None,
}
}
pub fn unsupported(reason: impl Into<String>) -> Self {
Self {
available: false,
backends: Vec::new(),
selected_backend: None,
reason: Some(reason.into()),
}
}
pub fn with_selected_backend(mut self, backend: Option<String>) -> Self {
self.selected_backend = backend;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformCapabilities {
pub mode: PlatformMode,
pub mode_label: &'static str,
pub video_capture: FeatureCapability,
pub encoder: FeatureCapability,
pub hid: FeatureCapability,
pub atx: FeatureCapability,
pub msd: FeatureCapability,
pub otg: FeatureCapability,
pub audio: FeatureCapability,
pub rustdesk: FeatureCapability,
pub diagnostics: FeatureCapability,
pub extensions: FeatureCapability,
pub service_installation: FeatureCapability,
}
impl PlatformCapabilities {
pub fn current() -> Self {
match PlatformMode::current() {
PlatformMode::Linux => crate::platform::linux::capabilities(),
PlatformMode::Windows => crate::platform::windows::capabilities(),
}
}
}

62
src/platform/defaults.rs Normal file
View File

@@ -0,0 +1,62 @@
use crate::config::{AppConfig, AtxDriverType, HidBackend};
pub fn apply(config: &mut AppConfig) {
if cfg!(windows) {
apply_windows(config);
}
}
fn apply_windows(config: &mut AppConfig) {
config.msd.enabled = false;
config.hid.otg_udc = None;
if config.hid.backend == HidBackend::Otg {
config.hid.backend = HidBackend::None;
}
if config.hid.ch9329_port == "/dev/ttyUSB0" {
config.hid.ch9329_port = "COM3".to_string();
}
if !config.initialized {
config.audio.enabled = false;
config.audio.device.clear();
}
if matches!(
config.atx.power.driver,
AtxDriverType::Gpio | AtxDriverType::UsbRelay
) {
config.atx.power.driver = AtxDriverType::None;
}
if matches!(
config.atx.reset.driver,
AtxDriverType::Gpio | AtxDriverType::UsbRelay
) {
config.atx.reset.driver = AtxDriverType::None;
}
if !config.initialized
&& config.atx.power.driver == AtxDriverType::None
&& config.atx.power.device.is_empty()
{
config.atx.power.driver = AtxDriverType::Serial;
config.atx.power.device = "COM4".to_string();
config.atx.power.pin = 1;
config.atx.power.baud_rate = 9600;
}
if !config.initialized
&& config.atx.reset.driver == AtxDriverType::None
&& config.atx.reset.device.is_empty()
{
config.atx.reset.driver = AtxDriverType::Serial;
config.atx.reset.device = "COM4".to_string();
config.atx.reset.pin = 2;
config.atx.reset.baud_rate = 9600;
}
config
.video
.device
.get_or_insert_with(|| "auto".to_string());
config
.video
.format
.get_or_insert_with(|| "MJPEG".to_string());
}

23
src/platform/linux.rs Normal file
View File

@@ -0,0 +1,23 @@
//! Linux platform capabilities.
use super::{FeatureCapability, PlatformCapabilities, PlatformMode};
pub fn capabilities() -> PlatformCapabilities {
PlatformCapabilities {
mode: PlatformMode::Linux,
mode_label: PlatformMode::Linux.label(),
video_capture: FeatureCapability::available(["v4l2"]),
encoder: FeatureCapability::available([
"software", "vaapi", "nvenc", "qsv", "amf", "rkmpp", "v4l2m2m",
]),
hid: FeatureCapability::available(["otg", "ch9329", "none"]),
atx: FeatureCapability::available(["gpio", "usb_relay", "serial", "wol", "none"]),
msd: FeatureCapability::available(["configfs"]),
otg: FeatureCapability::available(["configfs"]),
audio: FeatureCapability::available(["alsa"]),
rustdesk: FeatureCapability::available(["builtin"]),
diagnostics: FeatureCapability::available(["linux"]),
extensions: FeatureCapability::available(["linux"]),
service_installation: FeatureCapability::available(["systemd"]),
}
}

10
src/platform/mod.rs Normal file
View File

@@ -0,0 +1,10 @@
//! Platform selection and capability reporting.
pub mod capabilities;
pub mod defaults;
pub mod linux;
#[cfg(unix)]
pub mod usb_reset;
pub mod windows;
pub use capabilities::{FeatureCapability, PlatformCapabilities, PlatformMode};

174
src/platform/usb_reset.rs Normal file
View File

@@ -0,0 +1,174 @@
//! USB device enumeration and reset via sysfs `authorized`.
use std::io;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
fn usb_device_dir_for_v4l_sysfs(device_link: &Path) -> io::Result<PathBuf> {
let mut p = device_link.canonicalize()?;
loop {
if p.join("busnum").is_file() && p.join("devnum").is_file() {
return Ok(p);
}
p = p
.parent()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no USB parent in sysfs"))?
.to_path_buf();
if p.as_os_str().is_empty() || p == Path::new("/") {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"reached sysfs root without USB device",
));
}
}
}
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct UsbDeviceInfo {
pub bus_num: u32,
pub dev_num: u32,
pub id_vendor: String,
pub id_product: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub product: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manufacturer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorized: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub driver: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub video_device: Option<String>,
}
fn read_sysfs_str(dir: &Path, attr: &str) -> Option<String> {
std::fs::read_to_string(dir.join(attr))
.ok()
.map(|s| s.trim_end().to_string())
}
fn read_sysfs_u32(dir: &Path, attr: &str) -> Option<u32> {
read_sysfs_str(dir, attr).and_then(|s| s.parse().ok())
}
fn build_usb_to_video_map() -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::new();
let v4l_class = Path::new("/sys/class/video4linux");
let entries = match std::fs::read_dir(v4l_class) {
Ok(e) => e,
Err(_) => return map,
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = match name.to_str() {
Some(s) if s.starts_with("video") => s,
_ => continue,
};
let device_link = v4l_class.join(name_str).join("device");
if let Ok(usb_dir) = usb_device_dir_for_v4l_sysfs(&device_link) {
if let Some(key) = usb_dir.file_name().and_then(|k| k.to_str()) {
map.insert(key.to_string(), format!("/dev/{}", name_str));
}
}
}
map
}
pub fn list_usb_devices() -> Vec<UsbDeviceInfo> {
let usb_bus = Path::new("/sys/bus/usb/devices");
let entries = match std::fs::read_dir(usb_bus) {
Ok(e) => e,
Err(_) => return vec![],
};
let video_map = build_usb_to_video_map();
let mut devices: Vec<UsbDeviceInfo> = entries
.flatten()
.filter_map(|entry| {
let dir = entry.path();
let bus_num = read_sysfs_u32(&dir, "busnum")?;
let dev_num = read_sysfs_u32(&dir, "devnum")?;
let id_vendor = read_sysfs_str(&dir, "idVendor").unwrap_or_default();
let id_product = read_sysfs_str(&dir, "idProduct").unwrap_or_default();
let product = read_sysfs_str(&dir, "product");
let manufacturer = read_sysfs_str(&dir, "manufacturer");
let speed = read_sysfs_str(&dir, "speed");
let authorized = if dir.join("authorized").exists() {
read_sysfs_str(&dir, "authorized")
.and_then(|s| s.trim().parse::<u8>().ok())
.map(|v| v != 0)
} else {
None
};
let driver = std::fs::read_link(dir.join("driver"))
.ok()
.and_then(|p| p.file_name().map(|f| f.to_string_lossy().to_string()));
let dir_name = dir.file_name()?.to_str()?.to_string();
let video_device = video_map.get(&dir_name).cloned();
Some(UsbDeviceInfo {
bus_num,
dev_num,
id_vendor,
id_product,
product,
manufacturer,
speed,
authorized,
driver,
video_device,
})
})
.collect();
devices.sort_by(|a, b| (a.bus_num, a.dev_num).cmp(&(b.bus_num, b.dev_num)));
devices
}
pub fn reset_usb_device(bus_num: u32, dev_num: u32) -> io::Result<()> {
let usb_bus = Path::new("/sys/bus/usb/devices");
let entries = std::fs::read_dir(usb_bus)?;
for entry in entries.flatten() {
let dir = entry.path();
if read_sysfs_u32(&dir, "busnum") != Some(bus_num)
|| read_sysfs_u32(&dir, "devnum") != Some(dev_num)
{
continue;
}
let authorized = dir.join("authorized");
if !authorized.exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("device {bus_num}-{dev_num} has no authorized attribute"),
));
}
std::fs::write(&authorized, b"0")?;
std::thread::sleep(Duration::from_millis(300));
std::fs::write(&authorized, b"1")?;
let wait_until = Instant::now() + Duration::from_secs(2);
while !dir.join("busnum").exists() {
if Instant::now() >= wait_until {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
return Ok(());
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("USB device {bus_num}-{dev_num} not found in sysfs"),
))
}

33
src/platform/windows.rs Normal file
View File

@@ -0,0 +1,33 @@
//! Windows platform capabilities.
use super::{FeatureCapability, PlatformCapabilities, PlatformMode};
pub fn capabilities() -> PlatformCapabilities {
let linux_only = "unsupported on Windows";
PlatformCapabilities {
mode: PlatformMode::Windows,
mode_label: PlatformMode::Windows.label(),
video_capture: FeatureCapability::available(["directshow_uvc", "mjpeg"])
.with_selected_backend(Some("directshow_uvc".to_string())),
encoder: FeatureCapability::available([
"ffmpeg_h264",
"ffmpeg_h265",
"ffmpeg_vp8",
"ffmpeg_vp9",
"software",
"mjpeg",
]),
hid: FeatureCapability::available(["ch9329", "none"])
.with_selected_backend(Some("ch9329".to_string())),
atx: FeatureCapability::available(["serial", "wol", "none"]),
msd: FeatureCapability::unsupported(linux_only),
otg: FeatureCapability::unsupported(linux_only),
audio: FeatureCapability::available(["wasapi", "opus"])
.with_selected_backend(Some("wasapi".to_string())),
rustdesk: FeatureCapability::available(["builtin", "tcp_direct", "relay"])
.with_selected_backend(Some("builtin".to_string())),
diagnostics: FeatureCapability::available(["windows"]),
extensions: FeatureCapability::available(["windows_safe"]),
service_installation: FeatureCapability::available(["windows_service"]),
}
}