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

@@ -1,497 +1,150 @@
//! ATX Key Executor
//!
//! Lightweight executor for a single ATX key operation.
//! Each executor handles one button (power or reset) with its own hardware binding.
//! ATX key executor backend selector.
use gpio_cdev::{Chip, LineHandle, LineRequestFlags};
use serialport::SerialPort;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::os::fd::AsRawFd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::sleep;
use tracing::{debug, info};
use tracing::debug;
use super::types::{ActiveLevel, AtxDriverType, AtxKeyConfig};
use super::serial_relay::SerialRelayBackend;
use super::traits::{AtxKeyBackend, AtxKeyBackendContext, SharedSerialHandle};
use super::types::{AtxDriverType, AtxKeyConfig};
use crate::error::{AppError, Result};
pub type SharedSerialHandle = Arc<Mutex<Box<dyn SerialPort>>>;
const USB_RELAY_MAX_CHANNEL: u8 = 8;
const USB_RELAY_REPORT_LEN: usize = 9;
const HIDIOCSFEATURE_9: libc::c_ulong = 0xC009_4806; // _IOC(_IOC_READ|_IOC_WRITE, 'H', 0x06, 9)
/// Timing constants for ATX operations
pub mod timing {
use std::time::Duration;
/// Short press duration (power on/graceful shutdown)
pub const SHORT_PRESS: Duration = Duration::from_millis(500);
/// Long press duration (force power off)
pub const LONG_PRESS: Duration = Duration::from_millis(5000);
/// Reset press duration
pub const RESET_PRESS: Duration = Duration::from_millis(500);
}
/// Executor for a single ATX key operation
///
/// Each executor manages one hardware button (power or reset).
/// It handles both GPIO and USB relay backends.
pub struct AtxKeyExecutor {
config: AtxKeyConfig,
gpio_handle: Mutex<Option<LineHandle>>,
/// Cached USB relay file handle to avoid repeated open/close syscalls
usb_relay_handle: Mutex<Option<File>>,
/// Cached Serial port handle (can be shared across power/reset executors)
serial_handle: Mutex<Option<SharedSerialHandle>>,
initialized: AtomicBool,
backend: Option<Box<dyn AtxKeyBackend>>,
}
impl AtxKeyExecutor {
/// Create a new executor with the given configuration
pub fn new(config: AtxKeyConfig) -> Self {
Self {
config,
gpio_handle: Mutex::new(None),
usb_relay_handle: Mutex::new(None),
serial_handle: Mutex::new(None),
initialized: AtomicBool::new(false),
}
Self::with_context(config, AtxKeyBackendContext::Standalone)
}
/// Create a new executor with a pre-opened shared serial handle.
pub fn new_with_shared_serial(config: AtxKeyConfig, serial_handle: SharedSerialHandle) -> Self {
Self {
config,
gpio_handle: Mutex::new(None),
usb_relay_handle: Mutex::new(None),
serial_handle: Mutex::new(Some(serial_handle)),
initialized: AtomicBool::new(false),
}
Self::with_context(config, AtxKeyBackendContext::SharedSerial(serial_handle))
}
/// Open a serial relay device and wrap it for shared use.
pub fn open_shared_serial(device: &str, baud_rate: u32) -> Result<SharedSerialHandle> {
let port = serialport::new(device, baud_rate)
.timeout(Duration::from_millis(100))
.open()
.map_err(|e| AppError::Internal(format!("Serial port open failed: {}", e)))?;
Ok(Arc::new(Mutex::new(port)))
SerialRelayBackend::open_shared_serial(device, baud_rate)
}
fn with_context(config: AtxKeyConfig, context: AtxKeyBackendContext) -> Self {
let backend = build_backend(&config, context);
Self { config, backend }
}
/// Check if this executor is configured
pub fn is_configured(&self) -> bool {
self.config.is_configured()
}
/// Check if this executor is initialized
pub fn is_initialized(&self) -> bool {
self.initialized.load(Ordering::Relaxed)
}
/// Initialize the executor
pub async fn init(&mut self) -> Result<()> {
if !self.config.is_configured() {
debug!("ATX key executor not configured, skipping init");
return Ok(());
}
self.validate_runtime_config()?;
match self.config.driver {
AtxDriverType::Gpio => self.init_gpio().await?,
AtxDriverType::UsbRelay => self.init_usb_relay().await?,
AtxDriverType::Serial => self.init_serial().await?,
AtxDriverType::None => {}
}
self.initialized.store(true, Ordering::Relaxed);
Ok(())
}
fn validate_runtime_config(&self) -> Result<()> {
match self.config.driver {
AtxDriverType::Serial => {
if self.config.pin == 0 {
return Err(AppError::Config(
"Serial ATX channel must be 1-based (>= 1)".to_string(),
));
}
if self.config.pin > u8::MAX as u32 {
return Err(AppError::Config(format!(
"Serial ATX channel must be <= {}",
u8::MAX
)));
}
if self.config.baud_rate == 0 {
return Err(AppError::Config(
"Serial ATX baud_rate must be greater than 0".to_string(),
));
}
}
AtxDriverType::UsbRelay => {
if self.config.pin == 0 {
return Err(AppError::Config(
"USB relay channel must be 1-based (>= 1)".to_string(),
));
}
if self.config.pin > u8::MAX as u32 {
return Err(AppError::Config(format!(
"USB relay channel must be <= {}",
u8::MAX
)));
}
if self.config.pin > USB_RELAY_MAX_CHANNEL as u32 {
return Err(AppError::Config(format!(
"USB HID relay channel must be <= {}",
USB_RELAY_MAX_CHANNEL
)));
}
}
AtxDriverType::Gpio | AtxDriverType::None => {}
}
Ok(())
}
/// Initialize GPIO backend
async fn init_gpio(&mut self) -> Result<()> {
info!(
"Initializing GPIO ATX executor on {} pin {}",
self.config.device, self.config.pin
);
let mut chip = Chip::new(&self.config.device)
.map_err(|e| AppError::Internal(format!("GPIO chip open failed: {}", e)))?;
let line = chip.get_line(self.config.pin).map_err(|e| {
AppError::Internal(format!("GPIO line {} failed: {}", self.config.pin, e))
let backend = self.backend.as_mut().ok_or_else(|| {
AppError::Internal(format!(
"ATX backend {:?} is unsupported on this platform",
self.config.driver
))
})?;
// Initial value depends on active level (start in inactive state)
let initial_value = match self.config.active_level {
ActiveLevel::High => 0, // Inactive = low
ActiveLevel::Low => 1, // Inactive = high
};
let handle = line
.request(LineRequestFlags::OUTPUT, initial_value, "one-kvm-atx")
.map_err(|e| AppError::Internal(format!("GPIO request failed: {}", e)))?;
*self.gpio_handle.lock().unwrap() = Some(handle);
debug!("GPIO pin {} configured successfully", self.config.pin);
Ok(())
backend.init().await
}
/// Initialize USB relay backend
async fn init_usb_relay(&self) -> Result<()> {
info!(
"Initializing USB relay ATX executor on {} channel {}",
self.config.device, self.config.pin
);
// Open and cache the device handle
let device = OpenOptions::new()
.read(true)
.write(true)
.open(&self.config.device)
.map_err(|e| AppError::Internal(format!("USB relay device open failed: {}", e)))?;
*self.usb_relay_handle.lock().unwrap() = Some(device);
// Ensure relay is off initially
self.send_usb_relay_command(false)?;
debug!(
"USB relay channel {} configured successfully",
self.config.pin
);
Ok(())
}
/// Initialize Serial relay backend
async fn init_serial(&self) -> Result<()> {
info!(
"Initializing Serial relay ATX executor on {} channel {}",
self.config.device, self.config.pin
);
let existing_handle = self.serial_handle.lock().unwrap().as_ref().cloned();
if existing_handle.is_none() {
let shared = Self::open_shared_serial(&self.config.device, self.config.baud_rate)?;
*self.serial_handle.lock().unwrap() = Some(shared);
}
// Ensure relay is off initially
self.send_serial_relay_command(false)?;
debug!(
"Serial relay channel {} configured successfully",
self.config.pin
);
Ok(())
}
/// Pulse the button for the specified duration
pub async fn pulse(&self, duration: Duration) -> Result<()> {
if !self.is_configured() {
return Err(AppError::Internal("ATX key not configured".to_string()));
}
if !self.is_initialized() {
let backend = self.backend.as_ref().ok_or_else(|| {
AppError::Internal(format!(
"ATX backend {:?} is unsupported on this platform",
self.config.driver
))
})?;
if !backend.is_initialized() {
return Err(AppError::Internal("ATX key not initialized".to_string()));
}
match self.config.driver {
AtxDriverType::Gpio => self.pulse_gpio(duration).await,
AtxDriverType::UsbRelay => self.pulse_usb_relay(duration).await,
AtxDriverType::Serial => self.pulse_serial(duration).await,
AtxDriverType::None => Ok(()),
}
backend.pulse(duration).await
}
/// Pulse GPIO pin
async fn pulse_gpio(&self, duration: Duration) -> Result<()> {
let (active, inactive) = match self.config.active_level {
ActiveLevel::High => (1u8, 0u8),
ActiveLevel::Low => (0u8, 1u8),
};
// Set to active state
{
let guard = self.gpio_handle.lock().unwrap();
let handle = guard
.as_ref()
.ok_or_else(|| AppError::Internal("GPIO not initialized".to_string()))?;
handle
.set_value(active)
.map_err(|e| AppError::Internal(format!("GPIO set failed: {}", e)))?;
}
// Wait for duration (no lock held)
sleep(duration).await;
// Set to inactive state
{
let guard = self.gpio_handle.lock().unwrap();
if let Some(handle) = guard.as_ref() {
handle.set_value(inactive).ok();
}
}
Ok(())
}
/// Pulse USB relay
async fn pulse_usb_relay(&self, duration: Duration) -> Result<()> {
// Turn relay on
self.send_usb_relay_command(true)?;
// Wait for duration
sleep(duration).await;
// Turn relay off
self.send_usb_relay_command(false)?;
Ok(())
}
/// Send USB relay command using cached handle
fn send_usb_relay_command(&self, on: bool) -> Result<()> {
let channel = u8::try_from(self.config.pin).map_err(|_| {
AppError::Config(format!(
"USB relay channel {} exceeds max {}",
self.config.pin,
u8::MAX
))
})?;
if channel == 0 {
return Err(AppError::Config(
"USB relay channel must be 1-based (>= 1)".to_string(),
));
}
if channel > USB_RELAY_MAX_CHANNEL {
return Err(AppError::Config(format!(
"USB HID relay channel must be <= {}",
USB_RELAY_MAX_CHANNEL
)));
}
let cmd = Self::build_usb_relay_command(channel, on);
let mut guard = self.usb_relay_handle.lock().unwrap();
let device = guard
.as_mut()
.ok_or_else(|| AppError::Internal("USB relay not initialized".to_string()))?;
if let Err(feature_err) = Self::send_usb_relay_feature_report(device, &cmd) {
debug!(
"USB relay feature report failed ({}), falling back to hidraw write",
feature_err
);
device.write_all(&cmd).map_err(|write_err| {
AppError::Internal(format!(
"USB relay feature report failed: {}; raw write failed: {}",
feature_err, write_err
))
})?;
device
.flush()
.map_err(|e| AppError::Internal(format!("USB relay flush failed: {}", e)))?;
}
Ok(())
}
fn build_usb_relay_command(channel: u8, on: bool) -> [u8; USB_RELAY_REPORT_LEN] {
let mut cmd = [0x00; USB_RELAY_REPORT_LEN];
cmd[1] = if on { 0xFF } else { 0xFD };
cmd[2] = channel;
cmd
}
fn send_usb_relay_feature_report(
device: &File,
report: &[u8; USB_RELAY_REPORT_LEN],
) -> std::io::Result<()> {
// Linux hidraw feature reports include the report ID as the first byte.
let rc = unsafe { libc::ioctl(device.as_raw_fd(), HIDIOCSFEATURE_9, report.as_ptr()) };
if rc < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
/// Pulse Serial relay
async fn pulse_serial(&self, duration: Duration) -> Result<()> {
info!(
"Pulse serial relay on {} pin {}",
self.config.device, self.config.pin
);
// Turn relay on
self.send_serial_relay_command(true)?;
// Wait for duration
sleep(duration).await;
// Turn relay off
self.send_serial_relay_command(false)?;
Ok(())
}
/// Send Serial relay command using cached handle
fn send_serial_relay_command(&self, on: bool) -> Result<()> {
let channel = u8::try_from(self.config.pin).map_err(|_| {
AppError::Config(format!(
"Serial relay channel {} exceeds max {}",
self.config.pin,
u8::MAX
))
})?;
if channel == 0 {
return Err(AppError::Config(
"Serial relay channel must be 1-based (>= 1)".to_string(),
));
}
// LCUS-Type Protocol
// Frame: [StopByte(A0), Channel, State, Checksum]
// Checksum = A0 + channel + state
let state = if on { 1 } else { 0 };
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
// Example for Channel 1:
// ON: A0 01 01 A2
// OFF: A0 01 00 A1
let cmd = [0xA0, channel, state, checksum];
let serial_handle = self
.serial_handle
.lock()
.unwrap()
.as_ref()
.cloned()
.ok_or_else(|| AppError::Internal("Serial relay not initialized".to_string()))?;
let mut port = serial_handle.lock().unwrap();
port.write_all(&cmd)
.map_err(|e| AppError::Internal(format!("Serial relay write failed: {}", e)))?;
port.flush()
.map_err(|e| AppError::Internal(format!("Serial relay flush failed: {}", e)))?;
Ok(())
}
/// Shutdown the executor
pub async fn shutdown(&mut self) -> Result<()> {
if !self.is_initialized() {
return Ok(());
if let Some(backend) = self.backend.as_mut() {
backend.shutdown().await?;
}
match self.config.driver {
AtxDriverType::Gpio => {
// Release GPIO handle
*self.gpio_handle.lock().unwrap() = None;
}
AtxDriverType::UsbRelay => {
// Ensure relay is off before closing handle
let _ = self.send_usb_relay_command(false);
// Release USB relay handle
*self.usb_relay_handle.lock().unwrap() = None;
}
AtxDriverType::Serial => {
// Ensure relay is off before closing handle
let _ = self.send_serial_relay_command(false);
// Release Serial relay handle
*self.serial_handle.lock().unwrap() = None;
}
AtxDriverType::None => {}
}
self.initialized.store(false, Ordering::Relaxed);
debug!("ATX key executor shutdown complete");
Ok(())
}
}
impl Drop for AtxKeyExecutor {
fn drop(&mut self) {
// Ensure GPIO lines are released
*self.gpio_handle.lock().unwrap() = None;
// Ensure USB relay is off and handle released
if self.config.driver == AtxDriverType::UsbRelay && self.is_initialized() {
let _ = self.send_usb_relay_command(false);
}
*self.usb_relay_handle.lock().unwrap() = None;
// Ensure Serial relay is off and handle released
if self.config.driver == AtxDriverType::Serial && self.is_initialized() {
let _ = self.send_serial_relay_command(false);
}
*self.serial_handle.lock().unwrap() = None;
fn build_backend(
config: &AtxKeyConfig,
context: AtxKeyBackendContext,
) -> Option<Box<dyn AtxKeyBackend>> {
match config.driver {
AtxDriverType::Serial => Some(match context {
AtxKeyBackendContext::Standalone => Box::new(SerialRelayBackend::new(config.clone())),
AtxKeyBackendContext::SharedSerial(handle) => Box::new(
SerialRelayBackend::new_with_shared_serial(config.clone(), handle),
),
}),
AtxDriverType::Gpio => build_gpio_backend(config),
AtxDriverType::UsbRelay => build_hidraw_backend(config),
AtxDriverType::None => None,
}
}
#[cfg(unix)]
fn build_gpio_backend(config: &AtxKeyConfig) -> Option<Box<dyn AtxKeyBackend>> {
Some(Box::new(super::gpio_linux::GpioLinuxBackend::new(
config.clone(),
)))
}
#[cfg(not(unix))]
fn build_gpio_backend(_config: &AtxKeyConfig) -> Option<Box<dyn AtxKeyBackend>> {
Some(Box::new(super::disabled_key::DisabledAtxKeyBackend::new(
"GPIO ATX backend is only available on Linux",
)))
}
#[cfg(unix)]
fn build_hidraw_backend(config: &AtxKeyConfig) -> Option<Box<dyn AtxKeyBackend>> {
Some(Box::new(super::hidraw_linux::HidrawLinuxRelayBackend::new(
config.clone(),
)))
}
#[cfg(not(unix))]
fn build_hidraw_backend(_config: &AtxKeyConfig) -> Option<Box<dyn AtxKeyBackend>> {
Some(Box::new(super::disabled_key::DisabledAtxKeyBackend::new(
"USB hidraw relay backend is only available on Linux",
)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atx::ActiveLevel;
#[test]
fn test_executor_creation() {
fn executor_creation() {
let config = AtxKeyConfig::default();
let executor = AtxKeyExecutor::new(config);
assert!(!executor.is_configured());
assert!(!executor.is_initialized());
}
#[test]
fn test_executor_with_gpio_config() {
fn executor_with_gpio_config() {
let config = AtxKeyConfig {
driver: AtxDriverType::Gpio,
device: "/dev/gpiochip0".to_string(),
@@ -501,16 +154,15 @@ mod tests {
};
let executor = AtxKeyExecutor::new(config);
assert!(executor.is_configured());
assert!(!executor.is_initialized());
}
#[test]
fn test_executor_with_usb_relay_config() {
fn executor_with_usb_relay_config() {
let config = AtxKeyConfig {
driver: AtxDriverType::UsbRelay,
device: "/dev/hidraw0".to_string(),
pin: 1,
active_level: ActiveLevel::High, // Ignored for USB relay
active_level: ActiveLevel::High,
baud_rate: 9600,
};
let executor = AtxKeyExecutor::new(config);
@@ -518,12 +170,12 @@ mod tests {
}
#[test]
fn test_executor_with_serial_config() {
fn executor_with_serial_config() {
let config = AtxKeyConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
pin: 1,
active_level: ActiveLevel::High, // Ignored
active_level: ActiveLevel::High,
baud_rate: 9600,
};
let executor = AtxKeyExecutor::new(config);
@@ -531,91 +183,9 @@ mod tests {
}
#[test]
fn test_timing_constants() {
fn timing_constants() {
assert_eq!(timing::SHORT_PRESS.as_millis(), 500);
assert_eq!(timing::LONG_PRESS.as_millis(), 5000);
assert_eq!(timing::RESET_PRESS.as_millis(), 500);
}
#[test]
fn test_usb_relay_command_format() {
assert_eq!(
AtxKeyExecutor::build_usb_relay_command(1, true),
[0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
);
assert_eq!(
AtxKeyExecutor::build_usb_relay_command(1, false),
[0x00, 0xFD, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
);
}
#[tokio::test]
async fn test_executor_init_rejects_serial_channel_zero() {
let config = AtxKeyConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
pin: 0,
active_level: ActiveLevel::High,
baud_rate: 9600,
};
let mut executor = AtxKeyExecutor::new(config);
let err = executor.init().await.unwrap_err();
assert!(matches!(err, AppError::Config(_)));
}
#[tokio::test]
async fn test_executor_init_rejects_usb_relay_channel_zero() {
let config = AtxKeyConfig {
driver: AtxDriverType::UsbRelay,
device: "/dev/hidraw0".to_string(),
pin: 0,
active_level: ActiveLevel::High,
baud_rate: 9600,
};
let mut executor = AtxKeyExecutor::new(config);
let err = executor.init().await.unwrap_err();
assert!(matches!(err, AppError::Config(_)));
}
#[tokio::test]
async fn test_executor_init_rejects_usb_relay_channel_overflow() {
let config = AtxKeyConfig {
driver: AtxDriverType::UsbRelay,
device: "/dev/hidraw0".to_string(),
pin: USB_RELAY_MAX_CHANNEL as u32 + 1,
active_level: ActiveLevel::High,
baud_rate: 9600,
};
let mut executor = AtxKeyExecutor::new(config);
let err = executor.init().await.unwrap_err();
assert!(matches!(err, AppError::Config(_)));
}
#[tokio::test]
async fn test_executor_init_rejects_serial_channel_overflow() {
let config = AtxKeyConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
pin: 256,
active_level: ActiveLevel::High,
baud_rate: 9600,
};
let mut executor = AtxKeyExecutor::new(config);
let err = executor.init().await.unwrap_err();
assert!(matches!(err, AppError::Config(_)));
}
#[tokio::test]
async fn test_executor_init_rejects_zero_serial_baud_rate() {
let config = AtxKeyConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
pin: 1,
active_level: ActiveLevel::High,
baud_rate: 0,
};
let mut executor = AtxKeyExecutor::new(config);
let err = executor.init().await.unwrap_err();
assert!(matches!(err, AppError::Config(_)));
}
}