From e60152d38b50db8ec67bfde40827429967a8b1a3 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 7 Jul 2026 00:21:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=20ATX=20=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E9=80=BB=E8=BE=91=E4=B8=8E=E6=8E=A7=E4=BB=B6=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/atx/controller.rs | 251 ++++++++++++---- src/atx/disabled_led.rs | 10 +- src/atx/led.rs | 72 +++-- src/atx/mod.rs | 7 +- src/atx/types.rs | 134 ++++++--- src/config/schema/atx.rs | 58 +++- src/config/schema/mod.rs | 1 + src/events/types.rs | 1 + src/platform/defaults.rs | 31 +- src/state.rs | 21 +- src/web/handlers/atx_api.rs | 28 +- src/web/handlers/config/atx.rs | 34 +-- src/web/handlers/config/types.rs | 460 +++++++++++++++-------------- src/web/handlers/system.rs | 5 +- web/src/api/index.ts | 2 + web/src/components/AtxPopover.vue | 190 ++++++------ web/src/i18n/en-US.ts | 31 +- web/src/i18n/zh-CN.ts | 31 +- web/src/stores/system.ts | 4 + web/src/types/generated.ts | 51 ++-- web/src/views/SettingsView.vue | 472 +++++++++++++++--------------- 21 files changed, 1058 insertions(+), 836 deletions(-) diff --git a/src/atx/controller.rs b/src/atx/controller.rs index e50ae2b4..6cdc8b18 100644 --- a/src/atx/controller.rs +++ b/src/atx/controller.rs @@ -8,15 +8,22 @@ use tracing::{debug, info, warn}; use super::executor::{timing, AtxKeyExecutor}; use super::led::LedSensor; -use super::types::{AtxAction, AtxKeyConfig, AtxLedConfig, AtxState, PowerStatus}; +use super::types::{ + AtxAction, AtxDriverType, AtxInputBinding, AtxKeyConfig, AtxOutputBinding, AtxState, HddStatus, + PowerStatus, +}; use crate::error::{AppError, Result}; #[derive(Debug, Clone, Default)] pub struct AtxControllerConfig { pub enabled: bool, - pub power: AtxKeyConfig, - pub reset: AtxKeyConfig, - pub led: AtxLedConfig, + pub driver: AtxDriverType, + pub device: String, + pub baud_rate: u32, + pub power: AtxOutputBinding, + pub reset: AtxOutputBinding, + pub led: AtxInputBinding, + pub hdd: AtxInputBinding, } /// Grouped together to reduce lock acquisitions @@ -25,6 +32,7 @@ struct AtxInner { power_executor: Option, reset_executor: Option, led_sensor: Option, + hdd_sensor: Option, } /// Manages ATX power control through independent executors for each action. @@ -34,14 +42,44 @@ pub struct AtxController { } impl AtxController { - 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 + fn build_key_config( + config: &AtxControllerConfig, + binding: &AtxOutputBinding, + ) -> Option { + if !binding.is_configured_for(config.driver, &config.device) { + return None; + } + + let device = match config.driver { + AtxDriverType::Gpio => binding.device.clone(), + AtxDriverType::UsbRelay | AtxDriverType::Serial => config.device.clone(), + AtxDriverType::None => return None, + }; + + Some(AtxKeyConfig { + driver: config.driver, + device, + pin: binding.pin, + active_level: binding.active_level, + baud_rate: config.baud_rate, + }) + } + + fn runtime_key_configs( + config: &AtxControllerConfig, + ) -> (Option, Option) { + ( + Self::build_key_config(config, &config.power), + Self::build_key_config(config, &config.reset), + ) + } + + fn should_share_serial_device(config: &AtxControllerConfig) -> bool { + if config.driver != AtxDriverType::Serial || config.device.trim().is_empty() { + return false; + } + + config.power.enabled && config.reset.enabled } async fn init_key_executor( @@ -63,75 +101,80 @@ impl AtxController { } 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, - ) { + let (power_config, reset_config) = Self::runtime_key_configs(&inner.config); + + if Self::should_share_serial_device(&inner.config) { + match AtxKeyExecutor::open_shared_serial(&inner.config.device, inner.config.baud_rate) { Ok(shared_serial) => { for (slot, warn_label, info_label, config, serial) in [ ( &mut inner.power_executor, "power", "Power", - inner.config.power.clone(), + power_config.clone(), shared_serial.clone(), ), ( &mut inner.reset_executor, "reset", "Reset", - inner.config.reset.clone(), + reset_config.clone(), shared_serial, ), ] { - let executor = - AtxKeyExecutor::new_with_shared_serial(config.clone(), serial); - *slot = - Self::init_key_executor(warn_label, info_label, config, executor).await; + if let Some(config) = config { + let executor = + AtxKeyExecutor::new_with_shared_serial(config.clone(), serial); + *slot = + Self::init_key_executor(warn_label, info_label, config, executor) + .await; + } } } Err(e) => { warn!( "Failed to open shared serial device {} for ATX power/reset: {}", - inner.config.power.device, e + inner.config.device, e ); } } } else { 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(), - ), + (&mut inner.power_executor, "power", "Power", power_config), + (&mut inner.reset_executor, "reset", "Reset", reset_config), ] { - if config.is_configured() { + if let Some(config) = config { let executor = AtxKeyExecutor::new(config.clone()); *slot = Self::init_key_executor(warn_label, info_label, config, executor).await; } } } - if inner.config.led.is_configured() { + if inner.config.driver == AtxDriverType::Gpio && inner.config.led.is_configured() { let mut sensor = LedSensor::new(inner.config.led.clone()); if let Err(e) = sensor.init().await { warn!("Failed to initialize LED sensor: {}", e); } else { info!( "LED sensor initialized on {} pin {}", - inner.config.led.gpio_chip, inner.config.led.gpio_pin + inner.config.led.device, inner.config.led.pin ); inner.led_sensor = Some(sensor); } } + + if inner.config.driver == AtxDriverType::Gpio && inner.config.hdd.is_configured() { + let mut sensor = LedSensor::new(inner.config.hdd.clone()); + if let Err(e) = sensor.init().await { + warn!("Failed to initialize HDD sensor: {}", e); + } else { + info!( + "HDD sensor initialized on {} pin {}", + inner.config.hdd.device, inner.config.hdd.pin + ); + inner.hdd_sensor = Some(sensor); + } + } } async fn shutdown_components(inner: &mut AtxInner) { @@ -153,6 +196,13 @@ impl AtxController { } } inner.led_sensor = None; + + if let Some(sensor) = inner.hdd_sensor.as_mut() { + if let Err(e) = sensor.shutdown().await { + warn!("Failed to shutdown HDD sensor: {}", e); + } + } + inner.hdd_sensor = None; } async fn read_power_status(sensor: Option<&LedSensor>) -> PowerStatus { @@ -169,6 +219,21 @@ impl AtxController { } } + async fn read_hdd_status(sensor: Option<&LedSensor>) -> HddStatus { + let Some(sensor) = sensor else { + return HddStatus::Unknown; + }; + + match sensor.read_active().await { + Ok(true) => HddStatus::Active, + Ok(false) => HddStatus::Inactive, + Err(e) => { + debug!("Failed to read ATX HDD sensor: {}", e); + HddStatus::Unknown + } + } + } + pub fn new(config: AtxControllerConfig) -> Self { Self { inner: RwLock::new(AtxInner { @@ -176,6 +241,7 @@ impl AtxController { power_executor: None, reset_executor: None, led_sensor: None, + hdd_sensor: None, }), } } @@ -270,13 +336,21 @@ impl AtxController { let inner = self.inner.read().await; let power_status = Self::read_power_status(inner.led_sensor.as_ref()).await; + let hdd_status = Self::read_hdd_status(inner.hdd_sensor.as_ref()).await; AtxState { available: inner.config.enabled, + driver: if inner.config.enabled { + inner.config.driver + } else { + AtxDriverType::None + }, power_configured: inner.power_executor.is_some(), reset_configured: inner.reset_executor.is_some(), power_status, led_supported: inner.led_sensor.is_some(), + hdd_status, + hdd_supported: inner.hdd_sensor.is_some(), } } } @@ -284,45 +358,96 @@ impl AtxController { #[cfg(test)] mod tests { use super::*; - use crate::atx::AtxDriverType; + use crate::atx::{AtxDriverType, AtxOutputBinding}; #[test] fn test_should_share_serial_device_true() { - let power = AtxKeyConfig { + let config = AtxControllerConfig { 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, + power: AtxOutputBinding { + enabled: true, + pin: 1, + ..Default::default() + }, + reset: AtxOutputBinding { + enabled: true, + pin: 2, + ..Default::default() + }, + ..Default::default() }; - assert!(AtxController::should_share_serial_device(&power, &reset)); + assert!(AtxController::should_share_serial_device(&config)); } #[test] - fn test_should_share_serial_device_false_on_different_baud() { - let power = AtxKeyConfig { + fn test_should_share_serial_device_false_when_reset_disabled() { + let config = AtxControllerConfig { 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, + power: AtxOutputBinding { + enabled: true, + pin: 1, + ..Default::default() + }, + reset: AtxOutputBinding { + enabled: false, + pin: 2, + ..Default::default() + }, + ..Default::default() }; - assert!(!AtxController::should_share_serial_device(&power, &reset)); + assert!(!AtxController::should_share_serial_device(&config)); + } + + #[test] + fn test_gpio_runtime_key_uses_binding_device() { + let config = AtxControllerConfig { + driver: AtxDriverType::Gpio, + device: "/dev/ignored".to_string(), + baud_rate: 9600, + power: AtxOutputBinding { + enabled: true, + device: "/dev/gpiochip1".to_string(), + pin: 4, + active_level: super::super::types::ActiveLevel::Low, + }, + ..Default::default() + }; + + let (power, reset) = AtxController::runtime_key_configs(&config); + let power = power.unwrap(); + assert!(reset.is_none()); + assert_eq!(power.driver, AtxDriverType::Gpio); + assert_eq!(power.device, "/dev/gpiochip1"); + assert_eq!(power.pin, 4); + assert_eq!(power.active_level, super::super::types::ActiveLevel::Low); + } + + #[test] + fn test_serial_runtime_key_uses_top_level_device() { + let config = AtxControllerConfig { + driver: AtxDriverType::Serial, + device: "/dev/ttyUSB0".to_string(), + baud_rate: 115200, + power: AtxOutputBinding { + enabled: true, + device: "/dev/ignored".to_string(), + pin: 3, + ..Default::default() + }, + ..Default::default() + }; + + let (power, _) = AtxController::runtime_key_configs(&config); + let power = power.unwrap(); + assert_eq!(power.driver, AtxDriverType::Serial); + assert_eq!(power.device, "/dev/ttyUSB0"); + assert_eq!(power.pin, 3); + assert_eq!(power.baud_rate, 115200); } } diff --git a/src/atx/disabled_led.rs b/src/atx/disabled_led.rs index 52ef9034..47708e12 100644 --- a/src/atx/disabled_led.rs +++ b/src/atx/disabled_led.rs @@ -1,14 +1,14 @@ #![allow(dead_code)] -use super::types::{AtxLedConfig, PowerStatus}; +use super::types::{AtxInputBinding, PowerStatus}; use crate::error::Result; pub struct LedSensor { - config: AtxLedConfig, + config: AtxInputBinding, } impl LedSensor { - pub fn new(config: AtxLedConfig) -> Self { + pub fn new(config: AtxInputBinding) -> Self { Self { config } } @@ -28,6 +28,10 @@ impl LedSensor { Ok(PowerStatus::Unknown) } + pub async fn read_active(&self) -> Result { + Ok(false) + } + pub async fn shutdown(&mut self) -> Result<()> { Ok(()) } diff --git a/src/atx/led.rs b/src/atx/led.rs index 0b4db12e..2d0c3c47 100644 --- a/src/atx/led.rs +++ b/src/atx/led.rs @@ -7,17 +7,17 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; use tracing::{debug, info}; -use super::types::{AtxLedConfig, PowerStatus}; +use super::types::{ActiveLevel, AtxInputBinding, PowerStatus}; use crate::error::{AppError, Result}; pub struct LedSensor { - config: AtxLedConfig, + config: AtxInputBinding, handle: Mutex>, initialized: AtomicBool, } impl LedSensor { - pub fn new(config: AtxLedConfig) -> Self { + pub fn new(config: AtxInputBinding) -> Self { Self { config, handle: Mutex::new(None), @@ -33,17 +33,14 @@ impl LedSensor { info!( "Initializing LED sensor on {} pin {}", - self.config.gpio_chip, self.config.gpio_pin + self.config.device, self.config.pin ); - let mut chip = Chip::new(&self.config.gpio_chip) + let mut chip = Chip::new(&self.config.device) .map_err(|e| AppError::Internal(format!("LED GPIO chip failed: {}", e)))?; - let line = chip.get_line(self.config.gpio_pin).map_err(|e| { - AppError::Internal(format!( - "LED GPIO line {} failed: {}", - self.config.gpio_pin, e - )) + let line = chip.get_line(self.config.pin).map_err(|e| { + AppError::Internal(format!("LED GPIO line {} failed: {}", self.config.pin, e)) })?; let handle = line @@ -57,9 +54,11 @@ impl LedSensor { Ok(()) } - pub async fn read(&self) -> Result { + pub async fn read_active(&self) -> Result { if !self.config.is_configured() || !self.initialized.load(Ordering::Relaxed) { - return Ok(PowerStatus::Unknown); + return Err(AppError::Internal( + "GPIO input sensor not initialized".to_string(), + )); } let guard = self.handle.lock().unwrap(); @@ -69,22 +68,31 @@ impl LedSensor { .get_value() .map_err(|e| AppError::Internal(format!("LED read failed: {}", e)))?; - let is_on = if self.config.inverted { - value == 0 - } else { - value == 1 + let active = match self.config.active_level { + ActiveLevel::High => value == 1, + ActiveLevel::Low => value == 0, }; - Ok(if is_on { - PowerStatus::On - } else { - PowerStatus::Off - }) + Ok(active) } - None => Ok(PowerStatus::Unknown), + None => Err(AppError::Internal( + "GPIO input sensor not initialized".to_string(), + )), } } + pub async fn read(&self) -> Result { + if !self.config.is_configured() || !self.initialized.load(Ordering::Relaxed) { + return Ok(PowerStatus::Unknown); + } + + Ok(if self.read_active().await? { + PowerStatus::On + } else { + PowerStatus::Off + }) + } + pub async fn shutdown(&mut self) -> Result<()> { *self.handle.lock().unwrap() = None; self.initialized.store(false, Ordering::Relaxed); @@ -105,7 +113,7 @@ mod tests { #[test] fn test_led_sensor_creation() { - let config = AtxLedConfig::default(); + let config = AtxInputBinding::default(); let sensor = LedSensor::new(config); assert!(!sensor.config.is_configured()); assert!(!sensor.initialized.load(Ordering::Relaxed)); @@ -113,11 +121,11 @@ mod tests { #[test] fn test_led_sensor_with_config() { - let config = AtxLedConfig { + let config = AtxInputBinding { enabled: true, - gpio_chip: "/dev/gpiochip0".to_string(), - gpio_pin: 7, - inverted: false, + device: "/dev/gpiochip0".to_string(), + pin: 7, + active_level: ActiveLevel::High, }; let sensor = LedSensor::new(config); assert!(sensor.config.is_configured()); @@ -126,13 +134,13 @@ mod tests { #[test] fn test_led_sensor_inverted_config() { - let config = AtxLedConfig { + let config = AtxInputBinding { enabled: true, - gpio_chip: "/dev/gpiochip0".to_string(), - gpio_pin: 7, - inverted: true, + device: "/dev/gpiochip0".to_string(), + pin: 7, + active_level: ActiveLevel::Low, }; let sensor = LedSensor::new(config); - assert!(sensor.config.inverted); + assert_eq!(sensor.config.active_level, ActiveLevel::Low); } } diff --git a/src/atx/mod.rs b/src/atx/mod.rs index a4124abf..35e4a94a 100644 --- a/src/atx/mod.rs +++ b/src/atx/mod.rs @@ -24,8 +24,8 @@ mod wol; pub use controller::{AtxController, AtxControllerConfig}; pub use executor::timing; pub use types::{ - ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxKeyConfig, AtxLedConfig, AtxPowerRequest, - AtxState, PowerStatus, + ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxInputBinding, AtxKeyConfig, + AtxOutputBinding, AtxPowerRequest, AtxState, HddStatus, PowerStatus, }; pub use wol::{list_wol_history, record_wol_history, send_wol}; @@ -120,7 +120,8 @@ mod tests { let _: AtxDriverType = AtxDriverType::None; let _: ActiveLevel = ActiveLevel::High; let _: AtxKeyConfig = AtxKeyConfig::default(); - let _: AtxLedConfig = AtxLedConfig::default(); + let _: AtxInputBinding = AtxInputBinding::default(); + let _: AtxOutputBinding = AtxOutputBinding::default(); let _: AtxState = AtxState::default(); let _: AtxDevices = AtxDevices::default(); } diff --git a/src/atx/types.rs b/src/atx/types.rs index ba353062..c2c28588 100644 --- a/src/atx/types.rs +++ b/src/atx/types.rs @@ -1,7 +1,6 @@ //! ATX data types and structures //! -//! Defines the configuration and state types for the flexible ATX power control system. -//! Each ATX action (power, reset) can be independently configured with different hardware. +//! Defines the configuration and state types for the ATX power control system. use serde::{Deserialize, Serialize}; use typeshare::typeshare; @@ -15,6 +14,15 @@ pub enum PowerStatus { Unknown, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum HddStatus { + Active, + Inactive, + #[default] + Unknown, +} + #[typeshare] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] @@ -36,6 +44,56 @@ pub enum ActiveLevel { } #[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(default)] +pub struct AtxOutputBinding { + pub enabled: bool, + pub device: String, + pub pin: u32, + pub active_level: ActiveLevel, +} + +impl Default for AtxOutputBinding { + fn default() -> Self { + Self { + enabled: false, + device: String::new(), + pin: 1, + active_level: ActiveLevel::High, + } + } +} + +impl AtxOutputBinding { + pub fn is_configured_for(&self, driver: AtxDriverType, top_level_device: &str) -> bool { + if !self.enabled || driver == AtxDriverType::None { + return false; + } + + match driver { + AtxDriverType::Gpio => !self.device.trim().is_empty(), + AtxDriverType::UsbRelay | AtxDriverType::Serial => !top_level_device.trim().is_empty(), + AtxDriverType::None => false, + } + } +} + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(default)] +pub struct AtxInputBinding { + pub enabled: bool, + pub device: String, + pub pin: u32, + pub active_level: ActiveLevel, +} + +impl AtxInputBinding { + pub fn is_configured(&self) -> bool { + self.enabled && !self.device.trim().is_empty() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(default)] pub struct AtxKeyConfig { @@ -64,29 +122,16 @@ impl AtxKeyConfig { } } -#[typeshare] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] -#[serde(default)] -pub struct AtxLedConfig { - pub enabled: bool, - pub gpio_chip: String, - pub gpio_pin: u32, - pub inverted: bool, -} - -impl AtxLedConfig { - pub fn is_configured(&self) -> bool { - self.enabled && !self.gpio_chip.is_empty() - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AtxState { pub available: bool, + pub driver: AtxDriverType, pub power_configured: bool, pub reset_configured: bool, pub power_status: PowerStatus, pub led_supported: bool, + pub hdd_status: HddStatus, + pub hdd_supported: bool, } #[derive(Debug, Clone, Deserialize)] @@ -139,6 +184,29 @@ mod tests { assert_eq!(ActiveLevel::default(), ActiveLevel::High); } + #[test] + fn test_atx_output_binding_default() { + let config = AtxOutputBinding::default(); + assert!(!config.enabled); + assert!(config.device.is_empty()); + assert_eq!(config.pin, 1); + assert!(!config.is_configured_for(AtxDriverType::Gpio, "")); + } + + #[test] + fn test_atx_output_binding_is_configured() { + let mut config = AtxOutputBinding::default(); + assert!(!config.is_configured_for(AtxDriverType::Gpio, "")); + + config.enabled = true; + assert!(!config.is_configured_for(AtxDriverType::Gpio, "")); + + config.device = "/dev/gpiochip0".to_string(); + assert!(config.is_configured_for(AtxDriverType::Gpio, "")); + assert!(!config.is_configured_for(AtxDriverType::Serial, "")); + assert!(config.is_configured_for(AtxDriverType::Serial, "/dev/ttyUSB0")); + } + #[test] fn test_atx_key_config_default() { let config = AtxKeyConfig::default(); @@ -149,37 +217,22 @@ mod tests { } #[test] - fn test_atx_key_config_is_configured() { - let mut config = AtxKeyConfig::default(); - assert!(!config.is_configured()); - - config.driver = AtxDriverType::Gpio; - assert!(!config.is_configured()); - - config.device = "/dev/gpiochip0".to_string(); - assert!(config.is_configured()); - - config.driver = AtxDriverType::None; - assert!(!config.is_configured()); - } - - #[test] - fn test_atx_led_config_default() { - let config = AtxLedConfig::default(); + fn test_atx_input_binding_default() { + let config = AtxInputBinding::default(); assert!(!config.enabled); - assert!(config.gpio_chip.is_empty()); + assert!(config.device.is_empty()); assert!(!config.is_configured()); } #[test] - fn test_atx_led_config_is_configured() { - let mut config = AtxLedConfig::default(); + fn test_atx_input_binding_is_configured() { + let mut config = AtxInputBinding::default(); assert!(!config.is_configured()); config.enabled = true; assert!(!config.is_configured()); - config.gpio_chip = "/dev/gpiochip0".to_string(); + config.device = "/dev/gpiochip0".to_string(); assert!(config.is_configured()); } @@ -187,9 +240,12 @@ mod tests { fn test_atx_state_default() { let state = AtxState::default(); assert!(!state.available); + assert_eq!(state.driver, AtxDriverType::None); assert!(!state.power_configured); assert!(!state.reset_configured); assert_eq!(state.power_status, PowerStatus::Unknown); assert!(!state.led_supported); + assert_eq!(state.hdd_status, HddStatus::Unknown); + assert!(!state.hdd_supported); } } diff --git a/src/config/schema/atx.rs b/src/config/schema/atx.rs index e7c7c5f1..92d9dd18 100644 --- a/src/config/schema/atx.rs +++ b/src/config/schema/atx.rs @@ -1,27 +1,61 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; -pub use crate::atx::{ActiveLevel, AtxDriverType, AtxKeyConfig, AtxLedConfig}; +pub use crate::atx::{ActiveLevel, AtxDriverType, AtxInputBinding, AtxOutputBinding}; #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] -#[derive(Default)] pub struct AtxConfig { pub enabled: bool, - pub power: AtxKeyConfig, - pub reset: AtxKeyConfig, - pub led: AtxLedConfig, + pub driver: AtxDriverType, + pub device: String, + pub baud_rate: u32, + pub power: AtxOutputBinding, + pub reset: AtxOutputBinding, + pub led: AtxInputBinding, + pub hdd: AtxInputBinding, pub wol_interface: String, } -impl AtxConfig { - pub fn to_controller_config(&self) -> crate::atx::AtxControllerConfig { - crate::atx::AtxControllerConfig { - enabled: self.enabled, - power: self.power.clone(), - reset: self.reset.clone(), - led: self.led.clone(), +impl Default for AtxConfig { + fn default() -> Self { + Self { + enabled: false, + driver: AtxDriverType::None, + device: String::new(), + baud_rate: 9600, + power: AtxOutputBinding::default(), + reset: AtxOutputBinding::default(), + led: AtxInputBinding::default(), + hdd: AtxInputBinding::default(), + wol_interface: String::new(), + } + } +} + +impl AtxConfig { + pub fn normalize(&mut self) { + if self.driver == AtxDriverType::None { + self.enabled = false; + } + + if self.driver != AtxDriverType::Gpio { + self.led.enabled = false; + self.hdd.enabled = false; + } + } + + pub fn to_controller_config(&self) -> crate::atx::AtxControllerConfig { + crate::atx::AtxControllerConfig { + enabled: self.enabled, + driver: self.driver, + device: self.device.clone(), + baud_rate: self.baud_rate, + power: self.power.clone(), + reset: self.reset.clone(), + led: self.led.clone(), + hdd: self.hdd.clone(), } } } diff --git a/src/config/schema/mod.rs b/src/config/schema/mod.rs index b922c276..2eda5a78 100644 --- a/src/config/schema/mod.rs +++ b/src/config/schema/mod.rs @@ -45,6 +45,7 @@ impl AppConfig { if self.hid.backend != HidBackend::Otg { self.msd.enabled = false; } + self.atx.normalize(); } pub fn apply_platform_defaults(&mut self) { diff --git a/src/events/types.rs b/src/events/types.rs index 18ff0815..1b320d53 100644 --- a/src/events/types.rs +++ b/src/events/types.rs @@ -54,6 +54,7 @@ pub struct AtxDeviceInfo { pub backend: String, pub initialized: bool, pub power_on: bool, + pub hdd_status: String, pub error: Option, } diff --git a/src/platform/defaults.rs b/src/platform/defaults.rs index 0741b691..ba951cf9 100644 --- a/src/platform/defaults.rs +++ b/src/platform/defaults.rs @@ -83,34 +83,23 @@ fn apply_windows(config: &mut AppConfig) { } if matches!( - config.atx.power.driver, + config.atx.driver, AtxDriverType::Gpio | AtxDriverType::UsbRelay ) { - config.atx.power.driver = AtxDriverType::None; - } - if matches!( - config.atx.reset.driver, - AtxDriverType::Gpio | AtxDriverType::UsbRelay - ) { - config.atx.reset.driver = AtxDriverType::None; + config.atx.driver = AtxDriverType::None; + config.atx.enabled = false; } if !config.initialized - && config.atx.power.driver == AtxDriverType::None - && config.atx.power.device.is_empty() + && config.atx.driver == AtxDriverType::None + && config.atx.device.is_empty() { - config.atx.power.driver = AtxDriverType::Serial; - config.atx.power.device = "COM4".to_string(); + config.atx.driver = AtxDriverType::Serial; + config.atx.device = "COM4".to_string(); + config.atx.baud_rate = 9600; + config.atx.power.enabled = true; config.atx.power.pin = 1; - config.atx.power.baud_rate = 9600; - } - if !config.initialized - && config.atx.reset.driver == AtxDriverType::None - && config.atx.reset.device.is_empty() - { - config.atx.reset.driver = AtxDriverType::Serial; - config.atx.reset.device = "COM4".to_string(); + config.atx.reset.enabled = true; config.atx.reset.pin = 2; - config.atx.reset.baud_rate = 9600; } config diff --git a/src/state.rs b/src/state.rs index 500aed53..bc8346ef 100644 --- a/src/state.rs +++ b/src/state.rs @@ -247,26 +247,27 @@ impl AppState { } async fn collect_atx_info(&self) -> Option { - const BACKEND_POWER_ONLY: &str = "power: configured, reset: none"; - const BACKEND_RESET_ONLY: &str = "power: none, reset: configured"; - const BACKEND_BOTH: &str = "power: configured, reset: configured"; - const BACKEND_NONE: &str = "none"; - let atx_guard = self.atx.read().await; let atx = atx_guard.as_ref()?; let state = atx.state().await; Some(AtxDeviceInfo { available: state.available, - backend: match (state.power_configured, state.reset_configured) { - (true, true) => BACKEND_BOTH, - (true, false) => BACKEND_POWER_ONLY, - (false, true) => BACKEND_RESET_ONLY, - (false, false) => BACKEND_NONE, + backend: match state.driver { + crate::atx::AtxDriverType::Gpio => "gpio", + crate::atx::AtxDriverType::UsbRelay => "usbrelay", + crate::atx::AtxDriverType::Serial => "serial", + crate::atx::AtxDriverType::None => "none", } .to_string(), initialized: state.power_configured || state.reset_configured, power_on: state.power_status == crate::atx::PowerStatus::On, + hdd_status: match state.hdd_status { + crate::atx::HddStatus::Active => "active", + crate::atx::HddStatus::Inactive => "inactive", + crate::atx::HddStatus::Unknown => "unknown", + } + .to_string(), error: None, }) } diff --git a/src/web/handlers/atx_api.rs b/src/web/handlers/atx_api.rs index 5dc6869f..d439d0c3 100644 --- a/src/web/handlers/atx_api.rs +++ b/src/web/handlers/atx_api.rs @@ -1,6 +1,6 @@ use super::*; -use crate::atx::{AtxState, PowerStatus}; +use crate::atx::{AtxDriverType, AtxState, HddStatus, PowerStatus}; const WOL_HISTORY_DEFAULT_LIMIT: usize = 5; const WOL_HISTORY_MAX_LIMIT: usize = 50; @@ -13,21 +13,21 @@ pub struct AtxStateResponse { pub initialized: bool, pub power_status: String, pub led_supported: bool, + pub hdd_status: String, + pub hdd_supported: bool, } impl From for AtxStateResponse { fn from(state: AtxState) -> Self { Self { available: state.available, - backend: if state.power_configured || state.reset_configured { - format!( - "power: {}, reset: {}", - if state.power_configured { "yes" } else { "no" }, - if state.reset_configured { "yes" } else { "no" } - ) - } else { - "none".to_string() - }, + backend: match state.driver { + AtxDriverType::Gpio => "gpio", + AtxDriverType::UsbRelay => "usbrelay", + AtxDriverType::Serial => "serial", + AtxDriverType::None => "none", + } + .to_string(), initialized: state.power_configured || state.reset_configured, power_status: match state.power_status { PowerStatus::On => "on".to_string(), @@ -35,6 +35,12 @@ impl From for AtxStateResponse { PowerStatus::Unknown => "unknown".to_string(), }, led_supported: state.led_supported, + hdd_status: match state.hdd_status { + HddStatus::Active => "active".to_string(), + HddStatus::Inactive => "inactive".to_string(), + HddStatus::Unknown => "unknown".to_string(), + }, + hdd_supported: state.hdd_supported, } } } @@ -54,6 +60,8 @@ pub async fn atx_status(State(state): State>) -> Result Result<()> { return Ok(()); } - for (name, key) in [("power", &atx.power), ("reset", &atx.reset)] { - if !matches!(key.driver, AtxDriverType::Serial | AtxDriverType::None) { - return Err(AppError::BadRequest(format!( - "Windows ATX {} only supports serial relay or none", - name - ))); - } + if !matches!(atx.driver, AtxDriverType::Serial | AtxDriverType::None) { + return Err(AppError::BadRequest( + "Windows ATX only supports serial relay or none".to_string(), + )); } Ok(()) @@ -68,13 +65,11 @@ fn validate_serial_device_conflict(atx: &AtxConfig, hid: &HidConfig) -> Result<( return Ok(()); } - for (name, key) in [("power", &atx.power), ("reset", &atx.reset)] { - if key.driver == AtxDriverType::Serial && key.device.trim() == reserved { - return Err(AppError::BadRequest(format!( - "ATX {} serial device '{}' conflicts with HID CH9329 serial device", - name, reserved - ))); - } + if atx.driver == AtxDriverType::Serial && atx.device.trim() == reserved { + return Err(AppError::BadRequest(format!( + "ATX serial device '{}' conflicts with HID CH9329 serial device", + reserved + ))); } Ok(()) @@ -87,8 +82,8 @@ mod tests { #[test] fn test_validate_serial_device_conflict_rejects_ch9329_overlap() { let mut atx = AtxConfig::default(); - atx.power.driver = AtxDriverType::Serial; - atx.power.device = "/dev/ttyUSB0".to_string(); + atx.driver = AtxDriverType::Serial; + atx.device = "/dev/ttyUSB0".to_string(); let mut hid = HidConfig::default(); hid.backend = HidBackend::Ch9329; @@ -100,8 +95,8 @@ mod tests { #[test] fn test_validate_serial_device_conflict_allows_non_ch9329_backend() { let mut atx = AtxConfig::default(); - atx.power.driver = AtxDriverType::Serial; - atx.power.device = "/dev/ttyUSB0".to_string(); + atx.driver = AtxDriverType::Serial; + atx.device = "/dev/ttyUSB0".to_string(); let mut hid = HidConfig::default(); hid.backend = HidBackend::None; @@ -113,8 +108,7 @@ mod tests { #[test] fn test_validate_windows_atx_backends_allows_serial() { let mut atx = AtxConfig::default(); - atx.power.driver = AtxDriverType::Serial; - atx.reset.driver = AtxDriverType::None; + atx.driver = AtxDriverType::Serial; assert!(validate_windows_atx_backends(&atx).is_ok()); } diff --git a/src/web/handlers/config/types.rs b/src/web/handlers/config/types.rs index b4ac5546..5a0e7796 100644 --- a/src/web/handlers/config/types.rs +++ b/src/web/handlers/config/types.rs @@ -453,25 +453,24 @@ impl MsdConfigUpdate { } } -/// Update for a single ATX key configuration +/// Update for a single ATX output binding #[typeshare] #[derive(Debug, Deserialize)] -pub struct AtxKeyConfigUpdate { - pub driver: Option, +pub struct AtxOutputBindingUpdate { + pub enabled: Option, pub device: Option, - pub baud_rate: Option, pub pin: Option, pub active_level: Option, } -/// Update for LED sensing configuration +/// Update for ATX GPIO input sensing #[typeshare] #[derive(Debug, Deserialize)] -pub struct AtxLedConfigUpdate { +pub struct AtxInputBindingUpdate { pub enabled: Option, - pub gpio_chip: Option, - pub gpio_pin: Option, - pub inverted: Option, + pub device: Option, + pub pin: Option, + pub active_level: Option, } /// ATX configuration update request @@ -479,26 +478,46 @@ pub struct AtxLedConfigUpdate { #[derive(Debug, Deserialize)] pub struct AtxConfigUpdate { pub enabled: Option, + pub driver: Option, + pub device: Option, + pub baud_rate: Option, /// Power button configuration - pub power: Option, + pub power: Option, /// Reset button configuration - pub reset: Option, + pub reset: Option, /// LED sensing configuration - pub led: Option, + pub led: Option, + /// HDD activity sensing configuration + pub hdd: Option, /// Network interface for WOL packets (empty = auto) pub wol_interface: Option, } impl AtxConfigUpdate { pub fn validate(&self) -> crate::error::Result<()> { - // Validate power key config if present - if let Some(ref power) = self.power { - Self::validate_key_config(power, "power")?; + if let Some(baud_rate) = self.baud_rate { + if baud_rate == 0 { + return Err(AppError::BadRequest( + "ATX baud_rate must be greater than 0".to_string(), + )); + } } - // Validate reset key config if present - if let Some(ref reset) = self.reset { - Self::validate_key_config(reset, "reset")?; + + for (name, binding) in [ + ("power", self.power.as_ref()), + ("reset", self.reset.as_ref()), + ] { + if let Some(binding) = binding { + Self::validate_output_binding_update(binding, name)?; + } } + + for (name, binding) in [("led", self.led.as_ref()), ("hdd", self.hdd.as_ref())] { + if let Some(binding) = binding { + Self::validate_input_binding_update(binding, name)?; + } + } + Ok(()) } @@ -509,152 +528,123 @@ impl AtxConfigUpdate { let mut merged = current.clone(); self.apply_to(&mut merged); - Self::validate_effective_key_config(&merged.power, "power")?; - Self::validate_effective_key_config(&merged.reset, "reset")?; - Self::validate_shared_serial_baud_rate(&merged)?; + if merged.driver == crate::atx::AtxDriverType::None { + if merged.enabled { + return Err(AppError::BadRequest( + "ATX driver must not be none when ATX is enabled".to_string(), + )); + } + return Ok(()); + } + + merged.normalize(); + + if !merged.enabled { + return Ok(()); + } + + Self::validate_effective_config(&merged)?; Ok(()) } - fn validate_shared_serial_baud_rate(config: &AtxConfig) -> crate::error::Result<()> { - let power = &config.power; - let reset = &config.reset; - let same_serial_device = power.driver == crate::atx::AtxDriverType::Serial - && reset.driver == crate::atx::AtxDriverType::Serial - && !power.device.trim().is_empty() - && power.device.trim() == reset.device.trim(); + fn validate_device_path(device: &str, name: &str) -> crate::error::Result<()> { + let trimmed = device.trim(); + if trimmed.is_empty() { + return Err(AppError::BadRequest(format!( + "{} device cannot be empty", + name + ))); + } - if same_serial_device && power.baud_rate != reset.baud_rate { - return Err(AppError::BadRequest( - "ATX power/reset sharing the same serial relay device must use one baud_rate" - .to_string(), - )); + if !cfg!(windows) && !std::path::Path::new(trimmed).exists() { + return Err(AppError::BadRequest(format!( + "{} device '{}' does not exist", + name, trimmed + ))); } Ok(()) } - fn validate_key_config(key: &AtxKeyConfigUpdate, name: &str) -> crate::error::Result<()> { - if let Some(ref device) = key.device { - if !device.trim().is_empty() && !cfg!(windows) && !std::path::Path::new(device).exists() - { - return Err(AppError::BadRequest(format!( - "{} device '{}' does not exist", - name, device - ))); - } - } - if let Some(baud_rate) = key.baud_rate { - if baud_rate == 0 { - return Err(AppError::BadRequest(format!( - "{} baud_rate must be greater than 0", - name - ))); - } - } - - if let Some(driver) = key.driver { - match driver { - crate::atx::AtxDriverType::Serial => { - if let Some(pin) = key.pin { - if pin == 0 { - return Err(AppError::BadRequest(format!( - "{} serial channel must be 1-based (>= 1)", - name - ))); - } - if pin > u8::MAX as u32 { - return Err(AppError::BadRequest(format!( - "{} serial channel must be <= {}", - name, - u8::MAX - ))); - } - } - } - crate::atx::AtxDriverType::UsbRelay => { - if let Some(pin) = key.pin { - if pin == 0 { - return Err(AppError::BadRequest(format!( - "{} USB relay channel must be 1-based (>= 1)", - name - ))); - } - if pin > u8::MAX as u32 { - return Err(AppError::BadRequest(format!( - "{} USB relay channel must be <= {}", - name, - u8::MAX - ))); - } - if pin > 8 { - return Err(AppError::BadRequest(format!( - "{} USB HID relay channel must be <= 8", - name - ))); - } - } - } - crate::atx::AtxDriverType::Gpio | crate::atx::AtxDriverType::None => {} - } - } - Ok(()) - } - - fn validate_effective_key_config( - key: &crate::atx::AtxKeyConfig, + fn validate_output_binding_update( + binding: &AtxOutputBindingUpdate, name: &str, ) -> crate::error::Result<()> { - match key.driver { - crate::atx::AtxDriverType::Serial => { - if key.device.trim().is_empty() { - return Err(AppError::BadRequest(format!( - "{} serial device cannot be empty", - name - ))); + if let Some(ref device) = binding.device { + if !device.trim().is_empty() { + Self::validate_device_path(device, name)?; + } + } + + Ok(()) + } + + fn validate_input_binding_update( + binding: &AtxInputBindingUpdate, + name: &str, + ) -> crate::error::Result<()> { + if let Some(ref device) = binding.device { + if !device.trim().is_empty() { + Self::validate_device_path(device, name)?; + } + } + + Ok(()) + } + + fn validate_effective_config(config: &AtxConfig) -> crate::error::Result<()> { + match config.driver { + crate::atx::AtxDriverType::Gpio => { + for (name, binding) in [("power", &config.power), ("reset", &config.reset)] { + if binding.enabled { + Self::validate_device_path(&binding.device, name)?; + } } - if key.pin == 0 { - return Err(AppError::BadRequest(format!( - "{} serial channel must be 1-based (>= 1)", - name - ))); - } - if key.pin > u8::MAX as u32 { - return Err(AppError::BadRequest(format!( - "{} serial channel must be <= {}", - name, - u8::MAX - ))); - } - if key.baud_rate == 0 { - return Err(AppError::BadRequest(format!( - "{} baud_rate must be greater than 0", - name - ))); + + for (name, binding) in [("led", &config.led), ("hdd", &config.hdd)] { + if binding.enabled { + Self::validate_device_path(&binding.device, name)?; + } } } crate::atx::AtxDriverType::UsbRelay => { - if key.pin == 0 { - return Err(AppError::BadRequest(format!( - "{} USB relay channel must be 1-based (>= 1)", - name - ))); - } - if key.pin > u8::MAX as u32 { - return Err(AppError::BadRequest(format!( - "{} USB relay channel must be <= {}", - name, - u8::MAX - ))); - } - if key.pin > 8 { - return Err(AppError::BadRequest(format!( - "{} USB HID relay channel must be <= 8", - name - ))); - } + 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)?; } - crate::atx::AtxDriverType::Gpio | crate::atx::AtxDriverType::None => {} + crate::atx::AtxDriverType::Serial => { + Self::validate_device_path(&config.device, "ATX serial")?; + 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)?; + } + crate::atx::AtxDriverType::None => {} + }; + + Ok(()) + } + + fn validate_output_channel( + binding: &crate::atx::AtxOutputBinding, + name: &str, + min: u32, + max: u32, + ) -> crate::error::Result<()> { + if !binding.enabled { + return Ok(()); } + + if binding.pin < min || binding.pin > max { + return Err(AppError::BadRequest(format!( + "{} channel must be between {} and {}", + name, min, max + ))); + } + Ok(()) } @@ -662,50 +652,65 @@ impl AtxConfigUpdate { if let Some(enabled) = self.enabled { config.enabled = enabled; } + if let Some(driver) = self.driver { + config.driver = driver; + } + if let Some(ref device) = self.device { + config.device = device.clone(); + } + if let Some(baud_rate) = self.baud_rate { + config.baud_rate = baud_rate; + } if let Some(ref power) = self.power { - Self::apply_key_update(power, &mut config.power); + Self::apply_output_binding_update(power, &mut config.power); } if let Some(ref reset) = self.reset { - Self::apply_key_update(reset, &mut config.reset); + Self::apply_output_binding_update(reset, &mut config.reset); } if let Some(ref led) = self.led { - Self::apply_led_update(led, &mut config.led); + Self::apply_input_binding_update(led, &mut config.led); + } + if let Some(ref hdd) = self.hdd { + Self::apply_input_binding_update(hdd, &mut config.hdd); } if let Some(ref wol_interface) = self.wol_interface { config.wol_interface = wol_interface.clone(); } } - fn apply_key_update(update: &AtxKeyConfigUpdate, config: &mut crate::atx::AtxKeyConfig) { - if let Some(driver) = update.driver { - config.driver = driver; + fn apply_output_binding_update( + update: &AtxOutputBindingUpdate, + config: &mut crate::atx::AtxOutputBinding, + ) { + if let Some(enabled) = update.enabled { + config.enabled = enabled; } if let Some(ref device) = update.device { config.device = device.clone(); } - if let Some(baud_rate) = update.baud_rate { - config.baud_rate = baud_rate; + if let Some(pin) = update.pin { + config.pin = pin; + } + if let Some(active_level) = update.active_level { + config.active_level = active_level; + } + } + + fn apply_input_binding_update( + update: &AtxInputBindingUpdate, + config: &mut crate::atx::AtxInputBinding, + ) { + if let Some(enabled) = update.enabled { + config.enabled = enabled; + } + if let Some(ref device) = update.device { + config.device = device.clone(); } if let Some(pin) = update.pin { config.pin = pin; } - if let Some(level) = update.active_level { - config.active_level = level; - } - } - - fn apply_led_update(update: &AtxLedConfigUpdate, config: &mut crate::atx::AtxLedConfig) { - if let Some(enabled) = update.enabled { - config.enabled = enabled; - } - if let Some(ref chip) = update.gpio_chip { - config.gpio_chip = chip.clone(); - } - if let Some(pin) = update.gpio_pin { - config.gpio_pin = pin; - } - if let Some(inverted) = update.inverted { - config.inverted = inverted; + if let Some(active_level) = update.active_level { + config.active_level = active_level; } } } @@ -1233,77 +1238,90 @@ impl RedfishConfigUpdate { mod tests { use super::*; - #[test] - fn test_atx_apply_key_update_applies_baud_rate() { - let mut config = AtxConfig::default(); - let update = AtxConfigUpdate { + fn empty_atx_update() -> AtxConfigUpdate { + AtxConfigUpdate { enabled: None, - power: Some(AtxKeyConfigUpdate { - driver: Some(crate::atx::AtxDriverType::Serial), - device: Some("/dev/null".to_string()), - baud_rate: Some(19200), - pin: Some(1), - active_level: None, - }), + driver: None, + device: None, + baud_rate: None, + power: None, reset: None, led: None, + hdd: None, wol_interface: None, - }; + } + } + + #[test] + fn test_atx_apply_update_applies_global_baud_rate() { + let mut config = AtxConfig::default(); + let mut update = empty_atx_update(); + update.driver = Some(crate::atx::AtxDriverType::Serial); + update.device = Some("/dev/null".to_string()); + update.baud_rate = Some(19200); + update.power = Some(AtxOutputBindingUpdate { + enabled: Some(true), + device: Some("/dev/ignored".to_string()), + pin: Some(1), + active_level: None, + }); update.apply_to(&mut config); - assert_eq!(config.power.baud_rate, 19200); + assert_eq!(config.driver, crate::atx::AtxDriverType::Serial); + assert_eq!(config.device, "/dev/null"); + assert_eq!(config.baud_rate, 19200); + assert!(config.power.enabled); + assert_eq!(config.power.pin, 1); } #[test] fn test_atx_validate_with_current_rejects_serial_pin_zero() { let mut current = AtxConfig::default(); - current.power.driver = crate::atx::AtxDriverType::Serial; - current.power.device = "/dev/null".to_string(); + current.enabled = true; + current.driver = crate::atx::AtxDriverType::Serial; + current.device = "/dev/null".to_string(); + current.power.enabled = true; current.power.pin = 1; - current.power.baud_rate = 9600; + current.baud_rate = 9600; - let update = AtxConfigUpdate { + let mut update = empty_atx_update(); + update.power = Some(AtxOutputBindingUpdate { enabled: None, - power: Some(AtxKeyConfigUpdate { - driver: None, - device: None, - baud_rate: None, - pin: Some(0), - active_level: None, - }), - reset: None, - led: None, - wol_interface: None, - }; + device: None, + pin: Some(0), + active_level: None, + }); assert!(update.validate_with_current(¤t).is_err()); } #[test] - fn test_atx_validate_with_current_rejects_shared_serial_baud_mismatch() { + fn test_atx_validate_with_current_rejects_enabled_none_driver() { let mut current = AtxConfig::default(); - current.power.driver = crate::atx::AtxDriverType::Serial; - current.power.device = "/dev/ttyUSB0".to_string(); - current.power.pin = 1; - current.power.baud_rate = 9600; - current.reset.driver = crate::atx::AtxDriverType::Serial; - current.reset.device = "/dev/ttyUSB0".to_string(); - current.reset.pin = 2; - current.reset.baud_rate = 9600; + current.driver = crate::atx::AtxDriverType::None; - let update = AtxConfigUpdate { + let mut update = empty_atx_update(); + update.enabled = Some(true); + + assert!(update.validate_with_current(¤t).is_err()); + } + + #[test] + fn test_atx_validate_with_current_rejects_usb_relay_channel_over_8() { + let mut current = AtxConfig::default(); + current.enabled = true; + current.driver = crate::atx::AtxDriverType::UsbRelay; + current.device = "/dev/null".to_string(); + current.power.enabled = true; + current.power.pin = 1; + + let mut update = empty_atx_update(); + update.power = Some(AtxOutputBindingUpdate { enabled: None, - power: None, - reset: Some(AtxKeyConfigUpdate { - driver: None, - device: None, - baud_rate: Some(115200), - pin: None, - active_level: None, - }), - led: None, - wol_interface: None, - }; + device: None, + pin: Some(9), + active_level: None, + }); assert!(update.validate_with_current(¤t).is_err()); } diff --git a/src/web/handlers/system.rs b/src/web/handlers/system.rs index 15ded06f..a673d3ae 100644 --- a/src/web/handlers/system.rs +++ b/src/web/handlers/system.rs @@ -88,10 +88,7 @@ pub async fn system_info(State(state): State>) -> Json atx: CapabilityInfo { available: config.atx.enabled, backend: if config.atx.enabled { - Some(format!( - "power: {:?}, reset: {:?}", - config.atx.power.driver, config.atx.reset.driver - )) + Some(format!("{:?}", config.atx.driver)) } else { None }, diff --git a/web/src/api/index.ts b/web/src/api/index.ts index a2b54e2c..9f6042c8 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -549,6 +549,8 @@ export const atxApi = { initialized: boolean power_status: 'on' | 'off' | 'unknown' led_supported: boolean + hdd_status: 'active' | 'inactive' | 'unknown' + hdd_supported: boolean }>('/atx/status'), power: (action: 'short' | 'long' | 'reset') => diff --git a/web/src/components/AtxPopover.vue b/web/src/components/AtxPopover.vue index 50e5fe6d..8345fbdd 100644 --- a/web/src/components/AtxPopover.vue +++ b/web/src/components/AtxPopover.vue @@ -2,25 +2,23 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog' -import { Power, RotateCcw, CircleDot, Wifi, Send } from 'lucide-vue-next' +import { Power, RotateCcw, CircleDot, Wifi, Send, HardDrive } from 'lucide-vue-next' import { atxApi } from '@/api' import { atxConfigApi } from '@/api/config' +type AtxAction = 'short' | 'long' | 'reset' + +const minActionFeedbackMs = 800 +const actionDurations: Record = { + short: 500, + long: 5000, + reset: 500, +} + const emit = defineEmits<{ (e: 'close'): void (e: 'powerShort'): void @@ -34,21 +32,23 @@ const { t } = useI18n() const activeTab = ref('atx') const powerState = ref<'on' | 'off' | 'unknown'>('unknown') +const hddState = ref<'active' | 'inactive' | 'unknown'>('unknown') let powerStateTimer: number | null = null -// Decouple action data from dialog visibility to prevent race conditions -const pendingAction = ref<'short' | 'long' | 'reset' | null>(null) -const confirmDialogOpen = ref(false) +let actionTimer: number | null = null const wolMacAddress = ref('') const wolHistory = ref([]) const wolSending = ref(false) const wolLoadingHistory = ref(false) +const activeAction = ref(null) -const powerStateColor = computed(() => { +const actionBusy = computed(() => activeAction.value !== null) + +const powerStateIconColor = computed(() => { switch (powerState.value) { - case 'on': return 'bg-green-500' - case 'off': return 'bg-slate-400' - default: return 'bg-yellow-500' + case 'on': return 'text-green-600' + case 'off': return 'text-slate-500' + default: return 'text-yellow-600' } }) @@ -60,39 +60,42 @@ const powerStateText = computed(() => { } }) -function requestAction(action: 'short' | 'long' | 'reset') { - pendingAction.value = action - confirmDialogOpen.value = true -} +const hddStateIconColor = computed(() => { + switch (hddState.value) { + case 'active': return 'text-sky-600' + case 'inactive': return 'text-slate-500' + default: return 'text-yellow-600' + } +}) -function handleAction() { - console.log('[AtxPopover] Confirming action:', pendingAction.value) - if (pendingAction.value === 'short') emit('powerShort') - else if (pendingAction.value === 'long') emit('powerLong') - else if (pendingAction.value === 'reset') emit('reset') - confirmDialogOpen.value = false - setTimeout(() => { +const hddStateText = computed(() => { + switch (hddState.value) { + case 'active': return t('atx.hddActive') + case 'inactive': return t('atx.hddInactive') + default: return t('atx.stateUnknown') + } +}) + +function handleAction(action: AtxAction) { + if (actionBusy.value) return + + console.log('[AtxPopover] Running action:', action) + activeAction.value = action + + if (action === 'short') emit('powerShort') + else if (action === 'long') emit('powerLong') + else emit('reset') + + if (actionTimer !== null) { + window.clearTimeout(actionTimer) + } + actionTimer = window.setTimeout(() => { + activeAction.value = null + actionTimer = null refreshPowerState().catch(() => {}) - }, 1200) + }, Math.max(actionDurations[action], minActionFeedbackMs)) } -const confirmTitle = computed(() => { - switch (pendingAction.value) { - case 'short': return t('atx.confirmShortTitle') - case 'long': return t('atx.confirmLongTitle') - case 'reset': return t('atx.confirmResetTitle') - default: return '' - } -}) - -const confirmDescription = computed(() => { - switch (pendingAction.value) { - case 'short': return t('atx.confirmShortDesc') - case 'long': return t('atx.confirmLongDesc') - case 'reset': return t('atx.confirmResetDesc') - default: return '' - } -}) const isValidMac = computed(() => { const mac = wolMacAddress.value.trim() const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^([0-9A-Fa-f]{12})$/ @@ -142,8 +145,10 @@ async function refreshPowerState() { try { const state = await atxApi.status() powerState.value = state.power_status + hddState.value = state.hdd_status } catch { powerState.value = 'unknown' + hddState.value = 'unknown' } } @@ -159,6 +164,10 @@ onUnmounted(() => { window.clearInterval(powerStateTimer) powerStateTimer = null } + if (actionTimer !== null) { + window.clearTimeout(actionTimer) + actionTimer = null + } }) watch( @@ -173,70 +182,91 @@ watch( diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index c0001cf9..a24f6ba5 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -193,18 +193,15 @@ export default { title: 'Power Control', description: 'Control remote host power state', powerState: 'Power State', + hddState: 'HDD Activity', stateOn: 'On', stateOff: 'Off', stateUnknown: 'Unknown', + hddActive: 'Active', + hddInactive: 'Inactive', shortPress: 'Power (Short)', longPress: 'Power (Long/Force Off)', reset: 'Reset', - confirmShortTitle: 'Confirm Power Press', - confirmShortDesc: 'This will simulate pressing the power button, same as a physical short press.', - confirmLongTitle: 'Confirm Force Power Off', - confirmLongDesc: 'This will force power off the host, which may cause data loss. Are you sure?', - confirmResetTitle: 'Confirm Reset', - confirmResetDesc: 'This will reset the host, which may cause unsaved data loss. Are you sure?', wol: 'Wake-on-LAN', wolDescription: 'Send Wake-on-LAN magic packet to power on a remote machine.', macAddress: 'MAC Address', @@ -548,7 +545,6 @@ export default { detecting: 'Detecting...', networkSettings: 'Network Settings', msdSettings: 'MSD Settings', - atxSettings: 'ATX Settings', httpSettings: 'HTTP Settings', httpPort: 'HTTP Port', configureHttpPort: 'Configure HTTP server port', @@ -676,33 +672,22 @@ export default { disabled: 'Disabled', msdDesc: 'Mass Storage Device allows you to mount ISO images and virtual drives to the target machine. Use the MSD panel on the main page to manage images.', atxDesc: 'ATX power control allows you to remotely power on/off and reset the target machine. Use the ATX panel on the main page to control power.', - atxSettingsDesc: 'Configure ATX power control hardware bindings', - atxEnable: 'Enable ATX Control', - atxEnableDesc: 'Enable remote control of power and reset buttons', atxPowerButton: 'Power Button', - atxPowerButtonDesc: 'For power on (short press) and force off (long press)', atxResetButton: 'Reset Button', - atxResetButtonDesc: 'For resetting the target machine', atxDriver: 'Driver Type', atxDriverNone: 'Disabled', atxDriverGpio: 'GPIO', atxDriverUsbRelay: 'USB LCUS HID Relay', atxDriverSerial: 'USB LCUS Serial Relay', atxDevice: 'Device', + atxGpioChip: 'GPIO Chip', atxPin: 'GPIO Pin', atxChannel: 'Relay Channel', - atxSharedSerialBaudHint: 'When Power and Reset share one serial relay device, baud rate is controlled by the first config', - atxActiveLevel: 'Active Level', - atxLevelHigh: 'Active High', - atxLevelLow: 'Active Low', + atxActiveLevel: 'Trigger Level', + atxLevelHigh: 'High-level trigger', + atxLevelLow: 'Low-level trigger', atxLedSensing: 'LED Status Sensing', - atxLedSensingDesc: 'Detect host power LED to determine power state (optional)', - atxLedEnable: 'Enable LED Sensing', - atxLedEnableDesc: 'Read power LED status via GPIO', - atxLedChip: 'GPIO Chip', - atxLedPin: 'GPIO Pin', - atxLedInverted: 'Invert Logic', - atxLedInvertedDesc: 'GPIO is low when LED is on', + atxHddSensing: 'HDD Activity Sensing', atxWolSettings: 'Wake-on-LAN Settings', atxWolSettingsDesc: 'Configure WOL magic packet sending options', atxWolInterface: 'Network Interface', diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index de7ab67e..83127b6a 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -193,18 +193,15 @@ export default { title: '电源控制', description: '控制远程主机的电源状态', powerState: '电源状态', + hddState: '硬盘活动', stateOn: '已开机', stateOff: '已关机', stateUnknown: '未知', + hddActive: '活动', + hddInactive: '空闲', shortPress: '短按电源', longPress: '长按电源 (强制关机)', reset: '重启', - confirmShortTitle: '确认短按电源', - confirmShortDesc: '这将模拟按下电源按钮,与物理短按电源键效果相同。', - confirmLongTitle: '确认强制关机', - confirmLongDesc: '这将强制关闭主机,可能导致数据丢失。确定继续吗?', - confirmResetTitle: '确认重启', - confirmResetDesc: '这将重启主机,可能导致未保存的数据丢失。确定继续吗?', wol: '网络唤醒', wolDescription: '发送 Wake-on-LAN 魔术包以远程开机。', macAddress: 'MAC 地址', @@ -547,7 +544,6 @@ export default { detecting: '探测中...', networkSettings: '网络设置', msdSettings: 'MSD 设置', - atxSettings: 'ATX 设置', httpSettings: 'HTTP 设置', httpPort: 'HTTP 端口', configureHttpPort: '配置 HTTP 服务器端口', @@ -675,33 +671,22 @@ export default { disabled: '已禁用', msdDesc: '虚拟存储设备允许您将 ISO 镜像和虚拟驱动器挂载到目标机器。请在主页面的 MSD 面板中管理镜像。', atxDesc: 'ATX 电源控制允许您远程开关机和重启目标机器。请在主页面的 ATX 面板中控制电源。', - atxSettingsDesc: '配置 ATX 电源控制硬件绑定', - atxEnable: '启用 ATX 控制', - atxEnableDesc: '启用后可以远程控制电源和重启按钮', atxPowerButton: '电源按钮', - atxPowerButtonDesc: '用于开机(短按)和强制关机(长按)', atxResetButton: '重启按钮', - atxResetButtonDesc: '用于重启目标机器', atxDriver: '驱动类型', atxDriverNone: '禁用', atxDriverGpio: 'GPIO', atxDriverUsbRelay: 'USB LCUS HID继电器', atxDriverSerial: 'USB LCUS 串口继电器', atxDevice: '设备', + atxGpioChip: 'GPIO 芯片', atxPin: 'GPIO 引脚', atxChannel: '继电器通道', - atxSharedSerialBaudHint: 'Power 与 Reset 使用同一串口继电器时,波特率由第一个配置统一控制', - atxActiveLevel: '有效电平', - atxLevelHigh: '高电平有效', - atxLevelLow: '低电平有效', + atxActiveLevel: '触发电平', + atxLevelHigh: '高电平触发', + atxLevelLow: '低电平触发', atxLedSensing: 'LED 状态检测', - atxLedSensingDesc: '检测主机电源 LED 以确定电源状态(可选)', - atxLedEnable: '启用 LED 检测', - atxLedEnableDesc: '通过 GPIO 读取电源 LED 状态', - atxLedChip: 'GPIO 芯片', - atxLedPin: 'GPIO 引脚', - atxLedInverted: '反转逻辑', - atxLedInvertedDesc: 'LED 亮起时 GPIO 为低电平', + atxHddSensing: 'HDD 活动检测', atxWolSettings: '网络唤醒设置', atxWolSettingsDesc: '配置 Wake-on-LAN 魔术包发送选项', atxWolInterface: '网络接口', diff --git a/web/src/stores/system.ts b/web/src/stores/system.ts index cea074cb..85a0b4fc 100644 --- a/web/src/stores/system.ts +++ b/web/src/stores/system.ts @@ -50,6 +50,7 @@ interface AtxState { backend: string initialized: boolean powerOn: boolean + hddStatus: 'active' | 'inactive' | 'unknown' error: string | null } @@ -121,6 +122,7 @@ export interface AtxDeviceInfo { backend: string initialized: boolean power_on: boolean + hdd_status: 'active' | 'inactive' | 'unknown' error: string | null } @@ -235,6 +237,7 @@ export const useSystemStore = defineStore('system', () => { backend: state.backend, initialized: state.initialized, powerOn: state.power_status === 'on', + hddStatus: state.hdd_status, error: null, } return state @@ -376,6 +379,7 @@ export const useSystemStore = defineStore('system', () => { backend: data.atx.backend, initialized: data.atx.initialized, powerOn: data.atx.power_on, + hddStatus: data.atx.hdd_status, error: data.atx.error, } } else { diff --git a/web/src/types/generated.ts b/web/src/types/generated.ts index 4872aff8..7953a4be 100644 --- a/web/src/types/generated.ts +++ b/web/src/types/generated.ts @@ -94,26 +94,29 @@ export enum ActiveLevel { Low = "low", } -export interface AtxKeyConfig { - driver: AtxDriverType; +export interface AtxOutputBinding { + enabled: boolean; device: string; pin: number; active_level: ActiveLevel; - baud_rate: number; } -export interface AtxLedConfig { +export interface AtxInputBinding { enabled: boolean; - gpio_chip: string; - gpio_pin: number; - inverted: boolean; + device: string; + pin: number; + active_level: ActiveLevel; } export interface AtxConfig { enabled: boolean; - power: AtxKeyConfig; - reset: AtxKeyConfig; - led: AtxLedConfig; + driver: AtxDriverType; + device: string; + baud_rate: number; + power: AtxOutputBinding; + reset: AtxOutputBinding; + led: AtxInputBinding; + hdd: AtxInputBinding; wol_interface: string; } @@ -287,32 +290,36 @@ export interface AppConfig { redfish: RedfishConfig; } -/** Update for a single ATX key configuration */ -export interface AtxKeyConfigUpdate { - driver?: AtxDriverType; +/** Update for a single ATX output binding */ +export interface AtxOutputBindingUpdate { + enabled?: boolean; device?: string; - baud_rate?: number; pin?: number; active_level?: ActiveLevel; } -/** Update for LED sensing configuration */ -export interface AtxLedConfigUpdate { +/** Update for ATX GPIO input sensing */ +export interface AtxInputBindingUpdate { enabled?: boolean; - gpio_chip?: string; - gpio_pin?: number; - inverted?: boolean; + device?: string; + pin?: number; + active_level?: ActiveLevel; } /** ATX configuration update request */ export interface AtxConfigUpdate { enabled?: boolean; + driver?: AtxDriverType; + device?: string; + baud_rate?: number; /** Power button configuration */ - power?: AtxKeyConfigUpdate; + power?: AtxOutputBindingUpdate; /** Reset button configuration */ - reset?: AtxKeyConfigUpdate; + reset?: AtxOutputBindingUpdate; /** LED sensing configuration */ - led?: AtxLedConfigUpdate; + led?: AtxInputBindingUpdate; + /** HDD activity sensing configuration */ + hdd?: AtxInputBindingUpdate; /** Network interface for WOL packets (empty = auto) */ wol_interface?: string; } diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index a086353a..9e7db31a 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -1137,29 +1137,41 @@ watch(bindMode, (mode) => { const atxConfig = ref({ enabled: false, + driver: 'none' as AtxDriverType, + device: '', + baud_rate: 9600, power: { - driver: 'none' as AtxDriverType, + enabled: false, device: '', pin: 1, active_level: 'high' as ActiveLevel, - baud_rate: 9600, }, reset: { - driver: 'none' as AtxDriverType, + enabled: false, device: '', pin: 1, active_level: 'high' as ActiveLevel, - baud_rate: 9600, }, led: { enabled: false, - gpio_chip: '', - gpio_pin: 0, - inverted: false, + device: '', + pin: 0, + active_level: 'high' as ActiveLevel, + }, + hdd: { + enabled: false, + device: '', + pin: 0, + active_level: 'high' as ActiveLevel, }, wol_interface: '', }) +const atxSaving = ref(false) +const atxSaved = ref(false) +const wolSaving = ref(false) +const wolSaved = ref(false) + const atxDevices = ref({ gpio_chips: [], usb_relays: [], @@ -1183,17 +1195,6 @@ const atxDriverOptions = computed(() => { : options }) -const isSharedAtxSerialRelay = computed(() => { - const power = atxConfig.value.power - const reset = atxConfig.value.reset - return ( - power.driver === 'serial' - && reset.driver === 'serial' - && !!power.device.trim() - && power.device === reset.device - ) -}) - const availableBackends = ref([]) const selectedBackendFormats = computed(() => { @@ -1737,14 +1738,29 @@ async function loadAtxConfig() { const config = await configStore.refreshAtx() atxConfig.value = { enabled: config.enabled, - power: { ...config.power }, - reset: { ...config.reset }, - led: { ...config.led }, + driver: config.enabled ? config.driver : 'none' as AtxDriverType, + device: config.device || '', + baud_rate: config.baud_rate || 9600, + power: { + ...config.power, + active_level: config.power.active_level || 'high', + }, + reset: { + ...config.reset, + active_level: config.reset.active_level || 'high', + }, + led: { + ...config.led, + active_level: config.led.active_level || 'high', + }, + hdd: { + ...config.hdd, + active_level: config.hdd.active_level || 'high', + }, wol_interface: config.wol_interface || '', } clearAtxSerialDeviceConflicts() normalizeAtxRelayChannels() - syncSharedAtxSerialBaudRate() } catch { } } @@ -1756,43 +1772,64 @@ async function loadAtxDevices() { } } -async function saveAtxConfig() { - loading.value = true - saved.value = false +async function saveAtxSettings() { + atxSaving.value = true + atxSaved.value = false try { normalizeAtxRelayChannels() - syncSharedAtxSerialBaudRate() + const isGpio = atxConfig.value.driver === 'gpio' + const isRelay = ['usbrelay', 'serial'].includes(atxConfig.value.driver) await configStore.updateAtx({ - enabled: atxConfig.value.enabled, + enabled: atxConfig.value.driver !== 'none', + driver: atxConfig.value.driver, + device: atxConfig.value.device || undefined, + baud_rate: atxConfig.value.baud_rate, power: { - driver: atxConfig.value.power.driver, + enabled: isGpio ? !!atxConfig.value.power.device : isRelay, device: atxConfig.value.power.device || undefined, pin: atxConfig.value.power.pin, active_level: atxConfig.value.power.active_level, - baud_rate: atxConfig.value.power.baud_rate, }, reset: { - driver: atxConfig.value.reset.driver, + enabled: isGpio ? !!atxConfig.value.reset.device : isRelay, device: atxConfig.value.reset.device || undefined, pin: atxConfig.value.reset.pin, active_level: atxConfig.value.reset.active_level, - baud_rate: isSharedAtxSerialRelay.value - ? atxConfig.value.power.baud_rate - : atxConfig.value.reset.baud_rate, }, led: { - enabled: atxConfig.value.led.enabled, - gpio_chip: atxConfig.value.led.gpio_chip || undefined, - gpio_pin: atxConfig.value.led.gpio_pin, - inverted: atxConfig.value.led.inverted, + enabled: isGpio && !!atxConfig.value.led.device, + device: atxConfig.value.led.device || undefined, + pin: atxConfig.value.led.pin, + active_level: atxConfig.value.led.active_level, + }, + hdd: { + enabled: isGpio && !!atxConfig.value.hdd.device, + device: atxConfig.value.hdd.device || undefined, + pin: atxConfig.value.hdd.pin, + active_level: atxConfig.value.hdd.active_level, }, - wol_interface: atxConfig.value.wol_interface || undefined, }) - saved.value = true - setTimeout(() => (saved.value = false), 2000) + atxConfig.value.enabled = atxConfig.value.driver !== 'none' + atxSaved.value = true + setTimeout(() => (atxSaved.value = false), 2000) } catch { } finally { - loading.value = false + atxSaving.value = false + } +} + +async function saveWolSettings() { + wolSaving.value = true + wolSaved.value = false + try { + await configStore.updateAtx({ + wol_interface: atxConfig.value.wol_interface || undefined, + }) + wolSaved.value = true + setTimeout(() => (wolSaved.value = false), 2000) + } catch { + } finally { + wolSaving.value = false } } @@ -1823,25 +1860,21 @@ function clearAtxSerialDeviceConflicts() { const reserved = ch9329ReservedSerialDevice.value if (!reserved) return - if (atxConfig.value.power.driver === 'serial' && atxConfig.value.power.device === reserved) { - atxConfig.value.power.device = '' + if (atxConfig.value.driver === 'serial' && atxConfig.value.device === reserved) { + atxConfig.value.device = '' } - if (atxConfig.value.reset.driver === 'serial' && atxConfig.value.reset.device === reserved) { - atxConfig.value.reset.device = '' - } -} - -function syncSharedAtxSerialBaudRate() { - if (!isSharedAtxSerialRelay.value) return - atxConfig.value.reset.baud_rate = atxConfig.value.power.baud_rate } function normalizeAtxRelayChannels() { for (const key of [atxConfig.value.power, atxConfig.value.reset]) { - if (['usbrelay', 'serial'].includes(key.driver) && key.pin < 1) { + if (['usbrelay', 'serial'].includes(atxConfig.value.driver) && key.pin < 1) { key.pin = 1 } } + if (atxConfig.value.driver !== 'gpio') { + atxConfig.value.led.enabled = false + atxConfig.value.hdd.enabled = false + } } watch( @@ -1852,22 +1885,10 @@ watch( ) watch( - () => [atxConfig.value.power.driver, atxConfig.value.reset.driver], + () => atxConfig.value.driver, () => { normalizeAtxRelayChannels() - }, -) - -watch( - () => [ - atxConfig.value.power.driver, - atxConfig.value.power.device, - atxConfig.value.power.baud_rate, - atxConfig.value.reset.driver, - atxConfig.value.reset.device, - ], - () => { - syncSharedAtxSerialBaudRate() + clearAtxSerialDeviceConflicts() }, ) @@ -3947,83 +3968,40 @@ watch(isWindows, () => {
- - -
- {{ t('settings.atxSettings') }} - {{ t('settings.atxSettingsDesc') }} -
- -
- -
-
- -

{{ t('settings.atxEnableDesc') }}

-
- -
-
-
- - - - - {{ t('settings.atxPowerButton') }} - {{ t('settings.atxPowerButtonDesc') }} - - -
-
- -
-
- -
-
-
-
- - -
-
- - -
-
- - @@ -4032,128 +4010,138 @@ watch(isWindows, () => {
-
-
- - - - {{ t('settings.atxResetButton') }} - {{ t('settings.atxResetButtonDesc') }} - - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -

- {{ t('settings.atxSharedSerialBaudHint') }} -

-
-
-
-
- - - - - {{ t('settings.atxLedSensing') }} - {{ t('settings.atxLedSensingDesc') }} - - -
-
- -

{{ t('settings.atxLedEnableDesc') }}

-
- -
-