mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 11:51:44 +08:00
feat: 完善 ATX 功能逻辑与控件样式
This commit is contained in:
@@ -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<AtxKeyExecutor>,
|
||||
reset_executor: Option<AtxKeyExecutor>,
|
||||
led_sensor: Option<LedSensor>,
|
||||
hdd_sensor: Option<LedSensor>,
|
||||
}
|
||||
|
||||
/// 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<AtxKeyConfig> {
|
||||
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<AtxKeyConfig>, Option<AtxKeyConfig>) {
|
||||
(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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<Option<LineHandle>>,
|
||||
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<PowerStatus> {
|
||||
pub async fn read_active(&self) -> Result<bool> {
|
||||
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<PowerStatus> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
134
src/atx/types.rs
134
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -54,6 +54,7 @@ pub struct AtxDeviceInfo {
|
||||
pub backend: String,
|
||||
pub initialized: bool,
|
||||
pub power_on: bool,
|
||||
pub hdd_status: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
21
src/state.rs
21
src/state.rs
@@ -247,26 +247,27 @@ impl AppState {
|
||||
}
|
||||
|
||||
async fn collect_atx_info(&self) -> Option<AtxDeviceInfo> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<AtxState> 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<AtxState> 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<Arc<AppState>>) -> Result<Json<AtxSt
|
||||
initialized: false,
|
||||
power_status: "unknown".to_string(),
|
||||
led_supported: false,
|
||||
hdd_status: "unknown".to_string(),
|
||||
hdd_supported: false,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,10 @@ fn validate_windows_atx_backends(atx: &AtxConfig) -> 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());
|
||||
}
|
||||
|
||||
@@ -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<crate::atx::AtxDriverType>,
|
||||
pub struct AtxOutputBindingUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub device: Option<String>,
|
||||
pub baud_rate: Option<u32>,
|
||||
pub pin: Option<u32>,
|
||||
pub active_level: Option<crate::atx::ActiveLevel>,
|
||||
}
|
||||
|
||||
/// Update for LED sensing configuration
|
||||
/// Update for ATX GPIO input sensing
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AtxLedConfigUpdate {
|
||||
pub struct AtxInputBindingUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub gpio_chip: Option<String>,
|
||||
pub gpio_pin: Option<u32>,
|
||||
pub inverted: Option<bool>,
|
||||
pub device: Option<String>,
|
||||
pub pin: Option<u32>,
|
||||
pub active_level: Option<crate::atx::ActiveLevel>,
|
||||
}
|
||||
|
||||
/// ATX configuration update request
|
||||
@@ -479,26 +478,46 @@ pub struct AtxLedConfigUpdate {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AtxConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub driver: Option<crate::atx::AtxDriverType>,
|
||||
pub device: Option<String>,
|
||||
pub baud_rate: Option<u32>,
|
||||
/// Power button configuration
|
||||
pub power: Option<AtxKeyConfigUpdate>,
|
||||
pub power: Option<AtxOutputBindingUpdate>,
|
||||
/// Reset button configuration
|
||||
pub reset: Option<AtxKeyConfigUpdate>,
|
||||
pub reset: Option<AtxOutputBindingUpdate>,
|
||||
/// LED sensing configuration
|
||||
pub led: Option<AtxLedConfigUpdate>,
|
||||
pub led: Option<AtxInputBindingUpdate>,
|
||||
/// HDD activity sensing configuration
|
||||
pub hdd: Option<AtxInputBindingUpdate>,
|
||||
/// Network interface for WOL packets (empty = auto)
|
||||
pub wol_interface: Option<String>,
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -88,10 +88,7 @@ pub async fn system_info(State(state): State<Arc<AppState>>) -> Json<SystemInfo>
|
||||
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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user