mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 09:41:45 +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::executor::{timing, AtxKeyExecutor};
|
||||||
use super::led::LedSensor;
|
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};
|
use crate::error::{AppError, Result};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct AtxControllerConfig {
|
pub struct AtxControllerConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub power: AtxKeyConfig,
|
pub driver: AtxDriverType,
|
||||||
pub reset: AtxKeyConfig,
|
pub device: String,
|
||||||
pub led: AtxLedConfig,
|
pub baud_rate: u32,
|
||||||
|
pub power: AtxOutputBinding,
|
||||||
|
pub reset: AtxOutputBinding,
|
||||||
|
pub led: AtxInputBinding,
|
||||||
|
pub hdd: AtxInputBinding,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Grouped together to reduce lock acquisitions
|
/// Grouped together to reduce lock acquisitions
|
||||||
@@ -25,6 +32,7 @@ struct AtxInner {
|
|||||||
power_executor: Option<AtxKeyExecutor>,
|
power_executor: Option<AtxKeyExecutor>,
|
||||||
reset_executor: Option<AtxKeyExecutor>,
|
reset_executor: Option<AtxKeyExecutor>,
|
||||||
led_sensor: Option<LedSensor>,
|
led_sensor: Option<LedSensor>,
|
||||||
|
hdd_sensor: Option<LedSensor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manages ATX power control through independent executors for each action.
|
/// Manages ATX power control through independent executors for each action.
|
||||||
@@ -34,14 +42,44 @@ pub struct AtxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AtxController {
|
impl AtxController {
|
||||||
fn should_share_serial_device(power: &AtxKeyConfig, reset: &AtxKeyConfig) -> bool {
|
fn build_key_config(
|
||||||
power.is_configured()
|
config: &AtxControllerConfig,
|
||||||
&& reset.is_configured()
|
binding: &AtxOutputBinding,
|
||||||
&& power.driver == super::types::AtxDriverType::Serial
|
) -> Option<AtxKeyConfig> {
|
||||||
&& reset.driver == super::types::AtxDriverType::Serial
|
if !binding.is_configured_for(config.driver, &config.device) {
|
||||||
&& !power.device.is_empty()
|
return None;
|
||||||
&& power.device == reset.device
|
}
|
||||||
&& power.baud_rate == reset.baud_rate
|
|
||||||
|
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(
|
async fn init_key_executor(
|
||||||
@@ -63,75 +101,80 @@ impl AtxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn init_components(inner: &mut AtxInner) {
|
async fn init_components(inner: &mut AtxInner) {
|
||||||
if Self::should_share_serial_device(&inner.config.power, &inner.config.reset) {
|
let (power_config, reset_config) = Self::runtime_key_configs(&inner.config);
|
||||||
match AtxKeyExecutor::open_shared_serial(
|
|
||||||
&inner.config.power.device,
|
if Self::should_share_serial_device(&inner.config) {
|
||||||
inner.config.power.baud_rate,
|
match AtxKeyExecutor::open_shared_serial(&inner.config.device, inner.config.baud_rate) {
|
||||||
) {
|
|
||||||
Ok(shared_serial) => {
|
Ok(shared_serial) => {
|
||||||
for (slot, warn_label, info_label, config, serial) in [
|
for (slot, warn_label, info_label, config, serial) in [
|
||||||
(
|
(
|
||||||
&mut inner.power_executor,
|
&mut inner.power_executor,
|
||||||
"power",
|
"power",
|
||||||
"Power",
|
"Power",
|
||||||
inner.config.power.clone(),
|
power_config.clone(),
|
||||||
shared_serial.clone(),
|
shared_serial.clone(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
&mut inner.reset_executor,
|
&mut inner.reset_executor,
|
||||||
"reset",
|
"reset",
|
||||||
"Reset",
|
"Reset",
|
||||||
inner.config.reset.clone(),
|
reset_config.clone(),
|
||||||
shared_serial,
|
shared_serial,
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
if let Some(config) = config {
|
||||||
let executor =
|
let executor =
|
||||||
AtxKeyExecutor::new_with_shared_serial(config.clone(), serial);
|
AtxKeyExecutor::new_with_shared_serial(config.clone(), serial);
|
||||||
*slot =
|
*slot =
|
||||||
Self::init_key_executor(warn_label, info_label, config, executor).await;
|
Self::init_key_executor(warn_label, info_label, config, executor)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to open shared serial device {} for ATX power/reset: {}",
|
"Failed to open shared serial device {} for ATX power/reset: {}",
|
||||||
inner.config.power.device, e
|
inner.config.device, e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (slot, warn_label, info_label, config) in [
|
for (slot, warn_label, info_label, config) in [
|
||||||
(
|
(&mut inner.power_executor, "power", "Power", power_config),
|
||||||
&mut inner.power_executor,
|
(&mut inner.reset_executor, "reset", "Reset", reset_config),
|
||||||
"power",
|
|
||||||
"Power",
|
|
||||||
inner.config.power.clone(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
&mut inner.reset_executor,
|
|
||||||
"reset",
|
|
||||||
"Reset",
|
|
||||||
inner.config.reset.clone(),
|
|
||||||
),
|
|
||||||
] {
|
] {
|
||||||
if config.is_configured() {
|
if let Some(config) = config {
|
||||||
let executor = AtxKeyExecutor::new(config.clone());
|
let executor = AtxKeyExecutor::new(config.clone());
|
||||||
*slot = Self::init_key_executor(warn_label, info_label, config, executor).await;
|
*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());
|
let mut sensor = LedSensor::new(inner.config.led.clone());
|
||||||
if let Err(e) = sensor.init().await {
|
if let Err(e) = sensor.init().await {
|
||||||
warn!("Failed to initialize LED sensor: {}", e);
|
warn!("Failed to initialize LED sensor: {}", e);
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
"LED sensor initialized on {} pin {}",
|
"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);
|
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) {
|
async fn shutdown_components(inner: &mut AtxInner) {
|
||||||
@@ -153,6 +196,13 @@ impl AtxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
inner.led_sensor = None;
|
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 {
|
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 {
|
pub fn new(config: AtxControllerConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: RwLock::new(AtxInner {
|
inner: RwLock::new(AtxInner {
|
||||||
@@ -176,6 +241,7 @@ impl AtxController {
|
|||||||
power_executor: None,
|
power_executor: None,
|
||||||
reset_executor: None,
|
reset_executor: None,
|
||||||
led_sensor: None,
|
led_sensor: None,
|
||||||
|
hdd_sensor: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,13 +336,21 @@ impl AtxController {
|
|||||||
let inner = self.inner.read().await;
|
let inner = self.inner.read().await;
|
||||||
|
|
||||||
let power_status = Self::read_power_status(inner.led_sensor.as_ref()).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 {
|
AtxState {
|
||||||
available: inner.config.enabled,
|
available: inner.config.enabled,
|
||||||
|
driver: if inner.config.enabled {
|
||||||
|
inner.config.driver
|
||||||
|
} else {
|
||||||
|
AtxDriverType::None
|
||||||
|
},
|
||||||
power_configured: inner.power_executor.is_some(),
|
power_configured: inner.power_executor.is_some(),
|
||||||
reset_configured: inner.reset_executor.is_some(),
|
reset_configured: inner.reset_executor.is_some(),
|
||||||
power_status,
|
power_status,
|
||||||
led_supported: inner.led_sensor.is_some(),
|
led_supported: inner.led_sensor.is_some(),
|
||||||
|
hdd_status,
|
||||||
|
hdd_supported: inner.hdd_sensor.is_some(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,45 +358,96 @@ impl AtxController {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::atx::AtxDriverType;
|
use crate::atx::{AtxDriverType, AtxOutputBinding};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_share_serial_device_true() {
|
fn test_should_share_serial_device_true() {
|
||||||
let power = AtxKeyConfig {
|
let config = AtxControllerConfig {
|
||||||
driver: AtxDriverType::Serial,
|
driver: AtxDriverType::Serial,
|
||||||
device: "/dev/ttyUSB0".to_string(),
|
device: "/dev/ttyUSB0".to_string(),
|
||||||
|
baud_rate: 9600,
|
||||||
|
power: AtxOutputBinding {
|
||||||
|
enabled: true,
|
||||||
pin: 1,
|
pin: 1,
|
||||||
active_level: super::super::types::ActiveLevel::High,
|
..Default::default()
|
||||||
baud_rate: 9600,
|
},
|
||||||
};
|
reset: AtxOutputBinding {
|
||||||
let reset = AtxKeyConfig {
|
enabled: true,
|
||||||
driver: AtxDriverType::Serial,
|
|
||||||
device: "/dev/ttyUSB0".to_string(),
|
|
||||||
pin: 2,
|
pin: 2,
|
||||||
active_level: super::super::types::ActiveLevel::High,
|
..Default::default()
|
||||||
baud_rate: 9600,
|
},
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(AtxController::should_share_serial_device(&power, &reset));
|
assert!(AtxController::should_share_serial_device(&config));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_share_serial_device_false_on_different_baud() {
|
fn test_should_share_serial_device_false_when_reset_disabled() {
|
||||||
let power = AtxKeyConfig {
|
let config = AtxControllerConfig {
|
||||||
driver: AtxDriverType::Serial,
|
driver: AtxDriverType::Serial,
|
||||||
device: "/dev/ttyUSB0".to_string(),
|
device: "/dev/ttyUSB0".to_string(),
|
||||||
pin: 1,
|
|
||||||
active_level: super::super::types::ActiveLevel::High,
|
|
||||||
baud_rate: 9600,
|
baud_rate: 9600,
|
||||||
};
|
power: AtxOutputBinding {
|
||||||
let reset = AtxKeyConfig {
|
enabled: true,
|
||||||
driver: AtxDriverType::Serial,
|
pin: 1,
|
||||||
device: "/dev/ttyUSB0".to_string(),
|
..Default::default()
|
||||||
|
},
|
||||||
|
reset: AtxOutputBinding {
|
||||||
|
enabled: false,
|
||||||
pin: 2,
|
pin: 2,
|
||||||
active_level: super::super::types::ActiveLevel::High,
|
..Default::default()
|
||||||
baud_rate: 115200,
|
},
|
||||||
|
..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)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use super::types::{AtxLedConfig, PowerStatus};
|
use super::types::{AtxInputBinding, PowerStatus};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
|
||||||
pub struct LedSensor {
|
pub struct LedSensor {
|
||||||
config: AtxLedConfig,
|
config: AtxInputBinding,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LedSensor {
|
impl LedSensor {
|
||||||
pub fn new(config: AtxLedConfig) -> Self {
|
pub fn new(config: AtxInputBinding) -> Self {
|
||||||
Self { config }
|
Self { config }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +28,10 @@ impl LedSensor {
|
|||||||
Ok(PowerStatus::Unknown)
|
Ok(PowerStatus::Unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn read_active(&self) -> Result<bool> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn shutdown(&mut self) -> Result<()> {
|
pub async fn shutdown(&mut self) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use super::types::{AtxLedConfig, PowerStatus};
|
use super::types::{ActiveLevel, AtxInputBinding, PowerStatus};
|
||||||
use crate::error::{AppError, Result};
|
use crate::error::{AppError, Result};
|
||||||
|
|
||||||
pub struct LedSensor {
|
pub struct LedSensor {
|
||||||
config: AtxLedConfig,
|
config: AtxInputBinding,
|
||||||
handle: Mutex<Option<LineHandle>>,
|
handle: Mutex<Option<LineHandle>>,
|
||||||
initialized: AtomicBool,
|
initialized: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LedSensor {
|
impl LedSensor {
|
||||||
pub fn new(config: AtxLedConfig) -> Self {
|
pub fn new(config: AtxInputBinding) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
handle: Mutex::new(None),
|
handle: Mutex::new(None),
|
||||||
@@ -33,17 +33,14 @@ impl LedSensor {
|
|||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Initializing LED sensor on {} pin {}",
|
"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)))?;
|
.map_err(|e| AppError::Internal(format!("LED GPIO chip failed: {}", e)))?;
|
||||||
|
|
||||||
let line = chip.get_line(self.config.gpio_pin).map_err(|e| {
|
let line = chip.get_line(self.config.pin).map_err(|e| {
|
||||||
AppError::Internal(format!(
|
AppError::Internal(format!("LED GPIO line {} failed: {}", self.config.pin, e))
|
||||||
"LED GPIO line {} failed: {}",
|
|
||||||
self.config.gpio_pin, e
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let handle = line
|
let handle = line
|
||||||
@@ -57,9 +54,11 @@ impl LedSensor {
|
|||||||
Ok(())
|
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) {
|
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();
|
let guard = self.handle.lock().unwrap();
|
||||||
@@ -69,21 +68,30 @@ impl LedSensor {
|
|||||||
.get_value()
|
.get_value()
|
||||||
.map_err(|e| AppError::Internal(format!("LED read failed: {}", e)))?;
|
.map_err(|e| AppError::Internal(format!("LED read failed: {}", e)))?;
|
||||||
|
|
||||||
let is_on = if self.config.inverted {
|
let active = match self.config.active_level {
|
||||||
value == 0
|
ActiveLevel::High => value == 1,
|
||||||
} else {
|
ActiveLevel::Low => value == 0,
|
||||||
value == 1
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(if is_on {
|
Ok(active)
|
||||||
|
}
|
||||||
|
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
|
PowerStatus::On
|
||||||
} else {
|
} else {
|
||||||
PowerStatus::Off
|
PowerStatus::Off
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
None => Ok(PowerStatus::Unknown),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn shutdown(&mut self) -> Result<()> {
|
pub async fn shutdown(&mut self) -> Result<()> {
|
||||||
*self.handle.lock().unwrap() = None;
|
*self.handle.lock().unwrap() = None;
|
||||||
@@ -105,7 +113,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_led_sensor_creation() {
|
fn test_led_sensor_creation() {
|
||||||
let config = AtxLedConfig::default();
|
let config = AtxInputBinding::default();
|
||||||
let sensor = LedSensor::new(config);
|
let sensor = LedSensor::new(config);
|
||||||
assert!(!sensor.config.is_configured());
|
assert!(!sensor.config.is_configured());
|
||||||
assert!(!sensor.initialized.load(Ordering::Relaxed));
|
assert!(!sensor.initialized.load(Ordering::Relaxed));
|
||||||
@@ -113,11 +121,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_led_sensor_with_config() {
|
fn test_led_sensor_with_config() {
|
||||||
let config = AtxLedConfig {
|
let config = AtxInputBinding {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
gpio_chip: "/dev/gpiochip0".to_string(),
|
device: "/dev/gpiochip0".to_string(),
|
||||||
gpio_pin: 7,
|
pin: 7,
|
||||||
inverted: false,
|
active_level: ActiveLevel::High,
|
||||||
};
|
};
|
||||||
let sensor = LedSensor::new(config);
|
let sensor = LedSensor::new(config);
|
||||||
assert!(sensor.config.is_configured());
|
assert!(sensor.config.is_configured());
|
||||||
@@ -126,13 +134,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_led_sensor_inverted_config() {
|
fn test_led_sensor_inverted_config() {
|
||||||
let config = AtxLedConfig {
|
let config = AtxInputBinding {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
gpio_chip: "/dev/gpiochip0".to_string(),
|
device: "/dev/gpiochip0".to_string(),
|
||||||
gpio_pin: 7,
|
pin: 7,
|
||||||
inverted: true,
|
active_level: ActiveLevel::Low,
|
||||||
};
|
};
|
||||||
let sensor = LedSensor::new(config);
|
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 controller::{AtxController, AtxControllerConfig};
|
||||||
pub use executor::timing;
|
pub use executor::timing;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxKeyConfig, AtxLedConfig, AtxPowerRequest,
|
ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxInputBinding, AtxKeyConfig,
|
||||||
AtxState, PowerStatus,
|
AtxOutputBinding, AtxPowerRequest, AtxState, HddStatus, PowerStatus,
|
||||||
};
|
};
|
||||||
pub use wol::{list_wol_history, record_wol_history, send_wol};
|
pub use wol::{list_wol_history, record_wol_history, send_wol};
|
||||||
|
|
||||||
@@ -120,7 +120,8 @@ mod tests {
|
|||||||
let _: AtxDriverType = AtxDriverType::None;
|
let _: AtxDriverType = AtxDriverType::None;
|
||||||
let _: ActiveLevel = ActiveLevel::High;
|
let _: ActiveLevel = ActiveLevel::High;
|
||||||
let _: AtxKeyConfig = AtxKeyConfig::default();
|
let _: AtxKeyConfig = AtxKeyConfig::default();
|
||||||
let _: AtxLedConfig = AtxLedConfig::default();
|
let _: AtxInputBinding = AtxInputBinding::default();
|
||||||
|
let _: AtxOutputBinding = AtxOutputBinding::default();
|
||||||
let _: AtxState = AtxState::default();
|
let _: AtxState = AtxState::default();
|
||||||
let _: AtxDevices = AtxDevices::default();
|
let _: AtxDevices = AtxDevices::default();
|
||||||
}
|
}
|
||||||
|
|||||||
134
src/atx/types.rs
134
src/atx/types.rs
@@ -1,7 +1,6 @@
|
|||||||
//! ATX data types and structures
|
//! ATX data types and structures
|
||||||
//!
|
//!
|
||||||
//! Defines the configuration and state types for the flexible ATX power control system.
|
//! Defines the configuration and state types for the ATX power control system.
|
||||||
//! Each ATX action (power, reset) can be independently configured with different hardware.
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
@@ -15,6 +14,15 @@ pub enum PowerStatus {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum HddStatus {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
@@ -36,6 +44,56 @@ pub enum ActiveLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct AtxKeyConfig {
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct AtxState {
|
pub struct AtxState {
|
||||||
pub available: bool,
|
pub available: bool,
|
||||||
|
pub driver: AtxDriverType,
|
||||||
pub power_configured: bool,
|
pub power_configured: bool,
|
||||||
pub reset_configured: bool,
|
pub reset_configured: bool,
|
||||||
pub power_status: PowerStatus,
|
pub power_status: PowerStatus,
|
||||||
pub led_supported: bool,
|
pub led_supported: bool,
|
||||||
|
pub hdd_status: HddStatus,
|
||||||
|
pub hdd_supported: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@@ -139,6 +184,29 @@ mod tests {
|
|||||||
assert_eq!(ActiveLevel::default(), ActiveLevel::High);
|
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]
|
#[test]
|
||||||
fn test_atx_key_config_default() {
|
fn test_atx_key_config_default() {
|
||||||
let config = AtxKeyConfig::default();
|
let config = AtxKeyConfig::default();
|
||||||
@@ -149,37 +217,22 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_atx_key_config_is_configured() {
|
fn test_atx_input_binding_default() {
|
||||||
let mut config = AtxKeyConfig::default();
|
let config = AtxInputBinding::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();
|
|
||||||
assert!(!config.enabled);
|
assert!(!config.enabled);
|
||||||
assert!(config.gpio_chip.is_empty());
|
assert!(config.device.is_empty());
|
||||||
assert!(!config.is_configured());
|
assert!(!config.is_configured());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_atx_led_config_is_configured() {
|
fn test_atx_input_binding_is_configured() {
|
||||||
let mut config = AtxLedConfig::default();
|
let mut config = AtxInputBinding::default();
|
||||||
assert!(!config.is_configured());
|
assert!(!config.is_configured());
|
||||||
|
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
assert!(!config.is_configured());
|
assert!(!config.is_configured());
|
||||||
|
|
||||||
config.gpio_chip = "/dev/gpiochip0".to_string();
|
config.device = "/dev/gpiochip0".to_string();
|
||||||
assert!(config.is_configured());
|
assert!(config.is_configured());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,9 +240,12 @@ mod tests {
|
|||||||
fn test_atx_state_default() {
|
fn test_atx_state_default() {
|
||||||
let state = AtxState::default();
|
let state = AtxState::default();
|
||||||
assert!(!state.available);
|
assert!(!state.available);
|
||||||
|
assert_eq!(state.driver, AtxDriverType::None);
|
||||||
assert!(!state.power_configured);
|
assert!(!state.power_configured);
|
||||||
assert!(!state.reset_configured);
|
assert!(!state.reset_configured);
|
||||||
assert_eq!(state.power_status, PowerStatus::Unknown);
|
assert_eq!(state.power_status, PowerStatus::Unknown);
|
||||||
assert!(!state.led_supported);
|
assert!(!state.led_supported);
|
||||||
|
assert_eq!(state.hdd_status, HddStatus::Unknown);
|
||||||
|
assert!(!state.hdd_supported);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,61 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
pub use crate::atx::{ActiveLevel, AtxDriverType, AtxKeyConfig, AtxLedConfig};
|
pub use crate::atx::{ActiveLevel, AtxDriverType, AtxInputBinding, AtxOutputBinding};
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[derive(Default)]
|
|
||||||
pub struct AtxConfig {
|
pub struct AtxConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub power: AtxKeyConfig,
|
pub driver: AtxDriverType,
|
||||||
pub reset: AtxKeyConfig,
|
pub device: String,
|
||||||
pub led: AtxLedConfig,
|
pub baud_rate: u32,
|
||||||
|
pub power: AtxOutputBinding,
|
||||||
|
pub reset: AtxOutputBinding,
|
||||||
|
pub led: AtxInputBinding,
|
||||||
|
pub hdd: AtxInputBinding,
|
||||||
pub wol_interface: String,
|
pub wol_interface: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
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 {
|
pub fn to_controller_config(&self) -> crate::atx::AtxControllerConfig {
|
||||||
crate::atx::AtxControllerConfig {
|
crate::atx::AtxControllerConfig {
|
||||||
enabled: self.enabled,
|
enabled: self.enabled,
|
||||||
|
driver: self.driver,
|
||||||
|
device: self.device.clone(),
|
||||||
|
baud_rate: self.baud_rate,
|
||||||
power: self.power.clone(),
|
power: self.power.clone(),
|
||||||
reset: self.reset.clone(),
|
reset: self.reset.clone(),
|
||||||
led: self.led.clone(),
|
led: self.led.clone(),
|
||||||
|
hdd: self.hdd.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ impl AppConfig {
|
|||||||
if self.hid.backend != HidBackend::Otg {
|
if self.hid.backend != HidBackend::Otg {
|
||||||
self.msd.enabled = false;
|
self.msd.enabled = false;
|
||||||
}
|
}
|
||||||
|
self.atx.normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_platform_defaults(&mut self) {
|
pub fn apply_platform_defaults(&mut self) {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ pub struct AtxDeviceInfo {
|
|||||||
pub backend: String,
|
pub backend: String,
|
||||||
pub initialized: bool,
|
pub initialized: bool,
|
||||||
pub power_on: bool,
|
pub power_on: bool,
|
||||||
|
pub hdd_status: String,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,34 +83,23 @@ fn apply_windows(config: &mut AppConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
config.atx.power.driver,
|
config.atx.driver,
|
||||||
AtxDriverType::Gpio | AtxDriverType::UsbRelay
|
AtxDriverType::Gpio | AtxDriverType::UsbRelay
|
||||||
) {
|
) {
|
||||||
config.atx.power.driver = AtxDriverType::None;
|
config.atx.driver = AtxDriverType::None;
|
||||||
}
|
config.atx.enabled = false;
|
||||||
if matches!(
|
|
||||||
config.atx.reset.driver,
|
|
||||||
AtxDriverType::Gpio | AtxDriverType::UsbRelay
|
|
||||||
) {
|
|
||||||
config.atx.reset.driver = AtxDriverType::None;
|
|
||||||
}
|
}
|
||||||
if !config.initialized
|
if !config.initialized
|
||||||
&& config.atx.power.driver == AtxDriverType::None
|
&& config.atx.driver == AtxDriverType::None
|
||||||
&& config.atx.power.device.is_empty()
|
&& config.atx.device.is_empty()
|
||||||
{
|
{
|
||||||
config.atx.power.driver = AtxDriverType::Serial;
|
config.atx.driver = AtxDriverType::Serial;
|
||||||
config.atx.power.device = "COM4".to_string();
|
config.atx.device = "COM4".to_string();
|
||||||
|
config.atx.baud_rate = 9600;
|
||||||
|
config.atx.power.enabled = true;
|
||||||
config.atx.power.pin = 1;
|
config.atx.power.pin = 1;
|
||||||
config.atx.power.baud_rate = 9600;
|
config.atx.reset.enabled = true;
|
||||||
}
|
|
||||||
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.pin = 2;
|
config.atx.reset.pin = 2;
|
||||||
config.atx.reset.baud_rate = 9600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config
|
config
|
||||||
|
|||||||
21
src/state.rs
21
src/state.rs
@@ -247,26 +247,27 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn collect_atx_info(&self) -> Option<AtxDeviceInfo> {
|
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_guard = self.atx.read().await;
|
||||||
let atx = atx_guard.as_ref()?;
|
let atx = atx_guard.as_ref()?;
|
||||||
|
|
||||||
let state = atx.state().await;
|
let state = atx.state().await;
|
||||||
Some(AtxDeviceInfo {
|
Some(AtxDeviceInfo {
|
||||||
available: state.available,
|
available: state.available,
|
||||||
backend: match (state.power_configured, state.reset_configured) {
|
backend: match state.driver {
|
||||||
(true, true) => BACKEND_BOTH,
|
crate::atx::AtxDriverType::Gpio => "gpio",
|
||||||
(true, false) => BACKEND_POWER_ONLY,
|
crate::atx::AtxDriverType::UsbRelay => "usbrelay",
|
||||||
(false, true) => BACKEND_RESET_ONLY,
|
crate::atx::AtxDriverType::Serial => "serial",
|
||||||
(false, false) => BACKEND_NONE,
|
crate::atx::AtxDriverType::None => "none",
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
initialized: state.power_configured || state.reset_configured,
|
initialized: state.power_configured || state.reset_configured,
|
||||||
power_on: state.power_status == crate::atx::PowerStatus::On,
|
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,
|
error: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::atx::{AtxState, PowerStatus};
|
use crate::atx::{AtxDriverType, AtxState, HddStatus, PowerStatus};
|
||||||
|
|
||||||
const WOL_HISTORY_DEFAULT_LIMIT: usize = 5;
|
const WOL_HISTORY_DEFAULT_LIMIT: usize = 5;
|
||||||
const WOL_HISTORY_MAX_LIMIT: usize = 50;
|
const WOL_HISTORY_MAX_LIMIT: usize = 50;
|
||||||
@@ -13,21 +13,21 @@ pub struct AtxStateResponse {
|
|||||||
pub initialized: bool,
|
pub initialized: bool,
|
||||||
pub power_status: String,
|
pub power_status: String,
|
||||||
pub led_supported: bool,
|
pub led_supported: bool,
|
||||||
|
pub hdd_status: String,
|
||||||
|
pub hdd_supported: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AtxState> for AtxStateResponse {
|
impl From<AtxState> for AtxStateResponse {
|
||||||
fn from(state: AtxState) -> Self {
|
fn from(state: AtxState) -> Self {
|
||||||
Self {
|
Self {
|
||||||
available: state.available,
|
available: state.available,
|
||||||
backend: if state.power_configured || state.reset_configured {
|
backend: match state.driver {
|
||||||
format!(
|
AtxDriverType::Gpio => "gpio",
|
||||||
"power: {}, reset: {}",
|
AtxDriverType::UsbRelay => "usbrelay",
|
||||||
if state.power_configured { "yes" } else { "no" },
|
AtxDriverType::Serial => "serial",
|
||||||
if state.reset_configured { "yes" } else { "no" }
|
AtxDriverType::None => "none",
|
||||||
)
|
}
|
||||||
} else {
|
.to_string(),
|
||||||
"none".to_string()
|
|
||||||
},
|
|
||||||
initialized: state.power_configured || state.reset_configured,
|
initialized: state.power_configured || state.reset_configured,
|
||||||
power_status: match state.power_status {
|
power_status: match state.power_status {
|
||||||
PowerStatus::On => "on".to_string(),
|
PowerStatus::On => "on".to_string(),
|
||||||
@@ -35,6 +35,12 @@ impl From<AtxState> for AtxStateResponse {
|
|||||||
PowerStatus::Unknown => "unknown".to_string(),
|
PowerStatus::Unknown => "unknown".to_string(),
|
||||||
},
|
},
|
||||||
led_supported: state.led_supported,
|
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,
|
initialized: false,
|
||||||
power_status: "unknown".to_string(),
|
power_status: "unknown".to_string(),
|
||||||
led_supported: false,
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (name, key) in [("power", &atx.power), ("reset", &atx.reset)] {
|
if !matches!(atx.driver, AtxDriverType::Serial | AtxDriverType::None) {
|
||||||
if !matches!(key.driver, AtxDriverType::Serial | AtxDriverType::None) {
|
return Err(AppError::BadRequest(
|
||||||
return Err(AppError::BadRequest(format!(
|
"Windows ATX only supports serial relay or none".to_string(),
|
||||||
"Windows ATX {} only supports serial relay or none",
|
));
|
||||||
name
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -68,14 +65,12 @@ fn validate_serial_device_conflict(atx: &AtxConfig, hid: &HidConfig) -> Result<(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (name, key) in [("power", &atx.power), ("reset", &atx.reset)] {
|
if atx.driver == AtxDriverType::Serial && atx.device.trim() == reserved {
|
||||||
if key.driver == AtxDriverType::Serial && key.device.trim() == reserved {
|
|
||||||
return Err(AppError::BadRequest(format!(
|
return Err(AppError::BadRequest(format!(
|
||||||
"ATX {} serial device '{}' conflicts with HID CH9329 serial device",
|
"ATX serial device '{}' conflicts with HID CH9329 serial device",
|
||||||
name, reserved
|
reserved
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -87,8 +82,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_validate_serial_device_conflict_rejects_ch9329_overlap() {
|
fn test_validate_serial_device_conflict_rejects_ch9329_overlap() {
|
||||||
let mut atx = AtxConfig::default();
|
let mut atx = AtxConfig::default();
|
||||||
atx.power.driver = AtxDriverType::Serial;
|
atx.driver = AtxDriverType::Serial;
|
||||||
atx.power.device = "/dev/ttyUSB0".to_string();
|
atx.device = "/dev/ttyUSB0".to_string();
|
||||||
|
|
||||||
let mut hid = HidConfig::default();
|
let mut hid = HidConfig::default();
|
||||||
hid.backend = HidBackend::Ch9329;
|
hid.backend = HidBackend::Ch9329;
|
||||||
@@ -100,8 +95,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_validate_serial_device_conflict_allows_non_ch9329_backend() {
|
fn test_validate_serial_device_conflict_allows_non_ch9329_backend() {
|
||||||
let mut atx = AtxConfig::default();
|
let mut atx = AtxConfig::default();
|
||||||
atx.power.driver = AtxDriverType::Serial;
|
atx.driver = AtxDriverType::Serial;
|
||||||
atx.power.device = "/dev/ttyUSB0".to_string();
|
atx.device = "/dev/ttyUSB0".to_string();
|
||||||
|
|
||||||
let mut hid = HidConfig::default();
|
let mut hid = HidConfig::default();
|
||||||
hid.backend = HidBackend::None;
|
hid.backend = HidBackend::None;
|
||||||
@@ -113,8 +108,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_validate_windows_atx_backends_allows_serial() {
|
fn test_validate_windows_atx_backends_allows_serial() {
|
||||||
let mut atx = AtxConfig::default();
|
let mut atx = AtxConfig::default();
|
||||||
atx.power.driver = AtxDriverType::Serial;
|
atx.driver = AtxDriverType::Serial;
|
||||||
atx.reset.driver = AtxDriverType::None;
|
|
||||||
|
|
||||||
assert!(validate_windows_atx_backends(&atx).is_ok());
|
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]
|
#[typeshare]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct AtxKeyConfigUpdate {
|
pub struct AtxOutputBindingUpdate {
|
||||||
pub driver: Option<crate::atx::AtxDriverType>,
|
pub enabled: Option<bool>,
|
||||||
pub device: Option<String>,
|
pub device: Option<String>,
|
||||||
pub baud_rate: Option<u32>,
|
|
||||||
pub pin: Option<u32>,
|
pub pin: Option<u32>,
|
||||||
pub active_level: Option<crate::atx::ActiveLevel>,
|
pub active_level: Option<crate::atx::ActiveLevel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update for LED sensing configuration
|
/// Update for ATX GPIO input sensing
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct AtxLedConfigUpdate {
|
pub struct AtxInputBindingUpdate {
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
pub gpio_chip: Option<String>,
|
pub device: Option<String>,
|
||||||
pub gpio_pin: Option<u32>,
|
pub pin: Option<u32>,
|
||||||
pub inverted: Option<bool>,
|
pub active_level: Option<crate::atx::ActiveLevel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ATX configuration update request
|
/// ATX configuration update request
|
||||||
@@ -479,26 +478,46 @@ pub struct AtxLedConfigUpdate {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct AtxConfigUpdate {
|
pub struct AtxConfigUpdate {
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
|
pub driver: Option<crate::atx::AtxDriverType>,
|
||||||
|
pub device: Option<String>,
|
||||||
|
pub baud_rate: Option<u32>,
|
||||||
/// Power button configuration
|
/// Power button configuration
|
||||||
pub power: Option<AtxKeyConfigUpdate>,
|
pub power: Option<AtxOutputBindingUpdate>,
|
||||||
/// Reset button configuration
|
/// Reset button configuration
|
||||||
pub reset: Option<AtxKeyConfigUpdate>,
|
pub reset: Option<AtxOutputBindingUpdate>,
|
||||||
/// LED sensing configuration
|
/// 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)
|
/// Network interface for WOL packets (empty = auto)
|
||||||
pub wol_interface: Option<String>,
|
pub wol_interface: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AtxConfigUpdate {
|
impl AtxConfigUpdate {
|
||||||
pub fn validate(&self) -> crate::error::Result<()> {
|
pub fn validate(&self) -> crate::error::Result<()> {
|
||||||
// Validate power key config if present
|
if let Some(baud_rate) = self.baud_rate {
|
||||||
if let Some(ref power) = self.power {
|
if baud_rate == 0 {
|
||||||
Self::validate_key_config(power, "power")?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,152 +528,123 @@ impl AtxConfigUpdate {
|
|||||||
let mut merged = current.clone();
|
let mut merged = current.clone();
|
||||||
self.apply_to(&mut merged);
|
self.apply_to(&mut merged);
|
||||||
|
|
||||||
Self::validate_effective_key_config(&merged.power, "power")?;
|
if merged.driver == crate::atx::AtxDriverType::None {
|
||||||
Self::validate_effective_key_config(&merged.reset, "reset")?;
|
if merged.enabled {
|
||||||
Self::validate_shared_serial_baud_rate(&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();
|
|
||||||
|
|
||||||
if same_serial_device && power.baud_rate != reset.baud_rate {
|
|
||||||
return Err(AppError::BadRequest(
|
return Err(AppError::BadRequest(
|
||||||
"ATX power/reset sharing the same serial relay device must use one baud_rate"
|
"ATX driver must not be none when ATX is enabled".to_string(),
|
||||||
.to_string(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
merged.normalize();
|
||||||
|
|
||||||
|
if !merged.enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::validate_effective_config(&merged)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_key_config(key: &AtxKeyConfigUpdate, name: &str) -> crate::error::Result<()> {
|
fn validate_device_path(device: &str, name: &str) -> crate::error::Result<()> {
|
||||||
if let Some(ref device) = key.device {
|
let trimmed = device.trim();
|
||||||
if !device.trim().is_empty() && !cfg!(windows) && !std::path::Path::new(device).exists()
|
if trimmed.is_empty() {
|
||||||
{
|
return Err(AppError::BadRequest(format!(
|
||||||
|
"{} device cannot be empty",
|
||||||
|
name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg!(windows) && !std::path::Path::new(trimmed).exists() {
|
||||||
return Err(AppError::BadRequest(format!(
|
return Err(AppError::BadRequest(format!(
|
||||||
"{} device '{}' does not exist",
|
"{} device '{}' does not exist",
|
||||||
name, device
|
name, trimmed
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_effective_key_config(
|
fn validate_output_binding_update(
|
||||||
key: &crate::atx::AtxKeyConfig,
|
binding: &AtxOutputBindingUpdate,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> crate::error::Result<()> {
|
) -> crate::error::Result<()> {
|
||||||
match key.driver {
|
if let Some(ref device) = binding.device {
|
||||||
crate::atx::AtxDriverType::Serial => {
|
if !device.trim().is_empty() {
|
||||||
if key.device.trim().is_empty() {
|
Self::validate_device_path(device, name)?;
|
||||||
return Err(AppError::BadRequest(format!(
|
|
||||||
"{} serial device cannot be empty",
|
|
||||||
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!(
|
Ok(())
|
||||||
"{} serial channel must be <= {}",
|
}
|
||||||
name,
|
|
||||||
u8::MAX
|
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)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, binding) in [("led", &config.led), ("hdd", &config.hdd)] {
|
||||||
|
if binding.enabled {
|
||||||
|
Self::validate_device_path(&binding.device, name)?;
|
||||||
}
|
}
|
||||||
if key.baud_rate == 0 {
|
|
||||||
return Err(AppError::BadRequest(format!(
|
|
||||||
"{} baud_rate must be greater than 0",
|
|
||||||
name
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
crate::atx::AtxDriverType::UsbRelay => {
|
crate::atx::AtxDriverType::UsbRelay => {
|
||||||
if key.pin == 0 {
|
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::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!(
|
return Err(AppError::BadRequest(format!(
|
||||||
"{} USB relay channel must be 1-based (>= 1)",
|
"{} channel must be between {} and {}",
|
||||||
name
|
name, min, max
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
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
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::atx::AtxDriverType::Gpio | crate::atx::AtxDriverType::None => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,50 +652,65 @@ impl AtxConfigUpdate {
|
|||||||
if let Some(enabled) = self.enabled {
|
if let Some(enabled) = self.enabled {
|
||||||
config.enabled = 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 {
|
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 {
|
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 {
|
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 {
|
if let Some(ref wol_interface) = self.wol_interface {
|
||||||
config.wol_interface = wol_interface.clone();
|
config.wol_interface = wol_interface.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_key_update(update: &AtxKeyConfigUpdate, config: &mut crate::atx::AtxKeyConfig) {
|
fn apply_output_binding_update(
|
||||||
if let Some(driver) = update.driver {
|
update: &AtxOutputBindingUpdate,
|
||||||
config.driver = driver;
|
config: &mut crate::atx::AtxOutputBinding,
|
||||||
|
) {
|
||||||
|
if let Some(enabled) = update.enabled {
|
||||||
|
config.enabled = enabled;
|
||||||
}
|
}
|
||||||
if let Some(ref device) = update.device {
|
if let Some(ref device) = update.device {
|
||||||
config.device = device.clone();
|
config.device = device.clone();
|
||||||
}
|
}
|
||||||
if let Some(baud_rate) = update.baud_rate {
|
if let Some(pin) = update.pin {
|
||||||
config.baud_rate = baud_rate;
|
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 {
|
if let Some(pin) = update.pin {
|
||||||
config.pin = pin;
|
config.pin = pin;
|
||||||
}
|
}
|
||||||
if let Some(level) = update.active_level {
|
if let Some(active_level) = update.active_level {
|
||||||
config.active_level = level;
|
config.active_level = active_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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1233,77 +1238,90 @@ impl RedfishConfigUpdate {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
fn empty_atx_update() -> AtxConfigUpdate {
|
||||||
fn test_atx_apply_key_update_applies_baud_rate() {
|
AtxConfigUpdate {
|
||||||
let mut config = AtxConfig::default();
|
|
||||||
let update = AtxConfigUpdate {
|
|
||||||
enabled: None,
|
enabled: None,
|
||||||
power: Some(AtxKeyConfigUpdate {
|
driver: None,
|
||||||
driver: Some(crate::atx::AtxDriverType::Serial),
|
device: None,
|
||||||
device: Some("/dev/null".to_string()),
|
baud_rate: None,
|
||||||
baud_rate: Some(19200),
|
power: None,
|
||||||
pin: Some(1),
|
|
||||||
active_level: None,
|
|
||||||
}),
|
|
||||||
reset: None,
|
reset: None,
|
||||||
led: None,
|
led: None,
|
||||||
|
hdd: None,
|
||||||
wol_interface: 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);
|
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]
|
#[test]
|
||||||
fn test_atx_validate_with_current_rejects_serial_pin_zero() {
|
fn test_atx_validate_with_current_rejects_serial_pin_zero() {
|
||||||
let mut current = AtxConfig::default();
|
let mut current = AtxConfig::default();
|
||||||
current.power.driver = crate::atx::AtxDriverType::Serial;
|
current.enabled = true;
|
||||||
current.power.device = "/dev/null".to_string();
|
current.driver = crate::atx::AtxDriverType::Serial;
|
||||||
|
current.device = "/dev/null".to_string();
|
||||||
|
current.power.enabled = true;
|
||||||
current.power.pin = 1;
|
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,
|
enabled: None,
|
||||||
power: Some(AtxKeyConfigUpdate {
|
|
||||||
driver: None,
|
|
||||||
device: None,
|
device: None,
|
||||||
baud_rate: None,
|
|
||||||
pin: Some(0),
|
pin: Some(0),
|
||||||
active_level: None,
|
active_level: None,
|
||||||
}),
|
});
|
||||||
reset: None,
|
|
||||||
led: None,
|
|
||||||
wol_interface: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(update.validate_with_current(¤t).is_err());
|
assert!(update.validate_with_current(¤t).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let mut current = AtxConfig::default();
|
||||||
current.power.driver = crate::atx::AtxDriverType::Serial;
|
current.driver = crate::atx::AtxDriverType::None;
|
||||||
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;
|
|
||||||
|
|
||||||
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,
|
enabled: None,
|
||||||
power: None,
|
|
||||||
reset: Some(AtxKeyConfigUpdate {
|
|
||||||
driver: None,
|
|
||||||
device: None,
|
device: None,
|
||||||
baud_rate: Some(115200),
|
pin: Some(9),
|
||||||
pin: None,
|
|
||||||
active_level: None,
|
active_level: None,
|
||||||
}),
|
});
|
||||||
led: None,
|
|
||||||
wol_interface: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(update.validate_with_current(¤t).is_err());
|
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 {
|
atx: CapabilityInfo {
|
||||||
available: config.atx.enabled,
|
available: config.atx.enabled,
|
||||||
backend: if config.atx.enabled {
|
backend: if config.atx.enabled {
|
||||||
Some(format!(
|
Some(format!("{:?}", config.atx.driver))
|
||||||
"power: {:?}, reset: {:?}",
|
|
||||||
config.atx.power.driver, config.atx.reset.driver
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -549,6 +549,8 @@ export const atxApi = {
|
|||||||
initialized: boolean
|
initialized: boolean
|
||||||
power_status: 'on' | 'off' | 'unknown'
|
power_status: 'on' | 'off' | 'unknown'
|
||||||
led_supported: boolean
|
led_supported: boolean
|
||||||
|
hdd_status: 'active' | 'inactive' | 'unknown'
|
||||||
|
hdd_supported: boolean
|
||||||
}>('/atx/status'),
|
}>('/atx/status'),
|
||||||
|
|
||||||
power: (action: 'short' | 'long' | 'reset') =>
|
power: (action: 'short' | 'long' | 'reset') =>
|
||||||
|
|||||||
@@ -2,25 +2,23 @@
|
|||||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import {
|
import { Power, RotateCcw, CircleDot, Wifi, Send, HardDrive } from 'lucide-vue-next'
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog'
|
|
||||||
import { Power, RotateCcw, CircleDot, Wifi, Send } from 'lucide-vue-next'
|
|
||||||
import { atxApi } from '@/api'
|
import { atxApi } from '@/api'
|
||||||
import { atxConfigApi } from '@/api/config'
|
import { atxConfigApi } from '@/api/config'
|
||||||
|
|
||||||
|
type AtxAction = 'short' | 'long' | 'reset'
|
||||||
|
|
||||||
|
const minActionFeedbackMs = 800
|
||||||
|
const actionDurations: Record<AtxAction, number> = {
|
||||||
|
short: 500,
|
||||||
|
long: 5000,
|
||||||
|
reset: 500,
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void
|
(e: 'close'): void
|
||||||
(e: 'powerShort'): void
|
(e: 'powerShort'): void
|
||||||
@@ -34,21 +32,23 @@ const { t } = useI18n()
|
|||||||
const activeTab = ref('atx')
|
const activeTab = ref('atx')
|
||||||
|
|
||||||
const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
|
const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
|
||||||
|
const hddState = ref<'active' | 'inactive' | 'unknown'>('unknown')
|
||||||
let powerStateTimer: number | null = null
|
let powerStateTimer: number | null = null
|
||||||
// Decouple action data from dialog visibility to prevent race conditions
|
let actionTimer: number | null = null
|
||||||
const pendingAction = ref<'short' | 'long' | 'reset' | null>(null)
|
|
||||||
const confirmDialogOpen = ref(false)
|
|
||||||
|
|
||||||
const wolMacAddress = ref('')
|
const wolMacAddress = ref('')
|
||||||
const wolHistory = ref<string[]>([])
|
const wolHistory = ref<string[]>([])
|
||||||
const wolSending = ref(false)
|
const wolSending = ref(false)
|
||||||
const wolLoadingHistory = ref(false)
|
const wolLoadingHistory = ref(false)
|
||||||
|
const activeAction = ref<AtxAction | null>(null)
|
||||||
|
|
||||||
const powerStateColor = computed(() => {
|
const actionBusy = computed(() => activeAction.value !== null)
|
||||||
|
|
||||||
|
const powerStateIconColor = computed(() => {
|
||||||
switch (powerState.value) {
|
switch (powerState.value) {
|
||||||
case 'on': return 'bg-green-500'
|
case 'on': return 'text-green-600'
|
||||||
case 'off': return 'bg-slate-400'
|
case 'off': return 'text-slate-500'
|
||||||
default: return 'bg-yellow-500'
|
default: return 'text-yellow-600'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -60,39 +60,42 @@ const powerStateText = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function requestAction(action: 'short' | 'long' | 'reset') {
|
const hddStateIconColor = computed(() => {
|
||||||
pendingAction.value = action
|
switch (hddState.value) {
|
||||||
confirmDialogOpen.value = true
|
case 'active': return 'text-sky-600'
|
||||||
|
case 'inactive': return 'text-slate-500'
|
||||||
|
default: return 'text-yellow-600'
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function handleAction() {
|
const hddStateText = computed(() => {
|
||||||
console.log('[AtxPopover] Confirming action:', pendingAction.value)
|
switch (hddState.value) {
|
||||||
if (pendingAction.value === 'short') emit('powerShort')
|
case 'active': return t('atx.hddActive')
|
||||||
else if (pendingAction.value === 'long') emit('powerLong')
|
case 'inactive': return t('atx.hddInactive')
|
||||||
else if (pendingAction.value === 'reset') emit('reset')
|
default: return t('atx.stateUnknown')
|
||||||
confirmDialogOpen.value = false
|
}
|
||||||
setTimeout(() => {
|
})
|
||||||
|
|
||||||
|
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(() => {})
|
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 isValidMac = computed(() => {
|
||||||
const mac = wolMacAddress.value.trim()
|
const mac = wolMacAddress.value.trim()
|
||||||
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^([0-9A-Fa-f]{12})$/
|
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 {
|
try {
|
||||||
const state = await atxApi.status()
|
const state = await atxApi.status()
|
||||||
powerState.value = state.power_status
|
powerState.value = state.power_status
|
||||||
|
hddState.value = state.hdd_status
|
||||||
} catch {
|
} catch {
|
||||||
powerState.value = 'unknown'
|
powerState.value = 'unknown'
|
||||||
|
hddState.value = 'unknown'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +164,10 @@ onUnmounted(() => {
|
|||||||
window.clearInterval(powerStateTimer)
|
window.clearInterval(powerStateTimer)
|
||||||
powerStateTimer = null
|
powerStateTimer = null
|
||||||
}
|
}
|
||||||
|
if (actionTimer !== null) {
|
||||||
|
window.clearTimeout(actionTimer)
|
||||||
|
actionTimer = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -173,70 +182,91 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-3 space-y-3">
|
<div class="p-2.5 space-y-2.5">
|
||||||
<Tabs v-model="activeTab">
|
<Tabs v-model="activeTab">
|
||||||
<TabsList class="w-full grid grid-cols-2">
|
<TabsList class="h-8 w-full grid grid-cols-2">
|
||||||
<TabsTrigger value="atx" class="text-xs">
|
<TabsTrigger value="atx" class="h-7 text-xs">
|
||||||
<Power class="h-3.5 w-3.5 mr-1" />
|
<Power class="h-3 w-3 mr-1" />
|
||||||
{{ t('atx.title') }}
|
{{ t('atx.title') }}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="wol" class="text-xs">
|
<TabsTrigger value="wol" class="h-7 text-xs">
|
||||||
<Wifi class="h-3.5 w-3.5 mr-1" />
|
<Wifi class="h-3 w-3 mr-1" />
|
||||||
WOL
|
WOL
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<!-- ATX Tab -->
|
<!-- ATX Tab -->
|
||||||
<TabsContent value="atx" class="mt-3 space-y-3">
|
<TabsContent value="atx" class="mt-2.5 space-y-2.5">
|
||||||
<p class="text-xs text-muted-foreground">{{ t('atx.description') }}</p>
|
<p class="text-xs text-muted-foreground">{{ t('atx.description') }}</p>
|
||||||
|
|
||||||
<!-- Power State -->
|
<!-- Status -->
|
||||||
<div class="flex items-center justify-between p-2 rounded-md bg-muted/50">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<span class="text-xs text-muted-foreground">{{ t('atx.powerState') }}</span>
|
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
|
||||||
<Badge variant="outline" class="gap-1.5 text-xs">
|
<Power :class="['h-4 w-4 shrink-0', powerStateIconColor]" />
|
||||||
<span :class="['h-2 w-2 rounded-full', powerStateColor]" />
|
<div class="min-w-0">
|
||||||
{{ powerStateText }}
|
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.powerState') }}</p>
|
||||||
</Badge>
|
<p class="mt-1 truncate text-xs font-medium leading-none">{{ powerStateText }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
|
||||||
|
<HardDrive :class="['h-4 w-4 shrink-0', hddStateIconColor]" />
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.hddState') }}</p>
|
||||||
|
<p class="mt-1 truncate text-xs font-medium leading-none">{{ hddStateText }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<!-- Power Actions -->
|
<!-- Power Actions -->
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="w-full justify-start gap-2 h-8 text-xs"
|
:disabled="actionBusy"
|
||||||
@click="requestAction('short')"
|
:class="[
|
||||||
|
'w-full justify-start gap-2 h-7 text-xs',
|
||||||
|
activeAction === 'short' ? 'bg-muted text-muted-foreground' : '',
|
||||||
|
]"
|
||||||
|
@click="handleAction('short')"
|
||||||
>
|
>
|
||||||
<Power class="h-3.5 w-3.5" />
|
<Power class="h-3 w-3" />
|
||||||
{{ t('atx.shortPress') }}
|
{{ t('atx.shortPress') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="w-full justify-start gap-2 h-8 text-xs text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:hover:bg-orange-950"
|
:disabled="actionBusy"
|
||||||
@click="requestAction('long')"
|
:class="[
|
||||||
|
'w-full justify-start gap-2 h-7 text-xs text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:hover:bg-orange-950',
|
||||||
|
activeAction === 'long' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
|
||||||
|
]"
|
||||||
|
@click="handleAction('long')"
|
||||||
>
|
>
|
||||||
<CircleDot class="h-3.5 w-3.5" />
|
<CircleDot class="h-3 w-3" />
|
||||||
{{ t('atx.longPress') }}
|
{{ t('atx.longPress') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="w-full justify-start gap-2 h-8 text-xs text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950"
|
:disabled="actionBusy"
|
||||||
@click="requestAction('reset')"
|
:class="[
|
||||||
|
'w-full justify-start gap-2 h-7 text-xs text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950',
|
||||||
|
activeAction === 'reset' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
|
||||||
|
]"
|
||||||
|
@click="handleAction('reset')"
|
||||||
>
|
>
|
||||||
<RotateCcw class="h-3.5 w-3.5" />
|
<RotateCcw class="h-3 w-3" />
|
||||||
{{ t('atx.reset') }}
|
{{ t('atx.reset') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- WOL Tab -->
|
<!-- WOL Tab -->
|
||||||
<TabsContent value="wol" class="mt-3 space-y-3">
|
<TabsContent value="wol" class="mt-2.5 space-y-2.5">
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
{{ t('atx.wolDescription') }}
|
{{ t('atx.wolDescription') }}
|
||||||
</p>
|
</p>
|
||||||
@@ -287,18 +317,4 @@ watch(
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Dialog -->
|
|
||||||
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>{{ confirmTitle }}</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>{{ confirmDescription }}</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
|
||||||
<AlertDialogAction @click="handleAction">{{ t('common.confirm') }}</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -193,18 +193,15 @@ export default {
|
|||||||
title: 'Power Control',
|
title: 'Power Control',
|
||||||
description: 'Control remote host power state',
|
description: 'Control remote host power state',
|
||||||
powerState: 'Power State',
|
powerState: 'Power State',
|
||||||
|
hddState: 'HDD Activity',
|
||||||
stateOn: 'On',
|
stateOn: 'On',
|
||||||
stateOff: 'Off',
|
stateOff: 'Off',
|
||||||
stateUnknown: 'Unknown',
|
stateUnknown: 'Unknown',
|
||||||
|
hddActive: 'Active',
|
||||||
|
hddInactive: 'Inactive',
|
||||||
shortPress: 'Power (Short)',
|
shortPress: 'Power (Short)',
|
||||||
longPress: 'Power (Long/Force Off)',
|
longPress: 'Power (Long/Force Off)',
|
||||||
reset: 'Reset',
|
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',
|
wol: 'Wake-on-LAN',
|
||||||
wolDescription: 'Send Wake-on-LAN magic packet to power on a remote machine.',
|
wolDescription: 'Send Wake-on-LAN magic packet to power on a remote machine.',
|
||||||
macAddress: 'MAC Address',
|
macAddress: 'MAC Address',
|
||||||
@@ -548,7 +545,6 @@ export default {
|
|||||||
detecting: 'Detecting...',
|
detecting: 'Detecting...',
|
||||||
networkSettings: 'Network Settings',
|
networkSettings: 'Network Settings',
|
||||||
msdSettings: 'MSD Settings',
|
msdSettings: 'MSD Settings',
|
||||||
atxSettings: 'ATX Settings',
|
|
||||||
httpSettings: 'HTTP Settings',
|
httpSettings: 'HTTP Settings',
|
||||||
httpPort: 'HTTP Port',
|
httpPort: 'HTTP Port',
|
||||||
configureHttpPort: 'Configure HTTP server port',
|
configureHttpPort: 'Configure HTTP server port',
|
||||||
@@ -676,33 +672,22 @@ export default {
|
|||||||
disabled: 'Disabled',
|
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.',
|
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.',
|
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',
|
atxPowerButton: 'Power Button',
|
||||||
atxPowerButtonDesc: 'For power on (short press) and force off (long press)',
|
|
||||||
atxResetButton: 'Reset Button',
|
atxResetButton: 'Reset Button',
|
||||||
atxResetButtonDesc: 'For resetting the target machine',
|
|
||||||
atxDriver: 'Driver Type',
|
atxDriver: 'Driver Type',
|
||||||
atxDriverNone: 'Disabled',
|
atxDriverNone: 'Disabled',
|
||||||
atxDriverGpio: 'GPIO',
|
atxDriverGpio: 'GPIO',
|
||||||
atxDriverUsbRelay: 'USB LCUS HID Relay',
|
atxDriverUsbRelay: 'USB LCUS HID Relay',
|
||||||
atxDriverSerial: 'USB LCUS Serial Relay',
|
atxDriverSerial: 'USB LCUS Serial Relay',
|
||||||
atxDevice: 'Device',
|
atxDevice: 'Device',
|
||||||
|
atxGpioChip: 'GPIO Chip',
|
||||||
atxPin: 'GPIO Pin',
|
atxPin: 'GPIO Pin',
|
||||||
atxChannel: 'Relay Channel',
|
atxChannel: 'Relay Channel',
|
||||||
atxSharedSerialBaudHint: 'When Power and Reset share one serial relay device, baud rate is controlled by the first config',
|
atxActiveLevel: 'Trigger Level',
|
||||||
atxActiveLevel: 'Active Level',
|
atxLevelHigh: 'High-level trigger',
|
||||||
atxLevelHigh: 'Active High',
|
atxLevelLow: 'Low-level trigger',
|
||||||
atxLevelLow: 'Active Low',
|
|
||||||
atxLedSensing: 'LED Status Sensing',
|
atxLedSensing: 'LED Status Sensing',
|
||||||
atxLedSensingDesc: 'Detect host power LED to determine power state (optional)',
|
atxHddSensing: 'HDD Activity Sensing',
|
||||||
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',
|
|
||||||
atxWolSettings: 'Wake-on-LAN Settings',
|
atxWolSettings: 'Wake-on-LAN Settings',
|
||||||
atxWolSettingsDesc: 'Configure WOL magic packet sending options',
|
atxWolSettingsDesc: 'Configure WOL magic packet sending options',
|
||||||
atxWolInterface: 'Network Interface',
|
atxWolInterface: 'Network Interface',
|
||||||
|
|||||||
@@ -193,18 +193,15 @@ export default {
|
|||||||
title: '电源控制',
|
title: '电源控制',
|
||||||
description: '控制远程主机的电源状态',
|
description: '控制远程主机的电源状态',
|
||||||
powerState: '电源状态',
|
powerState: '电源状态',
|
||||||
|
hddState: '硬盘活动',
|
||||||
stateOn: '已开机',
|
stateOn: '已开机',
|
||||||
stateOff: '已关机',
|
stateOff: '已关机',
|
||||||
stateUnknown: '未知',
|
stateUnknown: '未知',
|
||||||
|
hddActive: '活动',
|
||||||
|
hddInactive: '空闲',
|
||||||
shortPress: '短按电源',
|
shortPress: '短按电源',
|
||||||
longPress: '长按电源 (强制关机)',
|
longPress: '长按电源 (强制关机)',
|
||||||
reset: '重启',
|
reset: '重启',
|
||||||
confirmShortTitle: '确认短按电源',
|
|
||||||
confirmShortDesc: '这将模拟按下电源按钮,与物理短按电源键效果相同。',
|
|
||||||
confirmLongTitle: '确认强制关机',
|
|
||||||
confirmLongDesc: '这将强制关闭主机,可能导致数据丢失。确定继续吗?',
|
|
||||||
confirmResetTitle: '确认重启',
|
|
||||||
confirmResetDesc: '这将重启主机,可能导致未保存的数据丢失。确定继续吗?',
|
|
||||||
wol: '网络唤醒',
|
wol: '网络唤醒',
|
||||||
wolDescription: '发送 Wake-on-LAN 魔术包以远程开机。',
|
wolDescription: '发送 Wake-on-LAN 魔术包以远程开机。',
|
||||||
macAddress: 'MAC 地址',
|
macAddress: 'MAC 地址',
|
||||||
@@ -547,7 +544,6 @@ export default {
|
|||||||
detecting: '探测中...',
|
detecting: '探测中...',
|
||||||
networkSettings: '网络设置',
|
networkSettings: '网络设置',
|
||||||
msdSettings: 'MSD 设置',
|
msdSettings: 'MSD 设置',
|
||||||
atxSettings: 'ATX 设置',
|
|
||||||
httpSettings: 'HTTP 设置',
|
httpSettings: 'HTTP 设置',
|
||||||
httpPort: 'HTTP 端口',
|
httpPort: 'HTTP 端口',
|
||||||
configureHttpPort: '配置 HTTP 服务器端口',
|
configureHttpPort: '配置 HTTP 服务器端口',
|
||||||
@@ -675,33 +671,22 @@ export default {
|
|||||||
disabled: '已禁用',
|
disabled: '已禁用',
|
||||||
msdDesc: '虚拟存储设备允许您将 ISO 镜像和虚拟驱动器挂载到目标机器。请在主页面的 MSD 面板中管理镜像。',
|
msdDesc: '虚拟存储设备允许您将 ISO 镜像和虚拟驱动器挂载到目标机器。请在主页面的 MSD 面板中管理镜像。',
|
||||||
atxDesc: 'ATX 电源控制允许您远程开关机和重启目标机器。请在主页面的 ATX 面板中控制电源。',
|
atxDesc: 'ATX 电源控制允许您远程开关机和重启目标机器。请在主页面的 ATX 面板中控制电源。',
|
||||||
atxSettingsDesc: '配置 ATX 电源控制硬件绑定',
|
|
||||||
atxEnable: '启用 ATX 控制',
|
|
||||||
atxEnableDesc: '启用后可以远程控制电源和重启按钮',
|
|
||||||
atxPowerButton: '电源按钮',
|
atxPowerButton: '电源按钮',
|
||||||
atxPowerButtonDesc: '用于开机(短按)和强制关机(长按)',
|
|
||||||
atxResetButton: '重启按钮',
|
atxResetButton: '重启按钮',
|
||||||
atxResetButtonDesc: '用于重启目标机器',
|
|
||||||
atxDriver: '驱动类型',
|
atxDriver: '驱动类型',
|
||||||
atxDriverNone: '禁用',
|
atxDriverNone: '禁用',
|
||||||
atxDriverGpio: 'GPIO',
|
atxDriverGpio: 'GPIO',
|
||||||
atxDriverUsbRelay: 'USB LCUS HID继电器',
|
atxDriverUsbRelay: 'USB LCUS HID继电器',
|
||||||
atxDriverSerial: 'USB LCUS 串口继电器',
|
atxDriverSerial: 'USB LCUS 串口继电器',
|
||||||
atxDevice: '设备',
|
atxDevice: '设备',
|
||||||
|
atxGpioChip: 'GPIO 芯片',
|
||||||
atxPin: 'GPIO 引脚',
|
atxPin: 'GPIO 引脚',
|
||||||
atxChannel: '继电器通道',
|
atxChannel: '继电器通道',
|
||||||
atxSharedSerialBaudHint: 'Power 与 Reset 使用同一串口继电器时,波特率由第一个配置统一控制',
|
atxActiveLevel: '触发电平',
|
||||||
atxActiveLevel: '有效电平',
|
atxLevelHigh: '高电平触发',
|
||||||
atxLevelHigh: '高电平有效',
|
atxLevelLow: '低电平触发',
|
||||||
atxLevelLow: '低电平有效',
|
|
||||||
atxLedSensing: 'LED 状态检测',
|
atxLedSensing: 'LED 状态检测',
|
||||||
atxLedSensingDesc: '检测主机电源 LED 以确定电源状态(可选)',
|
atxHddSensing: 'HDD 活动检测',
|
||||||
atxLedEnable: '启用 LED 检测',
|
|
||||||
atxLedEnableDesc: '通过 GPIO 读取电源 LED 状态',
|
|
||||||
atxLedChip: 'GPIO 芯片',
|
|
||||||
atxLedPin: 'GPIO 引脚',
|
|
||||||
atxLedInverted: '反转逻辑',
|
|
||||||
atxLedInvertedDesc: 'LED 亮起时 GPIO 为低电平',
|
|
||||||
atxWolSettings: '网络唤醒设置',
|
atxWolSettings: '网络唤醒设置',
|
||||||
atxWolSettingsDesc: '配置 Wake-on-LAN 魔术包发送选项',
|
atxWolSettingsDesc: '配置 Wake-on-LAN 魔术包发送选项',
|
||||||
atxWolInterface: '网络接口',
|
atxWolInterface: '网络接口',
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ interface AtxState {
|
|||||||
backend: string
|
backend: string
|
||||||
initialized: boolean
|
initialized: boolean
|
||||||
powerOn: boolean
|
powerOn: boolean
|
||||||
|
hddStatus: 'active' | 'inactive' | 'unknown'
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ export interface AtxDeviceInfo {
|
|||||||
backend: string
|
backend: string
|
||||||
initialized: boolean
|
initialized: boolean
|
||||||
power_on: boolean
|
power_on: boolean
|
||||||
|
hdd_status: 'active' | 'inactive' | 'unknown'
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +237,7 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
backend: state.backend,
|
backend: state.backend,
|
||||||
initialized: state.initialized,
|
initialized: state.initialized,
|
||||||
powerOn: state.power_status === 'on',
|
powerOn: state.power_status === 'on',
|
||||||
|
hddStatus: state.hdd_status,
|
||||||
error: null,
|
error: null,
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
@@ -376,6 +379,7 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
backend: data.atx.backend,
|
backend: data.atx.backend,
|
||||||
initialized: data.atx.initialized,
|
initialized: data.atx.initialized,
|
||||||
powerOn: data.atx.power_on,
|
powerOn: data.atx.power_on,
|
||||||
|
hddStatus: data.atx.hdd_status,
|
||||||
error: data.atx.error,
|
error: data.atx.error,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -94,26 +94,29 @@ export enum ActiveLevel {
|
|||||||
Low = "low",
|
Low = "low",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AtxKeyConfig {
|
export interface AtxOutputBinding {
|
||||||
driver: AtxDriverType;
|
enabled: boolean;
|
||||||
device: string;
|
device: string;
|
||||||
pin: number;
|
pin: number;
|
||||||
active_level: ActiveLevel;
|
active_level: ActiveLevel;
|
||||||
baud_rate: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AtxLedConfig {
|
export interface AtxInputBinding {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
gpio_chip: string;
|
device: string;
|
||||||
gpio_pin: number;
|
pin: number;
|
||||||
inverted: boolean;
|
active_level: ActiveLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AtxConfig {
|
export interface AtxConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
power: AtxKeyConfig;
|
driver: AtxDriverType;
|
||||||
reset: AtxKeyConfig;
|
device: string;
|
||||||
led: AtxLedConfig;
|
baud_rate: number;
|
||||||
|
power: AtxOutputBinding;
|
||||||
|
reset: AtxOutputBinding;
|
||||||
|
led: AtxInputBinding;
|
||||||
|
hdd: AtxInputBinding;
|
||||||
wol_interface: string;
|
wol_interface: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,32 +290,36 @@ export interface AppConfig {
|
|||||||
redfish: RedfishConfig;
|
redfish: RedfishConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update for a single ATX key configuration */
|
/** Update for a single ATX output binding */
|
||||||
export interface AtxKeyConfigUpdate {
|
export interface AtxOutputBindingUpdate {
|
||||||
driver?: AtxDriverType;
|
enabled?: boolean;
|
||||||
device?: string;
|
device?: string;
|
||||||
baud_rate?: number;
|
|
||||||
pin?: number;
|
pin?: number;
|
||||||
active_level?: ActiveLevel;
|
active_level?: ActiveLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update for LED sensing configuration */
|
/** Update for ATX GPIO input sensing */
|
||||||
export interface AtxLedConfigUpdate {
|
export interface AtxInputBindingUpdate {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
gpio_chip?: string;
|
device?: string;
|
||||||
gpio_pin?: number;
|
pin?: number;
|
||||||
inverted?: boolean;
|
active_level?: ActiveLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ATX configuration update request */
|
/** ATX configuration update request */
|
||||||
export interface AtxConfigUpdate {
|
export interface AtxConfigUpdate {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
driver?: AtxDriverType;
|
||||||
|
device?: string;
|
||||||
|
baud_rate?: number;
|
||||||
/** Power button configuration */
|
/** Power button configuration */
|
||||||
power?: AtxKeyConfigUpdate;
|
power?: AtxOutputBindingUpdate;
|
||||||
/** Reset button configuration */
|
/** Reset button configuration */
|
||||||
reset?: AtxKeyConfigUpdate;
|
reset?: AtxOutputBindingUpdate;
|
||||||
/** LED sensing configuration */
|
/** LED sensing configuration */
|
||||||
led?: AtxLedConfigUpdate;
|
led?: AtxInputBindingUpdate;
|
||||||
|
/** HDD activity sensing configuration */
|
||||||
|
hdd?: AtxInputBindingUpdate;
|
||||||
/** Network interface for WOL packets (empty = auto) */
|
/** Network interface for WOL packets (empty = auto) */
|
||||||
wol_interface?: string;
|
wol_interface?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1137,29 +1137,41 @@ watch(bindMode, (mode) => {
|
|||||||
|
|
||||||
const atxConfig = ref({
|
const atxConfig = ref({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
power: {
|
|
||||||
driver: 'none' as AtxDriverType,
|
driver: 'none' as AtxDriverType,
|
||||||
device: '',
|
device: '',
|
||||||
|
baud_rate: 9600,
|
||||||
|
power: {
|
||||||
|
enabled: false,
|
||||||
|
device: '',
|
||||||
pin: 1,
|
pin: 1,
|
||||||
active_level: 'high' as ActiveLevel,
|
active_level: 'high' as ActiveLevel,
|
||||||
baud_rate: 9600,
|
|
||||||
},
|
},
|
||||||
reset: {
|
reset: {
|
||||||
driver: 'none' as AtxDriverType,
|
enabled: false,
|
||||||
device: '',
|
device: '',
|
||||||
pin: 1,
|
pin: 1,
|
||||||
active_level: 'high' as ActiveLevel,
|
active_level: 'high' as ActiveLevel,
|
||||||
baud_rate: 9600,
|
|
||||||
},
|
},
|
||||||
led: {
|
led: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
gpio_chip: '',
|
device: '',
|
||||||
gpio_pin: 0,
|
pin: 0,
|
||||||
inverted: false,
|
active_level: 'high' as ActiveLevel,
|
||||||
|
},
|
||||||
|
hdd: {
|
||||||
|
enabled: false,
|
||||||
|
device: '',
|
||||||
|
pin: 0,
|
||||||
|
active_level: 'high' as ActiveLevel,
|
||||||
},
|
},
|
||||||
wol_interface: '',
|
wol_interface: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const atxSaving = ref(false)
|
||||||
|
const atxSaved = ref(false)
|
||||||
|
const wolSaving = ref(false)
|
||||||
|
const wolSaved = ref(false)
|
||||||
|
|
||||||
const atxDevices = ref<AtxDevices>({
|
const atxDevices = ref<AtxDevices>({
|
||||||
gpio_chips: [],
|
gpio_chips: [],
|
||||||
usb_relays: [],
|
usb_relays: [],
|
||||||
@@ -1183,17 +1195,6 @@ const atxDriverOptions = computed(() => {
|
|||||||
: options
|
: 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<EncoderBackendInfo[]>([])
|
const availableBackends = ref<EncoderBackendInfo[]>([])
|
||||||
|
|
||||||
const selectedBackendFormats = computed(() => {
|
const selectedBackendFormats = computed(() => {
|
||||||
@@ -1737,14 +1738,29 @@ async function loadAtxConfig() {
|
|||||||
const config = await configStore.refreshAtx()
|
const config = await configStore.refreshAtx()
|
||||||
atxConfig.value = {
|
atxConfig.value = {
|
||||||
enabled: config.enabled,
|
enabled: config.enabled,
|
||||||
power: { ...config.power },
|
driver: config.enabled ? config.driver : 'none' as AtxDriverType,
|
||||||
reset: { ...config.reset },
|
device: config.device || '',
|
||||||
led: { ...config.led },
|
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 || '',
|
wol_interface: config.wol_interface || '',
|
||||||
}
|
}
|
||||||
clearAtxSerialDeviceConflicts()
|
clearAtxSerialDeviceConflicts()
|
||||||
normalizeAtxRelayChannels()
|
normalizeAtxRelayChannels()
|
||||||
syncSharedAtxSerialBaudRate()
|
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1756,43 +1772,64 @@ async function loadAtxDevices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAtxConfig() {
|
async function saveAtxSettings() {
|
||||||
loading.value = true
|
atxSaving.value = true
|
||||||
saved.value = false
|
atxSaved.value = false
|
||||||
try {
|
try {
|
||||||
normalizeAtxRelayChannels()
|
normalizeAtxRelayChannels()
|
||||||
syncSharedAtxSerialBaudRate()
|
const isGpio = atxConfig.value.driver === 'gpio'
|
||||||
|
const isRelay = ['usbrelay', 'serial'].includes(atxConfig.value.driver)
|
||||||
await configStore.updateAtx({
|
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: {
|
power: {
|
||||||
driver: atxConfig.value.power.driver,
|
enabled: isGpio ? !!atxConfig.value.power.device : isRelay,
|
||||||
device: atxConfig.value.power.device || undefined,
|
device: atxConfig.value.power.device || undefined,
|
||||||
pin: atxConfig.value.power.pin,
|
pin: atxConfig.value.power.pin,
|
||||||
active_level: atxConfig.value.power.active_level,
|
active_level: atxConfig.value.power.active_level,
|
||||||
baud_rate: atxConfig.value.power.baud_rate,
|
|
||||||
},
|
},
|
||||||
reset: {
|
reset: {
|
||||||
driver: atxConfig.value.reset.driver,
|
enabled: isGpio ? !!atxConfig.value.reset.device : isRelay,
|
||||||
device: atxConfig.value.reset.device || undefined,
|
device: atxConfig.value.reset.device || undefined,
|
||||||
pin: atxConfig.value.reset.pin,
|
pin: atxConfig.value.reset.pin,
|
||||||
active_level: atxConfig.value.reset.active_level,
|
active_level: atxConfig.value.reset.active_level,
|
||||||
baud_rate: isSharedAtxSerialRelay.value
|
|
||||||
? atxConfig.value.power.baud_rate
|
|
||||||
: atxConfig.value.reset.baud_rate,
|
|
||||||
},
|
},
|
||||||
led: {
|
led: {
|
||||||
enabled: atxConfig.value.led.enabled,
|
enabled: isGpio && !!atxConfig.value.led.device,
|
||||||
gpio_chip: atxConfig.value.led.gpio_chip || undefined,
|
device: atxConfig.value.led.device || undefined,
|
||||||
gpio_pin: atxConfig.value.led.gpio_pin,
|
pin: atxConfig.value.led.pin,
|
||||||
inverted: atxConfig.value.led.inverted,
|
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
|
atxConfig.value.enabled = atxConfig.value.driver !== 'none'
|
||||||
setTimeout(() => (saved.value = false), 2000)
|
atxSaved.value = true
|
||||||
|
setTimeout(() => (atxSaved.value = false), 2000)
|
||||||
} catch {
|
} catch {
|
||||||
} finally {
|
} 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
|
const reserved = ch9329ReservedSerialDevice.value
|
||||||
if (!reserved) return
|
if (!reserved) return
|
||||||
|
|
||||||
if (atxConfig.value.power.driver === 'serial' && atxConfig.value.power.device === reserved) {
|
if (atxConfig.value.driver === 'serial' && atxConfig.value.device === reserved) {
|
||||||
atxConfig.value.power.device = ''
|
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() {
|
function normalizeAtxRelayChannels() {
|
||||||
for (const key of [atxConfig.value.power, atxConfig.value.reset]) {
|
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
|
key.pin = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (atxConfig.value.driver !== 'gpio') {
|
||||||
|
atxConfig.value.led.enabled = false
|
||||||
|
atxConfig.value.hdd.enabled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -1852,22 +1885,10 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [atxConfig.value.power.driver, atxConfig.value.reset.driver],
|
() => atxConfig.value.driver,
|
||||||
() => {
|
() => {
|
||||||
normalizeAtxRelayChannels()
|
normalizeAtxRelayChannels()
|
||||||
},
|
clearAtxSerialDeviceConflicts()
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [
|
|
||||||
atxConfig.value.power.driver,
|
|
||||||
atxConfig.value.power.device,
|
|
||||||
atxConfig.value.power.baud_rate,
|
|
||||||
atxConfig.value.reset.driver,
|
|
||||||
atxConfig.value.reset.device,
|
|
||||||
],
|
|
||||||
() => {
|
|
||||||
syncSharedAtxSerialBaudRate()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3947,83 +3968,40 @@ watch(isWindows, () => {
|
|||||||
|
|
||||||
<!-- ATX Section -->
|
<!-- ATX Section -->
|
||||||
<div v-show="activeSection === 'atx'" class="space-y-6">
|
<div v-show="activeSection === 'atx'" class="space-y-6">
|
||||||
<!-- Enable ATX -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
<CardContent class="space-y-4 pt-6">
|
||||||
<div class="space-y-1.5">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<CardTitle>{{ t('settings.atxSettings') }}</CardTitle>
|
<div class="flex min-w-0 flex-1 items-center gap-3">
|
||||||
<CardDescription>{{ t('settings.atxSettingsDesc') }}</CardDescription>
|
<Label for="atx-driver" class="shrink-0">{{ t('settings.atxDriver') }}</Label>
|
||||||
|
<select id="atx-driver" v-model="atxConfig.driver" class="h-9 w-full max-w-xs px-3 rounded-md border border-input bg-background text-sm">
|
||||||
|
<option v-for="option in atxDriverOptions" :key="option.value" :value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadAtxDevices">
|
<Button variant="ghost" size="icon" class="h-9 w-9 shrink-0" :aria-label="t('common.refresh')" @click="loadAtxDevices">
|
||||||
<RefreshCw class="h-4 w-4" />
|
<RefreshCw class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="space-y-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
<Label for="atx-enabled">{{ t('settings.atxEnable') }}</Label>
|
|
||||||
<p class="text-xs text-muted-foreground">{{ t('settings.atxEnableDesc') }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
|
||||||
id="atx-enabled"
|
|
||||||
v-model="atxConfig.enabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Power Button Config -->
|
<div v-if="['usbrelay', 'serial'].includes(atxConfig.driver)" class="grid gap-4 sm:grid-cols-2">
|
||||||
<Card v-if="atxConfig.enabled">
|
<div v-if="['usbrelay', 'serial'].includes(atxConfig.driver)" class="space-y-2">
|
||||||
<CardHeader>
|
<Label for="atx-device">{{ t('settings.atxDevice') }}</Label>
|
||||||
<CardTitle>{{ t('settings.atxPowerButton') }}</CardTitle>
|
<select id="atx-device" v-model="atxConfig.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||||
<CardDescription>{{ t('settings.atxPowerButtonDesc') }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="space-y-4">
|
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="power-driver">{{ t('settings.atxDriver') }}</Label>
|
|
||||||
<select id="power-driver" v-model="atxConfig.power.driver" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
|
||||||
<option v-for="option in atxDriverOptions" :key="option.value" :value="option.value">
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="power-device">{{ t('settings.atxDevice') }}</Label>
|
|
||||||
<select id="power-device" v-model="atxConfig.power.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="atxConfig.power.driver === 'none'">
|
|
||||||
<option value="">{{ t('settings.selectDevice') }}</option>
|
<option value="">{{ t('settings.selectDevice') }}</option>
|
||||||
<option
|
<option
|
||||||
v-for="dev in getAtxDevicesForDriver(atxConfig.power.driver)"
|
v-for="dev in getAtxDevicesForDriver(atxConfig.driver)"
|
||||||
:key="dev"
|
:key="dev"
|
||||||
:value="dev"
|
:value="dev"
|
||||||
:disabled="atxConfig.power.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
:disabled="atxConfig.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
||||||
>
|
>
|
||||||
{{ formatAtxDeviceLabel(atxConfig.power.driver, dev) }}
|
{{ formatAtxDeviceLabel(atxConfig.driver, dev) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="atxConfig.driver === 'serial'" class="space-y-2">
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<Label for="atx-baudrate">{{ t('settings.baudRate') }}</Label>
|
||||||
<div class="space-y-2">
|
<select id="atx-baudrate" v-model.number="atxConfig.baud_rate" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||||
<Label for="power-pin">{{ ['usbrelay', 'serial'].includes(atxConfig.power.driver) ? t('settings.atxChannel') : t('settings.atxPin') }}</Label>
|
|
||||||
<Input
|
|
||||||
id="power-pin"
|
|
||||||
type="number"
|
|
||||||
v-model.number="atxConfig.power.pin"
|
|
||||||
:min="['usbrelay', 'serial'].includes(atxConfig.power.driver) ? 1 : 0"
|
|
||||||
:disabled="atxConfig.power.driver === 'none'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="atxConfig.power.driver === 'gpio'" class="space-y-2">
|
|
||||||
<Label for="power-level">{{ t('settings.atxActiveLevel') }}</Label>
|
|
||||||
<select id="power-level" v-model="atxConfig.power.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
|
||||||
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
|
||||||
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div v-if="atxConfig.power.driver === 'serial'" class="space-y-2">
|
|
||||||
<Label for="power-baudrate">{{ t('settings.baudRate') }}</Label>
|
|
||||||
<select id="power-baudrate" v-model.number="atxConfig.power.baud_rate" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
|
||||||
<option :value="9600">9600</option>
|
<option :value="9600">9600</option>
|
||||||
<option :value="19200">19200</option>
|
<option :value="19200">19200</option>
|
||||||
<option :value="38400">38400</option>
|
<option :value="38400">38400</option>
|
||||||
@@ -4032,128 +4010,138 @@ watch(isWindows, () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Reset Button Config -->
|
<template v-if="atxConfig.driver === 'gpio'">
|
||||||
<Card v-if="atxConfig.enabled">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{{ t('settings.atxResetButton') }}</CardTitle>
|
|
||||||
<CardDescription>{{ t('settings.atxResetButtonDesc') }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="space-y-4">
|
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="reset-driver">{{ t('settings.atxDriver') }}</Label>
|
|
||||||
<select id="reset-driver" v-model="atxConfig.reset.driver" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
|
||||||
<option v-for="option in atxDriverOptions" :key="option.value" :value="option.value">
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="reset-device">{{ t('settings.atxDevice') }}</Label>
|
|
||||||
<select id="reset-device" v-model="atxConfig.reset.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="atxConfig.reset.driver === 'none'">
|
|
||||||
<option value="">{{ t('settings.selectDevice') }}</option>
|
|
||||||
<option
|
|
||||||
v-for="dev in getAtxDevicesForDriver(atxConfig.reset.driver)"
|
|
||||||
:key="dev"
|
|
||||||
:value="dev"
|
|
||||||
:disabled="atxConfig.reset.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
|
||||||
>
|
|
||||||
{{ formatAtxDeviceLabel(atxConfig.reset.driver, dev) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="reset-pin">{{ ['usbrelay', 'serial'].includes(atxConfig.reset.driver) ? t('settings.atxChannel') : t('settings.atxPin') }}</Label>
|
|
||||||
<Input
|
|
||||||
id="reset-pin"
|
|
||||||
type="number"
|
|
||||||
v-model.number="atxConfig.reset.pin"
|
|
||||||
:min="['usbrelay', 'serial'].includes(atxConfig.reset.driver) ? 1 : 0"
|
|
||||||
:disabled="atxConfig.reset.driver === 'none'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="atxConfig.reset.driver === 'gpio'" class="space-y-2">
|
|
||||||
<Label for="reset-level">{{ t('settings.atxActiveLevel') }}</Label>
|
|
||||||
<select id="reset-level" v-model="atxConfig.reset.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
|
||||||
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
|
||||||
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div v-if="atxConfig.reset.driver === 'serial'" class="space-y-2">
|
|
||||||
<Label for="reset-baudrate">{{ t('settings.baudRate') }}</Label>
|
|
||||||
<select
|
|
||||||
id="reset-baudrate"
|
|
||||||
v-model.number="atxConfig.reset.baud_rate"
|
|
||||||
class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
|
||||||
:disabled="isSharedAtxSerialRelay"
|
|
||||||
>
|
|
||||||
<option :value="9600">9600</option>
|
|
||||||
<option :value="19200">19200</option>
|
|
||||||
<option :value="38400">38400</option>
|
|
||||||
<option :value="57600">57600</option>
|
|
||||||
<option :value="115200">115200</option>
|
|
||||||
</select>
|
|
||||||
<p v-if="isSharedAtxSerialRelay" class="text-xs text-muted-foreground">
|
|
||||||
{{ t('settings.atxSharedSerialBaudHint') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- LED Sensing Config -->
|
|
||||||
<Card v-if="atxConfig.enabled && !isWindows">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{{ t('settings.atxLedSensing') }}</CardTitle>
|
|
||||||
<CardDescription>{{ t('settings.atxLedSensingDesc') }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="space-y-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
<Label for="led-enabled">{{ t('settings.atxLedEnable') }}</Label>
|
|
||||||
<p class="text-xs text-muted-foreground">{{ t('settings.atxLedEnableDesc') }}</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="led-enabled"
|
|
||||||
v-model="atxConfig.led.enabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template v-if="atxConfig.led.enabled">
|
|
||||||
<Separator />
|
<Separator />
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-3 rounded-md border p-3">
|
||||||
|
<Label>{{ t('settings.atxPowerButton') }}</Label>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="led-chip">{{ t('settings.atxLedChip') }}</Label>
|
<Label for="power-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||||
<select id="led-chip" v-model="atxConfig.led.gpio_chip" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
<select id="power-device" v-model="atxConfig.power.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||||
<option value="">{{ t('settings.selectDevice') }}</option>
|
<option value="">{{ t('settings.atxDriverNone') }}</option>
|
||||||
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="led-pin">{{ t('settings.atxLedPin') }}</Label>
|
<Label for="power-pin">{{ t('settings.atxPin') }}</Label>
|
||||||
<Input id="led-pin" type="number" v-model.number="atxConfig.led.gpio_pin" min="0" />
|
<Input id="power-pin" type="number" v-model.number="atxConfig.power.pin" min="0" :disabled="!atxConfig.power.device" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="power-active-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||||
|
<select id="power-active-level" v-model="atxConfig.power.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!atxConfig.power.device">
|
||||||
|
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||||
|
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
<Label for="led-inverted">{{ t('settings.atxLedInverted') }}</Label>
|
|
||||||
<p class="text-xs text-muted-foreground">{{ t('settings.atxLedInvertedDesc') }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
|
||||||
id="led-inverted"
|
<div class="space-y-3 rounded-md border p-3">
|
||||||
v-model="atxConfig.led.inverted"
|
<Label>{{ t('settings.atxResetButton') }}</Label>
|
||||||
/>
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="reset-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||||
|
<select id="reset-device" v-model="atxConfig.reset.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||||
|
<option value="">{{ t('settings.atxDriverNone') }}</option>
|
||||||
|
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="reset-pin">{{ t('settings.atxPin') }}</Label>
|
||||||
|
<Input id="reset-pin" type="number" v-model.number="atxConfig.reset.pin" min="0" :disabled="!atxConfig.reset.device" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="reset-active-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||||
|
<select id="reset-active-level" v-model="atxConfig.reset.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!atxConfig.reset.device">
|
||||||
|
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||||
|
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 rounded-md border p-3">
|
||||||
|
<Label>{{ t('settings.atxLedSensing') }}</Label>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="led-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||||
|
<select id="led-device" v-model="atxConfig.led.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||||
|
<option value="">{{ t('settings.atxDriverNone') }}</option>
|
||||||
|
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="led-pin">{{ t('settings.atxPin') }}</Label>
|
||||||
|
<Input id="led-pin" type="number" v-model.number="atxConfig.led.pin" min="0" :disabled="!atxConfig.led.device" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="led-active-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||||
|
<select id="led-active-level" v-model="atxConfig.led.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!atxConfig.led.device">
|
||||||
|
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||||
|
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 rounded-md border p-3">
|
||||||
|
<Label>{{ t('settings.atxHddSensing') }}</Label>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="hdd-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||||
|
<select id="hdd-device" v-model="atxConfig.hdd.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||||
|
<option value="">{{ t('settings.atxDriverNone') }}</option>
|
||||||
|
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="hdd-pin">{{ t('settings.atxPin') }}</Label>
|
||||||
|
<Input id="hdd-pin" type="number" v-model.number="atxConfig.hdd.pin" min="0" :disabled="!atxConfig.hdd.device" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="hdd-active-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||||
|
<select id="hdd-active-level" v-model="atxConfig.hdd.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!atxConfig.hdd.device">
|
||||||
|
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||||
|
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="['usbrelay', 'serial'].includes(atxConfig.driver)">
|
||||||
|
<Separator />
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-3 rounded-md border p-3">
|
||||||
|
<Label>{{ t('settings.atxPowerButton') }}</Label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="power-channel">{{ t('settings.atxChannel') }}</Label>
|
||||||
|
<Input id="power-channel" type="number" v-model.number="atxConfig.power.pin" min="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 rounded-md border p-3">
|
||||||
|
<Label>{{ t('settings.atxResetButton') }}</Label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="reset-channel">{{ t('settings.atxChannel') }}</Label>
|
||||||
|
<Input id="reset-channel" type="number" v-model.number="atxConfig.reset.pin" min="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button :disabled="atxSaving" @click="saveAtxSettings">
|
||||||
|
<Loader2 v-if="atxSaving" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="atxSaved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ atxSaving ? t('actionbar.applying') : atxSaved ? t('common.success') : t('common.save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- WOL Config -->
|
<!-- WOL Config -->
|
||||||
<Card v-if="atxConfig.enabled">
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{{ t('settings.atxWolSettings') }}</CardTitle>
|
<CardTitle>{{ t('settings.atxWolSettings') }}</CardTitle>
|
||||||
<CardDescription>{{ t('settings.atxWolSettingsDesc') }}</CardDescription>
|
<CardDescription>{{ t('settings.atxWolSettingsDesc') }}</CardDescription>
|
||||||
@@ -4170,11 +4158,9 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Save Button -->
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<Button :disabled="loading" @click="saveAtxConfig">
|
<Button :disabled="wolSaving" @click="saveWolSettings">
|
||||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
<Loader2 v-if="wolSaving" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="wolSaved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ wolSaving ? t('actionbar.applying') : wolSaved ? t('common.success') : t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user