diff --git a/src/atx/controller.rs b/src/atx/controller.rs index 7ca143cd..c4b022f4 100644 --- a/src/atx/controller.rs +++ b/src/atx/controller.rs @@ -43,32 +43,93 @@ pub struct AtxController { } impl AtxController { - async fn init_components(inner: &mut AtxInner) { - // 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); - } - } + fn should_share_serial_device(power: &AtxKeyConfig, reset: &AtxKeyConfig) -> bool { + power.is_configured() + && reset.is_configured() + && power.driver == super::types::AtxDriverType::Serial + && reset.driver == super::types::AtxDriverType::Serial + && !power.device.is_empty() + && power.device == reset.device + && power.baud_rate == reset.baud_rate + } - // 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); + async fn init_components(inner: &mut AtxInner) { + if Self::should_share_serial_device(&inner.config.power, &inner.config.reset) { + match AtxKeyExecutor::open_shared_serial( + &inner.config.power.device, + 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 + ); + 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); + } + } + Err(e) => { + warn!( + "Failed to open shared serial device {} for ATX power/reset: {}", + inner.config.power.device, e + ); + } + } + } 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); + } } } @@ -262,3 +323,49 @@ impl AtxController { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::atx::AtxDriverType; + + #[test] + fn test_should_share_serial_device_true() { + let power = AtxKeyConfig { + driver: AtxDriverType::Serial, + device: "/dev/ttyUSB0".to_string(), + pin: 1, + active_level: super::super::types::ActiveLevel::High, + baud_rate: 9600, + }; + let reset = AtxKeyConfig { + driver: AtxDriverType::Serial, + device: "/dev/ttyUSB0".to_string(), + pin: 2, + active_level: super::super::types::ActiveLevel::High, + baud_rate: 9600, + }; + + assert!(AtxController::should_share_serial_device(&power, &reset)); + } + + #[test] + fn test_should_share_serial_device_false_on_different_baud() { + let power = AtxKeyConfig { + driver: AtxDriverType::Serial, + device: "/dev/ttyUSB0".to_string(), + pin: 1, + active_level: super::super::types::ActiveLevel::High, + baud_rate: 9600, + }; + let reset = AtxKeyConfig { + driver: AtxDriverType::Serial, + device: "/dev/ttyUSB0".to_string(), + pin: 2, + active_level: super::super::types::ActiveLevel::High, + baud_rate: 115200, + }; + + assert!(!AtxController::should_share_serial_device(&power, &reset)); + } +} diff --git a/src/atx/executor.rs b/src/atx/executor.rs index 430c015d..f2e4c41d 100644 --- a/src/atx/executor.rs +++ b/src/atx/executor.rs @@ -8,7 +8,7 @@ use serialport::SerialPort; use std::fs::{File, OpenOptions}; use std::io::Write; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::time::sleep; use tracing::{debug, info}; @@ -16,6 +16,8 @@ use tracing::{debug, info}; use super::types::{ActiveLevel, AtxDriverType, AtxKeyConfig}; use crate::error::{AppError, Result}; +pub type SharedSerialHandle = Arc>>; + /// Timing constants for ATX operations pub mod timing { use std::time::Duration; @@ -39,8 +41,8 @@ pub struct AtxKeyExecutor { gpio_handle: Mutex>, /// Cached USB relay file handle to avoid repeated open/close syscalls usb_relay_handle: Mutex>, - /// Cached Serial port handle - serial_handle: Mutex>>, + /// Cached Serial port handle (can be shared across power/reset executors) + serial_handle: Mutex>, initialized: AtomicBool, } @@ -56,6 +58,26 @@ impl AtxKeyExecutor { } } + /// 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), + } + } + + /// Open a serial relay device and wrap it for shared use. + pub fn open_shared_serial(device: &str, baud_rate: u32) -> Result { + 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))) + } + /// Check if this executor is configured pub fn is_configured(&self) -> bool { self.config.is_configured() @@ -181,14 +203,11 @@ impl AtxKeyExecutor { self.config.device, self.config.pin ); - let baud_rate = self.config.baud_rate; - - 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); + 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)?; @@ -337,10 +356,14 @@ impl AtxKeyExecutor { // OFF: A0 01 00 A1 let cmd = [0xA0, channel, state, checksum]; - let mut guard = self.serial_handle.lock().unwrap(); - let port = guard - .as_mut() + 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)))?;