mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-17 16:36:44 +08:00
feat(atx): merge serial relay support from #223
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
//! Each executor handles one button (power or reset) with its own hardware binding.
|
||||
|
||||
use gpio_cdev::{Chip, LineHandle, LineRequestFlags};
|
||||
use serialport::SerialPort;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -38,6 +39,8 @@ pub struct AtxKeyExecutor {
|
||||
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
|
||||
serial_handle: Mutex<Option<Box<dyn SerialPort>>>,
|
||||
initialized: AtomicBool,
|
||||
}
|
||||
|
||||
@@ -48,6 +51,7 @@ impl AtxKeyExecutor {
|
||||
config,
|
||||
gpio_handle: Mutex::new(None),
|
||||
usb_relay_handle: Mutex::new(None),
|
||||
serial_handle: Mutex::new(None),
|
||||
initialized: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
@@ -72,6 +76,7 @@ impl AtxKeyExecutor {
|
||||
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 => {}
|
||||
}
|
||||
|
||||
@@ -134,6 +139,36 @@ impl AtxKeyExecutor {
|
||||
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 baud_rate = if self.config.baud_rate > 0 {
|
||||
self.config.baud_rate
|
||||
} else {
|
||||
9600
|
||||
};
|
||||
|
||||
let port = serialport::new(&self.config.device, baud_rate)
|
||||
.timeout(Duration::from_millis(100))
|
||||
.open()
|
||||
.map_err(|e| AppError::Internal(format!("Serial port open failed: {}", e)))?;
|
||||
|
||||
*self.serial_handle.lock().unwrap() = Some(port);
|
||||
|
||||
// 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() {
|
||||
@@ -147,6 +182,7 @@ impl AtxKeyExecutor {
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
@@ -220,6 +256,52 @@ impl AtxKeyExecutor {
|
||||
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<()> {
|
||||
// user config pin should be 1 for most LCUS modules (A0 01 01 A2)
|
||||
// if user set 0, it will send A0 00 01 A1 which might not work
|
||||
let channel = self.config.pin as u8;
|
||||
|
||||
// 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 mut guard = self.serial_handle.lock().unwrap();
|
||||
let port = guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| AppError::Internal("Serial relay not initialized".to_string()))?;
|
||||
|
||||
port.write_all(&cmd)
|
||||
.map_err(|e| AppError::Internal(format!("Serial relay write failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown the executor
|
||||
pub async fn shutdown(&mut self) -> Result<()> {
|
||||
if !self.is_initialized() {
|
||||
@@ -237,6 +319,12 @@ impl AtxKeyExecutor {
|
||||
// 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 => {}
|
||||
}
|
||||
|
||||
@@ -256,6 +344,12 @@ impl Drop for AtxKeyExecutor {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +372,7 @@ mod tests {
|
||||
device: "/dev/gpiochip0".to_string(),
|
||||
pin: 5,
|
||||
active_level: ActiveLevel::High,
|
||||
baud_rate: 9600,
|
||||
};
|
||||
let executor = AtxKeyExecutor::new(config);
|
||||
assert!(executor.is_configured());
|
||||
@@ -291,6 +386,20 @@ mod tests {
|
||||
device: "/dev/hidraw0".to_string(),
|
||||
pin: 0,
|
||||
active_level: ActiveLevel::High, // Ignored for USB relay
|
||||
baud_rate: 9600,
|
||||
};
|
||||
let executor = AtxKeyExecutor::new(config);
|
||||
assert!(executor.is_configured());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_with_serial_config() {
|
||||
let config = AtxKeyConfig {
|
||||
driver: AtxDriverType::Serial,
|
||||
device: "/dev/ttyUSB0".to_string(),
|
||||
pin: 1,
|
||||
active_level: ActiveLevel::High, // Ignored
|
||||
baud_rate: 9600,
|
||||
};
|
||||
let executor = AtxKeyExecutor::new(config);
|
||||
assert!(executor.is_configured());
|
||||
|
||||
@@ -28,12 +28,14 @@
|
||||
//! 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(),
|
||||
//! };
|
||||
@@ -72,12 +74,15 @@ pub fn discover_devices() -> AtxDevices {
|
||||
devices.gpio_chips.push(format!("/dev/{}", name_str));
|
||||
} else if name_str.starts_with("hidraw") {
|
||||
devices.usb_relays.push(format!("/dev/{}", name_str));
|
||||
} else if name_str.starts_with("ttyUSB") || name_str.starts_with("ttyACM") {
|
||||
devices.serial_ports.push(format!("/dev/{}", name_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
devices.gpio_chips.sort();
|
||||
devices.usb_relays.sort();
|
||||
devices.serial_ports.sort();
|
||||
|
||||
devices
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ pub enum AtxDriverType {
|
||||
Gpio,
|
||||
/// USB HID relay module
|
||||
UsbRelay,
|
||||
/// Serial/COM port relay (taobao LCUS type)
|
||||
Serial,
|
||||
/// Disabled / Not configured
|
||||
#[default]
|
||||
None,
|
||||
@@ -48,7 +50,7 @@ pub enum ActiveLevel {
|
||||
/// 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, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct AtxKeyConfig {
|
||||
/// Driver type (GPIO or USB Relay)
|
||||
@@ -60,9 +62,24 @@ pub struct AtxKeyConfig {
|
||||
/// Pin or channel number:
|
||||
/// - For GPIO: GPIO pin number
|
||||
/// - For USB Relay: relay channel (0-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
|
||||
pub baud_rate: u32,
|
||||
}
|
||||
|
||||
impl Default for AtxKeyConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
driver: AtxDriverType::None,
|
||||
device: String::new(),
|
||||
pin: 0,
|
||||
active_level: ActiveLevel::High,
|
||||
baud_rate: 9600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AtxKeyConfig {
|
||||
@@ -130,12 +147,24 @@ pub enum AtxAction {
|
||||
|
||||
/// Available ATX devices for discovery
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[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* or /dev/ttyACM*)
|
||||
pub serial_ports: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for AtxDevices {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
gpio_chips: Vec::new(),
|
||||
usb_relays: Vec::new(),
|
||||
serial_ports: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user