diff --git a/src/atx/hidraw_linux.rs b/src/atx/hidraw_linux.rs index b95f357a..3b53c8cc 100644 --- a/src/atx/hidraw_linux.rs +++ b/src/atx/hidraw_linux.rs @@ -1,7 +1,6 @@ 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; @@ -9,12 +8,10 @@ use tokio::time::sleep; use tracing::{debug, info}; use super::traits::AtxKeyBackend; -use super::types::AtxKeyConfig; +use super::types::{AtxKeyConfig, LCUS_RELAY_MAX_CHANNEL}; 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; +const LCUS_RELAY_COMMAND_LEN: usize = 4; pub struct HidrawLinuxRelayBackend { config: AtxKeyConfig, @@ -34,13 +31,13 @@ impl HidrawLinuxRelayBackend { fn validate_config(&self) -> Result<()> { if self.config.pin == 0 { return Err(AppError::Config( - "USB relay channel must be 1-based (>= 1)".to_string(), + "LCUS HID relay channel must be 1-based (>= 1)".to_string(), )); } - if self.config.pin > USB_RELAY_MAX_CHANNEL as u32 { + if self.config.pin > LCUS_RELAY_MAX_CHANNEL as u32 { return Err(AppError::Config(format!( - "USB HID relay channel must be <= {}", - USB_RELAY_MAX_CHANNEL + "LCUS HID relay channel must be <= {}", + LCUS_RELAY_MAX_CHANNEL ))); } Ok(()) @@ -49,20 +46,19 @@ impl HidrawLinuxRelayBackend { 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 + "LCUS HID relay channel {} exceeds max {}", + self.config.pin, LCUS_RELAY_MAX_CHANNEL )) })?; if channel == 0 { return Err(AppError::Config( - "USB relay channel must be 1-based (>= 1)".to_string(), + "LCUS HID relay channel must be 1-based (>= 1)".to_string(), )); } - if channel > USB_RELAY_MAX_CHANNEL { + if channel > LCUS_RELAY_MAX_CHANNEL { return Err(AppError::Config(format!( - "USB HID relay channel must be <= {}", - USB_RELAY_MAX_CHANNEL + "LCUS HID relay channel must be <= {}", + LCUS_RELAY_MAX_CHANNEL ))); } @@ -70,44 +66,22 @@ impl HidrawLinuxRelayBackend { let mut guard = self.handle.lock().unwrap(); let device = guard .as_mut() - .ok_or_else(|| AppError::Internal("USB relay not initialized".to_string()))?; + .ok_or_else(|| AppError::Internal("LCUS HID 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)))?; - } + device + .write_all(&cmd) + .map_err(|e| AppError::Internal(format!("LCUS HID relay write failed: {}", e)))?; + device + .flush() + .map_err(|e| AppError::Internal(format!("LCUS HID 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 as _, report.as_ptr()) }; - if rc < 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(()) - } + pub fn build_command(channel: u8, on: bool) -> [u8; LCUS_RELAY_COMMAND_LEN] { + let state = if on { 1 } else { 0 }; + let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state); + [0xA0, channel, state, checksum] } } @@ -117,7 +91,7 @@ impl AtxKeyBackend for HidrawLinuxRelayBackend { self.validate_config()?; info!( - "Initializing USB relay ATX backend on {} channel {}", + "Initializing LCUS HID relay ATX backend on {} channel {}", self.config.device, self.config.pin ); @@ -125,14 +99,14 @@ impl AtxKeyBackend for HidrawLinuxRelayBackend { .read(true) .write(true) .open(&self.config.device) - .map_err(|e| AppError::Internal(format!("USB relay device open failed: {}", e)))?; + .map_err(|e| AppError::Internal(format!("LCUS HID 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", + "LCUS HID relay channel {} configured successfully", self.config.pin ); Ok(()) @@ -140,7 +114,9 @@ impl AtxKeyBackend for HidrawLinuxRelayBackend { async fn pulse(&self, duration: Duration) -> Result<()> { if !self.is_initialized() { - return Err(AppError::Internal("USB relay not initialized".to_string())); + return Err(AppError::Internal( + "LCUS HID relay not initialized".to_string(), + )); } self.send_command(true)?; @@ -177,14 +153,14 @@ mod tests { use super::HidrawLinuxRelayBackend; #[test] - fn usb_relay_command_format() { + fn lcus_hid_relay_command_format() { assert_eq!( HidrawLinuxRelayBackend::build_command(1, true), - [0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + [0xA0, 0x01, 0x01, 0xA2] ); assert_eq!( HidrawLinuxRelayBackend::build_command(1, false), - [0x00, 0xFD, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + [0xA0, 0x01, 0x00, 0xA1] ); } } diff --git a/src/atx/mod.rs b/src/atx/mod.rs index 35e4a94a..3b51f8c4 100644 --- a/src/atx/mod.rs +++ b/src/atx/mod.rs @@ -25,21 +25,16 @@ pub use controller::{AtxController, AtxControllerConfig}; pub use executor::timing; pub use types::{ ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxInputBinding, AtxKeyConfig, - AtxOutputBinding, AtxPowerRequest, AtxState, HddStatus, PowerStatus, + AtxOutputBinding, AtxPowerRequest, AtxState, HddStatus, PowerStatus, LCUS_RELAY_MAX_CHANNEL, }; 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") - || upper.contains("00005131:00002007") - || upper.contains("16C0:05DF") + upper.contains("00005131:00002007") || upper.contains("5131:2007") - || upper.contains("PRODUCT=16C0/5DF") || upper.contains("PRODUCT=5131/2007") - || upper.contains("USBRELAY") - || upper.contains("USB RELAY") } #[cfg(unix)] @@ -94,14 +89,14 @@ mod tests { } #[test] - fn test_hidraw_uevent_detects_usb_relay_id() { - assert!(hidraw_uevent_is_usb_relay( + fn test_hidraw_uevent_rejects_non_lcus_usb_relay_id() { + assert!(!hidraw_uevent_is_usb_relay( "HID_ID=0003:000016C0:000005DF\nHID_NAME=www.dcttech.com USBRelay2\n" )); } #[test] - fn test_hidraw_uevent_detects_5131_usb_relay_id() { + fn test_hidraw_uevent_detects_lcus_hid_relay_id() { assert!(hidraw_uevent_is_usb_relay( "HID_ID=0003:00005131:00002007\n" )); diff --git a/src/atx/serial_relay.rs b/src/atx/serial_relay.rs index bdc23f7b..a21f5002 100644 --- a/src/atx/serial_relay.rs +++ b/src/atx/serial_relay.rs @@ -7,7 +7,7 @@ use tokio::time::sleep; use tracing::{debug, info}; use super::traits::{validate_serial_config, AtxKeyBackend, SharedSerialHandle}; -use super::types::AtxKeyConfig; +use super::types::{AtxKeyConfig, LCUS_RELAY_MAX_CHANNEL}; use crate::error::{AppError, Result}; pub struct SerialRelayBackend { @@ -44,15 +44,23 @@ impl SerialRelayBackend { 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 + "LCUS serial relay channel {} exceeds max {}", + self.config.pin, LCUS_RELAY_MAX_CHANNEL )) })?; + if channel == 0 { + return Err(AppError::Config( + "LCUS serial relay channel must be 1-based (>= 1)".to_string(), + )); + } + if channel > LCUS_RELAY_MAX_CHANNEL { + return Err(AppError::Config(format!( + "LCUS serial relay channel must be <= {}", + LCUS_RELAY_MAX_CHANNEL + ))); + } - let state = if on { 1 } else { 0 }; - let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state); - let cmd = [0xA0, channel, state, checksum]; + let cmd = Self::build_command(channel, on); let serial_handle = self .serial_handle @@ -60,16 +68,22 @@ impl SerialRelayBackend { .unwrap() .as_ref() .cloned() - .ok_or_else(|| AppError::Internal("Serial relay not initialized".to_string()))?; + .ok_or_else(|| AppError::Internal("LCUS 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)))?; + .map_err(|e| AppError::Internal(format!("LCUS serial relay write failed: {}", e)))?; port.flush() - .map_err(|e| AppError::Internal(format!("Serial relay flush failed: {}", e)))?; + .map_err(|e| AppError::Internal(format!("LCUS serial relay flush failed: {}", e)))?; Ok(()) } + + pub fn build_command(channel: u8, on: bool) -> [u8; 4] { + let state = if on { 1 } else { 0 }; + let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state); + [0xA0, channel, state, checksum] + } } #[async_trait] @@ -78,7 +92,7 @@ impl AtxKeyBackend for SerialRelayBackend { validate_serial_config(&self.config)?; info!( - "Initializing Serial relay ATX backend on {} channel {}", + "Initializing LCUS serial relay ATX backend on {} channel {}", self.config.device, self.config.pin ); @@ -92,7 +106,7 @@ impl AtxKeyBackend for SerialRelayBackend { self.initialized.store(true, Ordering::Relaxed); debug!( - "Serial relay channel {} configured successfully", + "LCUS serial relay channel {} configured successfully", self.config.pin ); Ok(()) @@ -101,12 +115,12 @@ impl AtxKeyBackend for SerialRelayBackend { async fn pulse(&self, duration: Duration) -> Result<()> { if !self.is_initialized() { return Err(AppError::Internal( - "Serial relay not initialized".to_string(), + "LCUS serial relay not initialized".to_string(), )); } info!( - "Pulse serial relay on {} pin {}", + "Pulse LCUS serial relay on {} channel {}", self.config.device, self.config.pin ); self.send_command(true)?; @@ -131,6 +145,23 @@ impl AtxKeyBackend for SerialRelayBackend { } } +#[cfg(test)] +mod tests { + use super::SerialRelayBackend; + + #[test] + fn lcus_serial_relay_command_format() { + assert_eq!( + SerialRelayBackend::build_command(1, true), + [0xA0, 0x01, 0x01, 0xA2] + ); + assert_eq!( + SerialRelayBackend::build_command(1, false), + [0xA0, 0x01, 0x00, 0xA1] + ); + } +} + impl Drop for SerialRelayBackend { fn drop(&mut self) { if self.is_initialized() { diff --git a/src/atx/traits.rs b/src/atx/traits.rs index d422b2a9..13e4bedd 100644 --- a/src/atx/traits.rs +++ b/src/atx/traits.rs @@ -3,7 +3,7 @@ use serialport::SerialPort; use std::sync::{Arc, Mutex}; use std::time::Duration; -use super::types::AtxKeyConfig; +use super::types::{AtxKeyConfig, LCUS_RELAY_MAX_CHANNEL}; use crate::error::Result; pub type SharedSerialHandle = Arc>>; @@ -36,10 +36,10 @@ pub fn validate_serial_config(config: &AtxKeyConfig) -> Result<()> { "Serial ATX channel must be 1-based (>= 1)".to_string(), )); } - if config.pin > u8::MAX as u32 { + if config.pin > LCUS_RELAY_MAX_CHANNEL as u32 { return Err(crate::error::AppError::Config(format!( - "Serial ATX channel must be <= {}", - u8::MAX + "LCUS serial relay channel must be <= {}", + LCUS_RELAY_MAX_CHANNEL ))); } if config.baud_rate == 0 { diff --git a/src/atx/types.rs b/src/atx/types.rs index c2c28588..306401bd 100644 --- a/src/atx/types.rs +++ b/src/atx/types.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; +pub const LCUS_RELAY_MAX_CHANNEL: u8 = 8; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum PowerStatus { diff --git a/src/web/handlers/config/types.rs b/src/web/handlers/config/types.rs index 5a0e7796..08330d71 100644 --- a/src/web/handlers/config/types.rs +++ b/src/web/handlers/config/types.rs @@ -608,19 +608,39 @@ impl AtxConfigUpdate { } } crate::atx::AtxDriverType::UsbRelay => { - Self::validate_device_path(&config.device, "ATX USB relay")?; - Self::validate_output_channel(&config.power, "power", 1, 8)?; - Self::validate_output_channel(&config.reset, "reset", 1, 8)?; + Self::validate_device_path(&config.device, "ATX LCUS HID relay")?; + Self::validate_output_channel( + &config.power, + "power", + 1, + crate::atx::LCUS_RELAY_MAX_CHANNEL as u32, + )?; + Self::validate_output_channel( + &config.reset, + "reset", + 1, + crate::atx::LCUS_RELAY_MAX_CHANNEL as u32, + )?; } crate::atx::AtxDriverType::Serial => { - Self::validate_device_path(&config.device, "ATX serial")?; + Self::validate_device_path(&config.device, "ATX LCUS serial relay")?; if config.baud_rate == 0 { return Err(AppError::BadRequest( "ATX baud_rate must be greater than 0".to_string(), )); } - Self::validate_output_channel(&config.power, "power", 1, u8::MAX as u32)?; - Self::validate_output_channel(&config.reset, "reset", 1, u8::MAX as u32)?; + Self::validate_output_channel( + &config.power, + "power", + 1, + crate::atx::LCUS_RELAY_MAX_CHANNEL as u32, + )?; + Self::validate_output_channel( + &config.reset, + "reset", + 1, + crate::atx::LCUS_RELAY_MAX_CHANNEL as u32, + )?; } crate::atx::AtxDriverType::None => {} }; @@ -1295,6 +1315,27 @@ mod tests { assert!(update.validate_with_current(¤t).is_err()); } + #[test] + fn test_atx_validate_with_current_rejects_lcus_serial_channel_over_8() { + let mut current = AtxConfig::default(); + current.enabled = true; + current.driver = crate::atx::AtxDriverType::Serial; + current.device = "/dev/null".to_string(); + current.power.enabled = true; + current.power.pin = 1; + current.baud_rate = 9600; + + let mut update = empty_atx_update(); + update.power = Some(AtxOutputBindingUpdate { + enabled: None, + device: None, + pin: Some(9), + active_level: None, + }); + + assert!(update.validate_with_current(¤t).is_err()); + } + #[test] fn test_atx_validate_with_current_rejects_enabled_none_driver() { let mut current = AtxConfig::default();