mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
@@ -11,20 +11,14 @@ use super::led::LedSensor;
|
||||
use super::types::{AtxAction, AtxKeyConfig, AtxLedConfig, AtxState, PowerStatus};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// ATX power control configuration
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AtxControllerConfig {
|
||||
/// Whether ATX is enabled
|
||||
pub enabled: bool,
|
||||
/// Power button configuration (used for both short and long press)
|
||||
pub power: AtxKeyConfig,
|
||||
/// Reset button configuration
|
||||
pub reset: AtxKeyConfig,
|
||||
/// LED sensing configuration
|
||||
pub led: AtxLedConfig,
|
||||
}
|
||||
|
||||
/// Internal state holding all ATX components
|
||||
/// Grouped together to reduce lock acquisitions
|
||||
struct AtxInner {
|
||||
config: AtxControllerConfig,
|
||||
@@ -33,12 +27,9 @@ struct AtxInner {
|
||||
led_sensor: Option<LedSensor>,
|
||||
}
|
||||
|
||||
/// ATX Controller
|
||||
///
|
||||
/// Manages ATX power control through independent executors for each action.
|
||||
/// Supports hot-reload of configuration.
|
||||
pub struct AtxController {
|
||||
/// Single lock for all internal state to reduce lock contention
|
||||
inner: RwLock<AtxInner>,
|
||||
}
|
||||
|
||||
@@ -53,6 +44,24 @@ impl AtxController {
|
||||
&& power.baud_rate == reset.baud_rate
|
||||
}
|
||||
|
||||
async fn init_key_executor(
|
||||
warn_label: &str,
|
||||
info_label: &str,
|
||||
config: AtxKeyConfig,
|
||||
mut executor: AtxKeyExecutor,
|
||||
) -> Option<AtxKeyExecutor> {
|
||||
if let Err(e) = executor.init().await {
|
||||
warn!("Failed to initialize {} executor: {}", warn_label, e);
|
||||
return None;
|
||||
}
|
||||
|
||||
info!(
|
||||
"{} executor initialized: {:?} on {} pin {}",
|
||||
info_label, config.driver, config.device, config.pin
|
||||
);
|
||||
Some(executor)
|
||||
}
|
||||
|
||||
async fn init_components(inner: &mut AtxInner) {
|
||||
if Self::should_share_serial_device(&inner.config.power, &inner.config.reset) {
|
||||
match AtxKeyExecutor::open_shared_serial(
|
||||
@@ -60,36 +69,28 @@ impl AtxController {
|
||||
inner.config.power.baud_rate,
|
||||
) {
|
||||
Ok(shared_serial) => {
|
||||
let mut power_executor = AtxKeyExecutor::new_with_shared_serial(
|
||||
inner.config.power.clone(),
|
||||
shared_serial.clone(),
|
||||
);
|
||||
if let Err(e) = power_executor.init().await {
|
||||
warn!("Failed to initialize power executor: {}", e);
|
||||
} else {
|
||||
info!(
|
||||
"Power executor initialized: {:?} on {} pin {}",
|
||||
inner.config.power.driver,
|
||||
inner.config.power.device,
|
||||
inner.config.power.pin
|
||||
for (slot, warn_label, info_label, config, serial) in [
|
||||
(
|
||||
&mut inner.power_executor,
|
||||
"power",
|
||||
"Power",
|
||||
inner.config.power.clone(),
|
||||
shared_serial.clone(),
|
||||
),
|
||||
(
|
||||
&mut inner.reset_executor,
|
||||
"reset",
|
||||
"Reset",
|
||||
inner.config.reset.clone(),
|
||||
shared_serial,
|
||||
),
|
||||
] {
|
||||
let executor = AtxKeyExecutor::new_with_shared_serial(
|
||||
config.clone(),
|
||||
serial,
|
||||
);
|
||||
inner.power_executor = Some(power_executor);
|
||||
}
|
||||
|
||||
let mut reset_executor = AtxKeyExecutor::new_with_shared_serial(
|
||||
inner.config.reset.clone(),
|
||||
shared_serial,
|
||||
);
|
||||
if let Err(e) = reset_executor.init().await {
|
||||
warn!("Failed to initialize reset executor: {}", e);
|
||||
} else {
|
||||
info!(
|
||||
"Reset executor initialized: {:?} on {} pin {}",
|
||||
inner.config.reset.driver,
|
||||
inner.config.reset.device,
|
||||
inner.config.reset.pin
|
||||
);
|
||||
inner.reset_executor = Some(reset_executor);
|
||||
*slot = Self::init_key_executor(warn_label, info_label, config, executor)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -100,40 +101,18 @@ impl AtxController {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Initialize power executor
|
||||
if inner.config.power.is_configured() {
|
||||
let mut executor = AtxKeyExecutor::new(inner.config.power.clone());
|
||||
if let Err(e) = executor.init().await {
|
||||
warn!("Failed to initialize power executor: {}", e);
|
||||
} else {
|
||||
info!(
|
||||
"Power executor initialized: {:?} on {} pin {}",
|
||||
inner.config.power.driver,
|
||||
inner.config.power.device,
|
||||
inner.config.power.pin
|
||||
);
|
||||
inner.power_executor = Some(executor);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize reset executor
|
||||
if inner.config.reset.is_configured() {
|
||||
let mut executor = AtxKeyExecutor::new(inner.config.reset.clone());
|
||||
if let Err(e) = executor.init().await {
|
||||
warn!("Failed to initialize reset executor: {}", e);
|
||||
} else {
|
||||
info!(
|
||||
"Reset executor initialized: {:?} on {} pin {}",
|
||||
inner.config.reset.driver,
|
||||
inner.config.reset.device,
|
||||
inner.config.reset.pin
|
||||
);
|
||||
inner.reset_executor = Some(executor);
|
||||
for (slot, warn_label, info_label, config) in [
|
||||
(&mut inner.power_executor, "power", "Power", inner.config.power.clone()),
|
||||
(&mut inner.reset_executor, "reset", "Reset", inner.config.reset.clone()),
|
||||
] {
|
||||
if config.is_configured() {
|
||||
let executor = AtxKeyExecutor::new(config.clone());
|
||||
*slot = Self::init_key_executor(warn_label, info_label, config, executor)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize LED sensor
|
||||
if inner.config.led.is_configured() {
|
||||
let mut sensor = LedSensor::new(inner.config.led.clone());
|
||||
if let Err(e) = sensor.init().await {
|
||||
@@ -149,19 +128,17 @@ impl AtxController {
|
||||
}
|
||||
|
||||
async fn shutdown_components(inner: &mut AtxInner) {
|
||||
if let Some(executor) = inner.power_executor.as_mut() {
|
||||
if let Err(e) = executor.shutdown().await {
|
||||
warn!("Failed to shutdown power executor: {}", e);
|
||||
for (slot, label) in [
|
||||
(&mut inner.power_executor, "power"),
|
||||
(&mut inner.reset_executor, "reset"),
|
||||
] {
|
||||
if let Some(executor) = slot.as_mut() {
|
||||
if let Err(e) = executor.shutdown().await {
|
||||
warn!("Failed to shutdown {} executor: {}", label, e);
|
||||
}
|
||||
}
|
||||
*slot = None;
|
||||
}
|
||||
inner.power_executor = None;
|
||||
|
||||
if let Some(executor) = inner.reset_executor.as_mut() {
|
||||
if let Err(e) = executor.shutdown().await {
|
||||
warn!("Failed to shutdown reset executor: {}", e);
|
||||
}
|
||||
}
|
||||
inner.reset_executor = None;
|
||||
|
||||
if let Some(sensor) = inner.led_sensor.as_mut() {
|
||||
if let Err(e) = sensor.shutdown().await {
|
||||
@@ -171,7 +148,20 @@ impl AtxController {
|
||||
inner.led_sensor = None;
|
||||
}
|
||||
|
||||
/// Create a new ATX controller with the specified configuration
|
||||
async fn read_power_status(sensor: Option<&LedSensor>) -> PowerStatus {
|
||||
let Some(sensor) = sensor else {
|
||||
return PowerStatus::Unknown;
|
||||
};
|
||||
|
||||
match sensor.read().await {
|
||||
Ok(status) => status,
|
||||
Err(e) => {
|
||||
debug!("Failed to read ATX LED sensor: {}", e);
|
||||
PowerStatus::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(config: AtxControllerConfig) -> Self {
|
||||
Self {
|
||||
inner: RwLock::new(AtxInner {
|
||||
@@ -183,12 +173,10 @@ impl AtxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a disabled ATX controller
|
||||
pub fn disabled() -> Self {
|
||||
Self::new(AtxControllerConfig::default())
|
||||
}
|
||||
|
||||
/// Initialize the ATX controller and its executors
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let mut inner = self.inner.write().await;
|
||||
|
||||
@@ -204,7 +192,6 @@ impl AtxController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reload ATX controller configuration
|
||||
pub async fn reload(&self, config: AtxControllerConfig) -> Result<()> {
|
||||
let mut inner = self.inner.write().await;
|
||||
|
||||
@@ -225,7 +212,6 @@ impl AtxController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown ATX controller and release all resources
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
let mut inner = self.inner.write().await;
|
||||
Self::shutdown_components(&mut inner).await;
|
||||
@@ -233,86 +219,48 @@ impl AtxController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Trigger a power action (short/long/reset)
|
||||
pub async fn trigger_power_action(&self, action: AtxAction) -> Result<()> {
|
||||
let inner = self.inner.read().await;
|
||||
|
||||
match action {
|
||||
AtxAction::Short | AtxAction::Long => {
|
||||
if let Some(executor) = &inner.power_executor {
|
||||
let duration = match action {
|
||||
AtxAction::Short => timing::SHORT_PRESS,
|
||||
AtxAction::Long => timing::LONG_PRESS,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
executor.pulse(duration).await?;
|
||||
} else {
|
||||
return Err(AppError::Config(
|
||||
"Power button not configured for ATX controller".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
AtxAction::Reset => {
|
||||
if let Some(executor) = &inner.reset_executor {
|
||||
executor.pulse(timing::RESET_PRESS).await?;
|
||||
} else {
|
||||
return Err(AppError::Config(
|
||||
"Reset button not configured for ATX controller".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (executor, duration) = match action {
|
||||
AtxAction::Short => (inner.power_executor.as_ref(), timing::SHORT_PRESS),
|
||||
AtxAction::Long => (inner.power_executor.as_ref(), timing::LONG_PRESS),
|
||||
AtxAction::Reset => (inner.reset_executor.as_ref(), timing::RESET_PRESS),
|
||||
};
|
||||
|
||||
let Some(executor) = executor else {
|
||||
return Err(AppError::Config(match action {
|
||||
AtxAction::Reset => "Reset button not configured for ATX controller",
|
||||
_ => "Power button not configured for ATX controller",
|
||||
}
|
||||
.to_string()));
|
||||
};
|
||||
|
||||
executor.pulse(duration).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Trigger a short power button press
|
||||
pub async fn power_short(&self) -> Result<()> {
|
||||
self.trigger_power_action(AtxAction::Short).await
|
||||
}
|
||||
|
||||
/// Trigger a long power button press
|
||||
pub async fn power_long(&self) -> Result<()> {
|
||||
self.trigger_power_action(AtxAction::Long).await
|
||||
}
|
||||
|
||||
/// Trigger a reset button press
|
||||
pub async fn reset(&self) -> Result<()> {
|
||||
self.trigger_power_action(AtxAction::Reset).await
|
||||
}
|
||||
|
||||
/// Get the current power status using the LED sensor (if configured)
|
||||
pub async fn power_status(&self) -> PowerStatus {
|
||||
let inner = self.inner.read().await;
|
||||
|
||||
if let Some(sensor) = &inner.led_sensor {
|
||||
match sensor.read().await {
|
||||
Ok(status) => status,
|
||||
Err(e) => {
|
||||
debug!("Failed to read ATX LED sensor: {}", e);
|
||||
PowerStatus::Unknown
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PowerStatus::Unknown
|
||||
}
|
||||
Self::read_power_status(inner.led_sensor.as_ref()).await
|
||||
}
|
||||
|
||||
/// Get a snapshot of the ATX state for API responses
|
||||
pub async fn state(&self) -> AtxState {
|
||||
let inner = self.inner.read().await;
|
||||
|
||||
let power_status = if let Some(sensor) = &inner.led_sensor {
|
||||
match sensor.read().await {
|
||||
Ok(status) => status,
|
||||
Err(e) => {
|
||||
debug!("Failed to read ATX LED sensor: {}", e);
|
||||
PowerStatus::Unknown
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PowerStatus::Unknown
|
||||
};
|
||||
let power_status = Self::read_power_status(inner.led_sensor.as_ref()).await;
|
||||
|
||||
AtxState {
|
||||
available: inner.config.enabled,
|
||||
|
||||
34
src/atx/disabled_key.rs
Normal file
34
src/atx/disabled_key.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use async_trait::async_trait;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::traits::AtxKeyBackend;
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
pub struct DisabledAtxKeyBackend {
|
||||
reason: &'static str,
|
||||
}
|
||||
|
||||
impl DisabledAtxKeyBackend {
|
||||
pub fn new(reason: &'static str) -> Self {
|
||||
Self { reason }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AtxKeyBackend for DisabledAtxKeyBackend {
|
||||
async fn init(&mut self) -> Result<()> {
|
||||
Err(AppError::Internal(self.reason.to_string()))
|
||||
}
|
||||
|
||||
async fn pulse(&self, _duration: Duration) -> Result<()> {
|
||||
Err(AppError::Internal(self.reason.to_string()))
|
||||
}
|
||||
|
||||
async fn shutdown(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
34
src/atx/disabled_led.rs
Normal file
34
src/atx/disabled_led.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::types::{AtxLedConfig, PowerStatus};
|
||||
use crate::error::Result;
|
||||
|
||||
pub struct LedSensor {
|
||||
config: AtxLedConfig,
|
||||
}
|
||||
|
||||
impl LedSensor {
|
||||
pub fn new(config: AtxLedConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub fn is_configured(&self) -> bool {
|
||||
self.config.is_configured()
|
||||
}
|
||||
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn init(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read(&self) -> Result<PowerStatus> {
|
||||
Ok(PowerStatus::Unknown)
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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(_)));
|
||||
}
|
||||
}
|
||||
|
||||
106
src/atx/gpio_linux.rs
Normal file
106
src/atx/gpio_linux.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use async_trait::async_trait;
|
||||
use gpio_cdev::{Chip, LineHandle, LineRequestFlags};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use super::traits::AtxKeyBackend;
|
||||
use super::types::{ActiveLevel, AtxKeyConfig};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
pub struct GpioLinuxBackend {
|
||||
config: AtxKeyConfig,
|
||||
handle: Mutex<Option<LineHandle>>,
|
||||
initialized: AtomicBool,
|
||||
}
|
||||
|
||||
impl GpioLinuxBackend {
|
||||
pub fn new(config: AtxKeyConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
handle: Mutex::new(None),
|
||||
initialized: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AtxKeyBackend for GpioLinuxBackend {
|
||||
async fn init(&mut self) -> Result<()> {
|
||||
info!(
|
||||
"Initializing GPIO ATX backend 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 initial_value = match self.config.active_level {
|
||||
ActiveLevel::High => 0,
|
||||
ActiveLevel::Low => 1,
|
||||
};
|
||||
|
||||
let handle = line
|
||||
.request(LineRequestFlags::OUTPUT, initial_value, "one-kvm-atx")
|
||||
.map_err(|e| AppError::Internal(format!("GPIO request failed: {}", e)))?;
|
||||
|
||||
*self.handle.lock().unwrap() = Some(handle);
|
||||
self.initialized.store(true, Ordering::Relaxed);
|
||||
debug!("GPIO pin {} configured successfully", self.config.pin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pulse(&self, duration: Duration) -> Result<()> {
|
||||
if !self.is_initialized() {
|
||||
return Err(AppError::Internal("GPIO not initialized".to_string()));
|
||||
}
|
||||
|
||||
let (active, inactive) = match self.config.active_level {
|
||||
ActiveLevel::High => (1u8, 0u8),
|
||||
ActiveLevel::Low => (0u8, 1u8),
|
||||
};
|
||||
|
||||
{
|
||||
let guard = self.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)))?;
|
||||
}
|
||||
|
||||
sleep(duration).await;
|
||||
|
||||
{
|
||||
let guard = self.handle.lock().unwrap();
|
||||
if let Some(handle) = guard.as_ref() {
|
||||
handle.set_value(inactive).ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&mut self) -> Result<()> {
|
||||
*self.handle.lock().unwrap() = None;
|
||||
self.initialized.store(false, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.initialized.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GpioLinuxBackend {
|
||||
fn drop(&mut self) {
|
||||
*self.handle.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
190
src/atx/hidraw_linux.rs
Normal file
190
src/atx/hidraw_linux.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use async_trait::async_trait;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use super::traits::AtxKeyBackend;
|
||||
use super::types::AtxKeyConfig;
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
const USB_RELAY_MAX_CHANNEL: u8 = 8;
|
||||
const USB_RELAY_REPORT_LEN: usize = 9;
|
||||
const HIDIOCSFEATURE_9: libc::c_ulong = 0xC009_4806;
|
||||
|
||||
pub struct HidrawLinuxRelayBackend {
|
||||
config: AtxKeyConfig,
|
||||
handle: Mutex<Option<File>>,
|
||||
initialized: AtomicBool,
|
||||
}
|
||||
|
||||
impl HidrawLinuxRelayBackend {
|
||||
pub fn new(config: AtxKeyConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
handle: Mutex::new(None),
|
||||
initialized: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_config(&self) -> Result<()> {
|
||||
if self.config.pin == 0 {
|
||||
return Err(AppError::Config(
|
||||
"USB relay channel must be 1-based (>= 1)".to_string(),
|
||||
));
|
||||
}
|
||||
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
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_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_command(channel, on);
|
||||
let mut guard = self.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_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(())
|
||||
}
|
||||
|
||||
pub fn build_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_feature_report(
|
||||
device: &File,
|
||||
report: &[u8; USB_RELAY_REPORT_LEN],
|
||||
) -> std::io::Result<()> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AtxKeyBackend for HidrawLinuxRelayBackend {
|
||||
async fn init(&mut self) -> Result<()> {
|
||||
self.validate_config()?;
|
||||
|
||||
info!(
|
||||
"Initializing USB relay ATX backend on {} channel {}",
|
||||
self.config.device, self.config.pin
|
||||
);
|
||||
|
||||
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.handle.lock().unwrap() = Some(device);
|
||||
self.send_command(false)?;
|
||||
self.initialized.store(true, Ordering::Relaxed);
|
||||
|
||||
debug!(
|
||||
"USB relay channel {} configured successfully",
|
||||
self.config.pin
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pulse(&self, duration: Duration) -> Result<()> {
|
||||
if !self.is_initialized() {
|
||||
return Err(AppError::Internal("USB relay not initialized".to_string()));
|
||||
}
|
||||
|
||||
self.send_command(true)?;
|
||||
sleep(duration).await;
|
||||
self.send_command(false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&mut self) -> Result<()> {
|
||||
if self.is_initialized() {
|
||||
let _ = self.send_command(false);
|
||||
}
|
||||
*self.handle.lock().unwrap() = None;
|
||||
self.initialized.store(false, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.initialized.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HidrawLinuxRelayBackend {
|
||||
fn drop(&mut self) {
|
||||
if self.is_initialized() {
|
||||
let _ = self.send_command(false);
|
||||
}
|
||||
*self.handle.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::HidrawLinuxRelayBackend;
|
||||
|
||||
#[test]
|
||||
fn usb_relay_command_format() {
|
||||
assert_eq!(
|
||||
HidrawLinuxRelayBackend::build_command(1, true),
|
||||
[0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
);
|
||||
assert_eq!(
|
||||
HidrawLinuxRelayBackend::build_command(1, false),
|
||||
[0x00, 0xFD, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,6 @@ use tracing::{debug, info};
|
||||
use super::types::{AtxLedConfig, PowerStatus};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// LED sensor for reading power status
|
||||
///
|
||||
/// Uses GPIO to read the power LED state and determine if the system is on or off.
|
||||
pub struct LedSensor {
|
||||
config: AtxLedConfig,
|
||||
handle: Mutex<Option<LineHandle>>,
|
||||
@@ -20,7 +17,6 @@ pub struct LedSensor {
|
||||
}
|
||||
|
||||
impl LedSensor {
|
||||
/// Create a new LED sensor with the given configuration
|
||||
pub fn new(config: AtxLedConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
@@ -29,17 +25,6 @@ impl LedSensor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the sensor is configured
|
||||
pub fn is_configured(&self) -> bool {
|
||||
self.config.is_configured()
|
||||
}
|
||||
|
||||
/// Check if the sensor is initialized
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
self.initialized.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Initialize the LED sensor
|
||||
pub async fn init(&mut self) -> Result<()> {
|
||||
if !self.config.is_configured() {
|
||||
debug!("LED sensor not configured, skipping init");
|
||||
@@ -72,9 +57,8 @@ impl LedSensor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the current power status
|
||||
pub async fn read(&self) -> Result<PowerStatus> {
|
||||
if !self.is_configured() || !self.is_initialized() {
|
||||
if !self.config.is_configured() || !self.initialized.load(Ordering::Relaxed) {
|
||||
return Ok(PowerStatus::Unknown);
|
||||
}
|
||||
|
||||
@@ -85,11 +69,10 @@ impl LedSensor {
|
||||
.get_value()
|
||||
.map_err(|e| AppError::Internal(format!("LED read failed: {}", e)))?;
|
||||
|
||||
// Apply inversion if configured
|
||||
let is_on = if self.config.inverted {
|
||||
value == 0 // Active low: 0 means on
|
||||
value == 0
|
||||
} else {
|
||||
value == 1 // Active high: 1 means on
|
||||
value == 1
|
||||
};
|
||||
|
||||
Ok(if is_on {
|
||||
@@ -102,7 +85,6 @@ impl LedSensor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdown the LED sensor
|
||||
pub async fn shutdown(&mut self) -> Result<()> {
|
||||
*self.handle.lock().unwrap() = None;
|
||||
self.initialized.store(false, Ordering::Relaxed);
|
||||
@@ -125,8 +107,8 @@ mod tests {
|
||||
fn test_led_sensor_creation() {
|
||||
let config = AtxLedConfig::default();
|
||||
let sensor = LedSensor::new(config);
|
||||
assert!(!sensor.is_configured());
|
||||
assert!(!sensor.is_initialized());
|
||||
assert!(!sensor.config.is_configured());
|
||||
assert!(!sensor.initialized.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -138,8 +120,8 @@ mod tests {
|
||||
inverted: false,
|
||||
};
|
||||
let sensor = LedSensor::new(config);
|
||||
assert!(sensor.is_configured());
|
||||
assert!(!sensor.is_initialized());
|
||||
assert!(sensor.config.is_configured());
|
||||
assert!(!sensor.initialized.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -151,7 +133,6 @@ mod tests {
|
||||
inverted: true,
|
||||
};
|
||||
let sensor = LedSensor::new(config);
|
||||
assert!(sensor.is_configured());
|
||||
assert!(sensor.config.inverted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,53 +2,22 @@
|
||||
//!
|
||||
//! Provides ATX power management functionality for IP-KVM.
|
||||
//! Supports flexible hardware binding with independent configuration for each action.
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! - Power button control (short press for on/graceful shutdown, long press for force off)
|
||||
//! - Reset button control
|
||||
//! - Power status monitoring via LED sensing (GPIO only)
|
||||
//! - Independent hardware binding for each action (GPIO or USB relay)
|
||||
//! - Hot-reload configuration support
|
||||
//!
|
||||
//! # Hardware Support
|
||||
//!
|
||||
//! - **GPIO**: Uses Linux GPIO character device (/dev/gpiochipX) for direct hardware control
|
||||
//! - **USB Relay**: Uses HID USB relay modules for isolated switching
|
||||
//! - **Serial Relay**: Uses LCUS-style serial relay modules
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use one_kvm::atx::{AtxController, AtxControllerConfig, AtxKeyConfig, AtxDriverType, ActiveLevel};
|
||||
//!
|
||||
//! let config = AtxControllerConfig {
|
||||
//! enabled: true,
|
||||
//! power: AtxKeyConfig {
|
||||
//! driver: AtxDriverType::Gpio,
|
||||
//! device: "/dev/gpiochip0".to_string(),
|
||||
//! pin: 5,
|
||||
//! active_level: ActiveLevel::High,
|
||||
//! baud_rate: 9600,
|
||||
//! },
|
||||
//! reset: AtxKeyConfig {
|
||||
//! driver: AtxDriverType::UsbRelay,
|
||||
//! device: "/dev/hidraw0".to_string(),
|
||||
//! pin: 0,
|
||||
//! active_level: ActiveLevel::High,
|
||||
//! baud_rate: 9600,
|
||||
//! },
|
||||
//! led: Default::default(),
|
||||
//! };
|
||||
//!
|
||||
//! let controller = AtxController::new(config);
|
||||
//! controller.init().await?;
|
||||
//! controller.power_short().await?; // Turn on or graceful shutdown
|
||||
//! ```
|
||||
|
||||
mod controller;
|
||||
#[cfg(not(unix))]
|
||||
mod disabled_key;
|
||||
mod executor;
|
||||
#[cfg(unix)]
|
||||
mod gpio_linux;
|
||||
#[cfg(unix)]
|
||||
mod hidraw_linux;
|
||||
#[cfg(unix)]
|
||||
mod led;
|
||||
#[cfg(not(unix))]
|
||||
#[path = "disabled_led.rs"]
|
||||
mod led;
|
||||
mod serial_relay;
|
||||
mod traits;
|
||||
mod types;
|
||||
mod wol;
|
||||
|
||||
@@ -58,8 +27,9 @@ pub use types::{
|
||||
ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxKeyConfig, AtxLedConfig, AtxPowerRequest,
|
||||
AtxState, PowerStatus,
|
||||
};
|
||||
pub use wol::send_wol;
|
||||
pub use wol::{list_wol_history, record_wol_history, send_wol};
|
||||
|
||||
#[cfg(any(unix, test))]
|
||||
fn hidraw_uevent_is_usb_relay(uevent: &str) -> bool {
|
||||
let upper = uevent.to_ascii_uppercase();
|
||||
upper.contains("000016C0:000005DF")
|
||||
@@ -69,6 +39,7 @@ fn hidraw_uevent_is_usb_relay(uevent: &str) -> bool {
|
||||
|| upper.contains("USB RELAY")
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_usb_relay_hidraw(name: &str) -> bool {
|
||||
let uevent_path = format!("/sys/class/hidraw/{}/device/uevent", name);
|
||||
std::fs::read_to_string(uevent_path)
|
||||
@@ -82,7 +53,9 @@ fn is_usb_relay_hidraw(name: &str) -> bool {
|
||||
pub fn discover_devices() -> AtxDevices {
|
||||
let mut devices = AtxDevices::default();
|
||||
|
||||
// Single pass through /dev directory
|
||||
devices.serial_ports = crate::utils::list_serial_ports();
|
||||
|
||||
#[cfg(unix)]
|
||||
if let Ok(entries) = std::fs::read_dir("/dev") {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
@@ -100,6 +73,7 @@ pub fn discover_devices() -> AtxDevices {
|
||||
devices.gpio_chips.sort();
|
||||
devices.usb_relays.sort();
|
||||
devices.serial_ports.sort();
|
||||
devices.serial_ports.dedup();
|
||||
|
||||
devices
|
||||
}
|
||||
@@ -129,7 +103,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_module_exports() {
|
||||
// Verify all public exports are accessible
|
||||
let _: AtxDriverType = AtxDriverType::None;
|
||||
let _: ActiveLevel = ActiveLevel::High;
|
||||
let _: AtxKeyConfig = AtxKeyConfig::default();
|
||||
|
||||
141
src/atx/serial_relay.rs
Normal file
141
src/atx/serial_relay.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use async_trait::async_trait;
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use super::traits::{validate_serial_config, AtxKeyBackend, SharedSerialHandle};
|
||||
use super::types::AtxKeyConfig;
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
pub struct SerialRelayBackend {
|
||||
config: AtxKeyConfig,
|
||||
serial_handle: Mutex<Option<SharedSerialHandle>>,
|
||||
initialized: AtomicBool,
|
||||
}
|
||||
|
||||
impl SerialRelayBackend {
|
||||
pub fn new(config: AtxKeyConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
serial_handle: Mutex::new(None),
|
||||
initialized: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_shared_serial(config: AtxKeyConfig, serial_handle: SharedSerialHandle) -> Self {
|
||||
Self {
|
||||
config,
|
||||
serial_handle: Mutex::new(Some(serial_handle)),
|
||||
initialized: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
fn send_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
|
||||
))
|
||||
})?;
|
||||
|
||||
let state = if on { 1 } else { 0 };
|
||||
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AtxKeyBackend for SerialRelayBackend {
|
||||
async fn init(&mut self) -> Result<()> {
|
||||
validate_serial_config(&self.config)?;
|
||||
|
||||
info!(
|
||||
"Initializing Serial relay ATX backend 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);
|
||||
}
|
||||
|
||||
self.send_command(false)?;
|
||||
self.initialized.store(true, Ordering::Relaxed);
|
||||
|
||||
debug!(
|
||||
"Serial relay channel {} configured successfully",
|
||||
self.config.pin
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pulse(&self, duration: Duration) -> Result<()> {
|
||||
if !self.is_initialized() {
|
||||
return Err(AppError::Internal(
|
||||
"Serial relay not initialized".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
info!(
|
||||
"Pulse serial relay on {} pin {}",
|
||||
self.config.device, self.config.pin
|
||||
);
|
||||
self.send_command(true)?;
|
||||
sleep(duration).await;
|
||||
self.send_command(false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&mut self) -> Result<()> {
|
||||
if !self.is_initialized() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _ = self.send_command(false);
|
||||
*self.serial_handle.lock().unwrap() = None;
|
||||
self.initialized.store(false, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.initialized.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SerialRelayBackend {
|
||||
fn drop(&mut self) {
|
||||
if self.is_initialized() {
|
||||
let _ = self.send_command(false);
|
||||
}
|
||||
*self.serial_handle.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
51
src/atx/traits.rs
Normal file
51
src/atx/traits.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use async_trait::async_trait;
|
||||
use serialport::SerialPort;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::types::AtxKeyConfig;
|
||||
use crate::error::Result;
|
||||
|
||||
pub type SharedSerialHandle = Arc<Mutex<Box<dyn SerialPort>>>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AtxKeyBackend: Send + Sync {
|
||||
async fn init(&mut self) -> Result<()>;
|
||||
|
||||
async fn pulse(&self, duration: Duration) -> Result<()>;
|
||||
|
||||
async fn shutdown(&mut self) -> Result<()>;
|
||||
|
||||
fn is_initialized(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AtxKeyBackendContext {
|
||||
Standalone,
|
||||
SharedSerial(SharedSerialHandle),
|
||||
}
|
||||
|
||||
pub fn validate_serial_config(config: &AtxKeyConfig) -> Result<()> {
|
||||
if config.device.trim().is_empty() {
|
||||
return Err(crate::error::AppError::Config(
|
||||
"Serial ATX device cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if config.pin == 0 {
|
||||
return Err(crate::error::AppError::Config(
|
||||
"Serial ATX channel must be 1-based (>= 1)".to_string(),
|
||||
));
|
||||
}
|
||||
if config.pin > u8::MAX as u32 {
|
||||
return Err(crate::error::AppError::Config(format!(
|
||||
"Serial ATX channel must be <= {}",
|
||||
u8::MAX
|
||||
)));
|
||||
}
|
||||
if config.baud_rate == 0 {
|
||||
return Err(crate::error::AppError::Config(
|
||||
"Serial ATX baud_rate must be greater than 0".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,67 +6,43 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
/// Power status
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PowerStatus {
|
||||
/// Power is on
|
||||
On,
|
||||
/// Power is off
|
||||
Off,
|
||||
/// Power status unknown (no LED connected)
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Driver type for ATX key operations
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AtxDriverType {
|
||||
/// GPIO control via Linux character device
|
||||
Gpio,
|
||||
/// USB HID relay module
|
||||
UsbRelay,
|
||||
/// Serial/COM port relay (taobao LCUS type)
|
||||
Serial,
|
||||
/// Disabled / Not configured
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
/// Active level for GPIO pins
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ActiveLevel {
|
||||
/// Active high (default for most cases)
|
||||
#[default]
|
||||
High,
|
||||
/// Active low (inverted)
|
||||
Low,
|
||||
}
|
||||
|
||||
/// Configuration for a single ATX key (power or reset)
|
||||
/// This is the "four-tuple" configuration: (driver, device, pin/channel, level)
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct AtxKeyConfig {
|
||||
/// Driver type (GPIO or USB Relay)
|
||||
pub driver: AtxDriverType,
|
||||
/// Device path:
|
||||
/// - For GPIO: /dev/gpiochipX
|
||||
/// - For USB Relay: /dev/hidrawX
|
||||
pub device: String,
|
||||
/// Pin or channel number:
|
||||
/// - For GPIO: GPIO pin number
|
||||
/// - For USB Relay: relay channel (1-based)
|
||||
/// - For Serial Relay (LCUS): relay channel (1-based)
|
||||
pub pin: u32,
|
||||
/// Active level (only applicable to GPIO, ignored for USB Relay)
|
||||
pub active_level: ActiveLevel,
|
||||
/// Baud rate for serial relay (start with 9600)
|
||||
pub baud_rate: u32,
|
||||
}
|
||||
|
||||
@@ -83,77 +59,54 @@ impl Default for AtxKeyConfig {
|
||||
}
|
||||
|
||||
impl AtxKeyConfig {
|
||||
/// Check if this key is configured
|
||||
pub fn is_configured(&self) -> bool {
|
||||
self.driver != AtxDriverType::None && !self.device.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// LED sensing configuration (optional)
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(default)]
|
||||
pub struct AtxLedConfig {
|
||||
/// Whether LED sensing is enabled
|
||||
pub enabled: bool,
|
||||
/// GPIO chip for LED sensing
|
||||
pub gpio_chip: String,
|
||||
/// GPIO pin for LED input
|
||||
pub gpio_pin: u32,
|
||||
/// Whether LED is active low (inverted logic)
|
||||
pub inverted: bool,
|
||||
}
|
||||
|
||||
impl AtxLedConfig {
|
||||
/// Check if LED sensing is configured
|
||||
pub fn is_configured(&self) -> bool {
|
||||
self.enabled && !self.gpio_chip.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// ATX state information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct AtxState {
|
||||
/// Whether ATX feature is available/enabled
|
||||
pub available: bool,
|
||||
/// Whether power button is configured
|
||||
pub power_configured: bool,
|
||||
/// Whether reset button is configured
|
||||
pub reset_configured: bool,
|
||||
/// Current power status
|
||||
pub power_status: PowerStatus,
|
||||
/// Whether power LED sensing is supported
|
||||
pub led_supported: bool,
|
||||
}
|
||||
|
||||
/// ATX power action request
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AtxPowerRequest {
|
||||
/// Action to perform: "short", "long", "reset"
|
||||
pub action: AtxAction,
|
||||
}
|
||||
|
||||
/// ATX power action
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AtxAction {
|
||||
/// Short press power button (turn on or graceful shutdown)
|
||||
Short,
|
||||
/// Long press power button (force power off)
|
||||
Long,
|
||||
/// Press reset button
|
||||
Reset,
|
||||
}
|
||||
|
||||
/// Available ATX devices for discovery
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AtxDevices {
|
||||
/// Available GPIO chips (/dev/gpiochip*)
|
||||
pub gpio_chips: Vec<String>,
|
||||
/// Available USB HID relay devices (/dev/hidraw*)
|
||||
pub usb_relays: Vec<String>,
|
||||
/// Available Serial ports (/dev/ttyUSB*)
|
||||
pub serial_ports: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -201,13 +154,13 @@ mod tests {
|
||||
assert!(!config.is_configured());
|
||||
|
||||
config.driver = AtxDriverType::Gpio;
|
||||
assert!(!config.is_configured()); // device still empty
|
||||
assert!(!config.is_configured());
|
||||
|
||||
config.device = "/dev/gpiochip0".to_string();
|
||||
assert!(config.is_configured());
|
||||
|
||||
config.driver = AtxDriverType::None;
|
||||
assert!(!config.is_configured()); // driver is None
|
||||
assert!(!config.is_configured());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -224,7 +177,7 @@ mod tests {
|
||||
assert!(!config.is_configured());
|
||||
|
||||
config.enabled = true;
|
||||
assert!(!config.is_configured()); // gpio_chip still empty
|
||||
assert!(!config.is_configured());
|
||||
|
||||
config.gpio_chip = "/dev/gpiochip0".to_string();
|
||||
assert!(config.is_configured());
|
||||
|
||||
@@ -3,18 +3,14 @@
|
||||
//! Sends magic packets to wake up remote machines.
|
||||
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use tracing::{debug, info};
|
||||
use tracing::info;
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// WOL magic packet structure:
|
||||
/// - 6 bytes of 0xFF
|
||||
/// - 16 repetitions of the target MAC address (6 bytes each)
|
||||
/// Total: 6 + 16 * 6 = 102 bytes
|
||||
const WOL_HISTORY_MAX_ENTRIES: i64 = 50;
|
||||
|
||||
const MAGIC_PACKET_SIZE: usize = 102;
|
||||
|
||||
/// Parse MAC address string into bytes
|
||||
/// Supports formats: "AA:BB:CC:DD:EE:FF" or "AA-BB-CC-DD-EE-FF"
|
||||
fn parse_mac_address(mac: &str) -> Result<[u8; 6]> {
|
||||
let mac = mac.trim().to_uppercase();
|
||||
let parts: Vec<&str> = if mac.contains(':') {
|
||||
@@ -44,16 +40,13 @@ fn parse_mac_address(mac: &str) -> Result<[u8; 6]> {
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Build WOL magic packet
|
||||
fn build_magic_packet(mac: &[u8; 6]) -> [u8; MAGIC_PACKET_SIZE] {
|
||||
let mut packet = [0u8; MAGIC_PACKET_SIZE];
|
||||
|
||||
// First 6 bytes are 0xFF
|
||||
for byte in packet.iter_mut().take(6) {
|
||||
*byte = 0xFF;
|
||||
}
|
||||
|
||||
// Next 96 bytes are 16 repetitions of the MAC address
|
||||
for i in 0..16 {
|
||||
let offset = 6 + i * 6;
|
||||
packet[offset..offset + 6].copy_from_slice(mac);
|
||||
@@ -73,16 +66,13 @@ pub fn send_wol(mac_address: &str, interface: Option<&str>) -> Result<()> {
|
||||
|
||||
info!("Sending WOL packet to {} via {:?}", mac_address, interface);
|
||||
|
||||
// Create UDP socket
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")
|
||||
.map_err(|e| AppError::Internal(format!("Failed to create UDP socket: {}", e)))?;
|
||||
|
||||
// Enable broadcast
|
||||
socket
|
||||
.set_broadcast(true)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to enable broadcast: {}", e)))?;
|
||||
|
||||
// Bind to specific interface if specified
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(iface) = interface {
|
||||
if !iface.is_empty() {
|
||||
@@ -90,8 +80,7 @@ pub fn send_wol(mac_address: &str, interface: Option<&str>) -> Result<()> {
|
||||
let fd = socket.as_raw_fd();
|
||||
let iface_bytes = iface.as_bytes();
|
||||
|
||||
// SO_BINDTODEVICE requires interface name as null-terminated string
|
||||
let mut iface_buf = [0u8; 16]; // IFNAMSIZ is typically 16
|
||||
let mut iface_buf = [0u8; 16];
|
||||
let len = iface_bytes.len().min(15);
|
||||
iface_buf[..len].copy_from_slice(&iface_bytes[..len]);
|
||||
|
||||
@@ -112,18 +101,16 @@ pub fn send_wol(mac_address: &str, interface: Option<&str>) -> Result<()> {
|
||||
iface, err
|
||||
)));
|
||||
}
|
||||
debug!("Bound to interface: {}", iface);
|
||||
tracing::debug!("Bound to interface: {}", iface);
|
||||
}
|
||||
}
|
||||
|
||||
// Send to broadcast address on port 9 (discard protocol, commonly used for WOL)
|
||||
let broadcast_addr: SocketAddr = "255.255.255.255:9".parse().unwrap();
|
||||
|
||||
socket
|
||||
.send_to(&packet, broadcast_addr)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to send WOL packet: {}", e)))?;
|
||||
|
||||
// Also try sending to port 7 (echo protocol, alternative WOL port)
|
||||
let broadcast_addr_7: SocketAddr = "255.255.255.255:7".parse().unwrap();
|
||||
let _ = socket.send_to(&packet, broadcast_addr_7);
|
||||
|
||||
@@ -131,6 +118,55 @@ pub fn send_wol(mac_address: &str, interface: Option<&str>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn record_wol_history(pool: &sqlx::Pool<sqlx::Sqlite>, mac_address: &str) -> Result<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO wol_history (mac_address, updated_at)
|
||||
VALUES (?1, CAST(strftime('%s', 'now') AS INTEGER))
|
||||
ON CONFLICT(mac_address) DO UPDATE SET
|
||||
updated_at = excluded.updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(mac_address)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM wol_history
|
||||
WHERE mac_address NOT IN (
|
||||
SELECT mac_address FROM wol_history
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?1
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(WOL_HISTORY_MAX_ENTRIES)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_wol_history(
|
||||
pool: &sqlx::Pool<sqlx::Sqlite>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<(String, i64)>> {
|
||||
let rows = sqlx::query_as(
|
||||
r#"
|
||||
SELECT mac_address, updated_at
|
||||
FROM wol_history
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?1
|
||||
"#,
|
||||
)
|
||||
.bind(limit as i64)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -159,12 +195,10 @@ mod tests {
|
||||
let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
|
||||
let packet = build_magic_packet(&mac);
|
||||
|
||||
// Check header (6 bytes of 0xFF)
|
||||
for byte in packet.iter().take(6) {
|
||||
assert_eq!(*byte, 0xFF);
|
||||
}
|
||||
|
||||
// Check MAC repetitions
|
||||
for i in 0..16 {
|
||||
let offset = 6 + i * 6;
|
||||
assert_eq!(&packet[offset..offset + 6], &mac);
|
||||
|
||||
Reference in New Issue
Block a user