feat: 完善 ATX 功能逻辑与控件样式

This commit is contained in:
mofeng-git
2026-07-07 00:21:54 +08:00
parent f6e97a06f5
commit e60152d38b
21 changed files with 1058 additions and 836 deletions

View File

@@ -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);
}
}

View File

@@ -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(())
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}