feat(atx): merge serial relay support from #223

This commit is contained in:
mofeng-git
2026-02-20 14:11:00 +08:00
parent ce622e4492
commit 251a1e00c4
6 changed files with 202 additions and 16 deletions

View File

@@ -4,6 +4,7 @@
//! Each executor handles one button (power or reset) with its own hardware binding. //! Each executor handles one button (power or reset) with its own hardware binding.
use gpio_cdev::{Chip, LineHandle, LineRequestFlags}; use gpio_cdev::{Chip, LineHandle, LineRequestFlags};
use serialport::SerialPort;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::Write; use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@@ -38,6 +39,8 @@ pub struct AtxKeyExecutor {
gpio_handle: Mutex<Option<LineHandle>>, gpio_handle: Mutex<Option<LineHandle>>,
/// Cached USB relay file handle to avoid repeated open/close syscalls /// Cached USB relay file handle to avoid repeated open/close syscalls
usb_relay_handle: Mutex<Option<File>>, usb_relay_handle: Mutex<Option<File>>,
/// Cached Serial port handle
serial_handle: Mutex<Option<Box<dyn SerialPort>>>,
initialized: AtomicBool, initialized: AtomicBool,
} }
@@ -48,6 +51,7 @@ impl AtxKeyExecutor {
config, config,
gpio_handle: Mutex::new(None), gpio_handle: Mutex::new(None),
usb_relay_handle: Mutex::new(None), usb_relay_handle: Mutex::new(None),
serial_handle: Mutex::new(None),
initialized: AtomicBool::new(false), initialized: AtomicBool::new(false),
} }
} }
@@ -72,6 +76,7 @@ impl AtxKeyExecutor {
match self.config.driver { match self.config.driver {
AtxDriverType::Gpio => self.init_gpio().await?, AtxDriverType::Gpio => self.init_gpio().await?,
AtxDriverType::UsbRelay => self.init_usb_relay().await?, AtxDriverType::UsbRelay => self.init_usb_relay().await?,
AtxDriverType::Serial => self.init_serial().await?,
AtxDriverType::None => {} AtxDriverType::None => {}
} }
@@ -134,6 +139,36 @@ impl AtxKeyExecutor {
Ok(()) Ok(())
} }
/// Initialize Serial relay backend
async fn init_serial(&self) -> Result<()> {
info!(
"Initializing Serial relay ATX executor on {} channel {}",
self.config.device, self.config.pin
);
let baud_rate = if self.config.baud_rate > 0 {
self.config.baud_rate
} else {
9600
};
let port = serialport::new(&self.config.device, baud_rate)
.timeout(Duration::from_millis(100))
.open()
.map_err(|e| AppError::Internal(format!("Serial port open failed: {}", e)))?;
*self.serial_handle.lock().unwrap() = Some(port);
// Ensure relay is off initially
self.send_serial_relay_command(false)?;
debug!(
"Serial relay channel {} configured successfully",
self.config.pin
);
Ok(())
}
/// Pulse the button for the specified duration /// Pulse the button for the specified duration
pub async fn pulse(&self, duration: Duration) -> Result<()> { pub async fn pulse(&self, duration: Duration) -> Result<()> {
if !self.is_configured() { if !self.is_configured() {
@@ -147,6 +182,7 @@ impl AtxKeyExecutor {
match self.config.driver { match self.config.driver {
AtxDriverType::Gpio => self.pulse_gpio(duration).await, AtxDriverType::Gpio => self.pulse_gpio(duration).await,
AtxDriverType::UsbRelay => self.pulse_usb_relay(duration).await, AtxDriverType::UsbRelay => self.pulse_usb_relay(duration).await,
AtxDriverType::Serial => self.pulse_serial(duration).await,
AtxDriverType::None => Ok(()), AtxDriverType::None => Ok(()),
} }
} }
@@ -220,6 +256,52 @@ impl AtxKeyExecutor {
Ok(()) Ok(())
} }
/// Pulse Serial relay
async fn pulse_serial(&self, duration: Duration) -> Result<()> {
info!(
"Pulse serial relay on {} pin {}",
self.config.device, self.config.pin
);
// Turn relay on
self.send_serial_relay_command(true)?;
// Wait for duration
sleep(duration).await;
// Turn relay off
self.send_serial_relay_command(false)?;
Ok(())
}
/// Send Serial relay command using cached handle
fn send_serial_relay_command(&self, on: bool) -> Result<()> {
// user config pin should be 1 for most LCUS modules (A0 01 01 A2)
// if user set 0, it will send A0 00 01 A1 which might not work
let channel = self.config.pin as u8;
// LCUS-Type Protocol
// Frame: [StopByte(A0), Channel, State, Checksum]
// Checksum = A0 + channel + state
let state = if on { 1 } else { 0 };
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
// Example for Channel 1:
// ON: A0 01 01 A2
// OFF: A0 01 00 A1
let cmd = [0xA0, channel, state, checksum];
let mut guard = self.serial_handle.lock().unwrap();
let port = guard
.as_mut()
.ok_or_else(|| AppError::Internal("Serial relay not initialized".to_string()))?;
port.write_all(&cmd)
.map_err(|e| AppError::Internal(format!("Serial relay write failed: {}", e)))?;
Ok(())
}
/// Shutdown the executor /// Shutdown the executor
pub async fn shutdown(&mut self) -> Result<()> { pub async fn shutdown(&mut self) -> Result<()> {
if !self.is_initialized() { if !self.is_initialized() {
@@ -237,6 +319,12 @@ impl AtxKeyExecutor {
// Release USB relay handle // Release USB relay handle
*self.usb_relay_handle.lock().unwrap() = None; *self.usb_relay_handle.lock().unwrap() = None;
} }
AtxDriverType::Serial => {
// Ensure relay is off before closing handle
let _ = self.send_serial_relay_command(false);
// Release Serial relay handle
*self.serial_handle.lock().unwrap() = None;
}
AtxDriverType::None => {} AtxDriverType::None => {}
} }
@@ -256,6 +344,12 @@ impl Drop for AtxKeyExecutor {
let _ = self.send_usb_relay_command(false); let _ = self.send_usb_relay_command(false);
} }
*self.usb_relay_handle.lock().unwrap() = None; *self.usb_relay_handle.lock().unwrap() = None;
// Ensure Serial relay is off and handle released
if self.config.driver == AtxDriverType::Serial && self.is_initialized() {
let _ = self.send_serial_relay_command(false);
}
*self.serial_handle.lock().unwrap() = None;
} }
} }
@@ -278,6 +372,7 @@ mod tests {
device: "/dev/gpiochip0".to_string(), device: "/dev/gpiochip0".to_string(),
pin: 5, pin: 5,
active_level: ActiveLevel::High, active_level: ActiveLevel::High,
baud_rate: 9600,
}; };
let executor = AtxKeyExecutor::new(config); let executor = AtxKeyExecutor::new(config);
assert!(executor.is_configured()); assert!(executor.is_configured());
@@ -291,6 +386,20 @@ mod tests {
device: "/dev/hidraw0".to_string(), device: "/dev/hidraw0".to_string(),
pin: 0, pin: 0,
active_level: ActiveLevel::High, // Ignored for USB relay active_level: ActiveLevel::High, // Ignored for USB relay
baud_rate: 9600,
};
let executor = AtxKeyExecutor::new(config);
assert!(executor.is_configured());
}
#[test]
fn test_executor_with_serial_config() {
let config = AtxKeyConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
pin: 1,
active_level: ActiveLevel::High, // Ignored
baud_rate: 9600,
}; };
let executor = AtxKeyExecutor::new(config); let executor = AtxKeyExecutor::new(config);
assert!(executor.is_configured()); assert!(executor.is_configured());

View File

@@ -28,12 +28,14 @@
//! device: "/dev/gpiochip0".to_string(), //! device: "/dev/gpiochip0".to_string(),
//! pin: 5, //! pin: 5,
//! active_level: ActiveLevel::High, //! active_level: ActiveLevel::High,
//! baud_rate: 9600,
//! }, //! },
//! reset: AtxKeyConfig { //! reset: AtxKeyConfig {
//! driver: AtxDriverType::UsbRelay, //! driver: AtxDriverType::UsbRelay,
//! device: "/dev/hidraw0".to_string(), //! device: "/dev/hidraw0".to_string(),
//! pin: 0, //! pin: 0,
//! active_level: ActiveLevel::High, //! active_level: ActiveLevel::High,
//! baud_rate: 9600,
//! }, //! },
//! led: Default::default(), //! led: Default::default(),
//! }; //! };
@@ -72,12 +74,15 @@ pub fn discover_devices() -> AtxDevices {
devices.gpio_chips.push(format!("/dev/{}", name_str)); devices.gpio_chips.push(format!("/dev/{}", name_str));
} else if name_str.starts_with("hidraw") { } else if name_str.starts_with("hidraw") {
devices.usb_relays.push(format!("/dev/{}", name_str)); devices.usb_relays.push(format!("/dev/{}", name_str));
} else if name_str.starts_with("ttyUSB") || name_str.starts_with("ttyACM") {
devices.serial_ports.push(format!("/dev/{}", name_str));
} }
} }
} }
devices.gpio_chips.sort(); devices.gpio_chips.sort();
devices.usb_relays.sort(); devices.usb_relays.sort();
devices.serial_ports.sort();
devices devices
} }

View File

@@ -28,6 +28,8 @@ pub enum AtxDriverType {
Gpio, Gpio,
/// USB HID relay module /// USB HID relay module
UsbRelay, UsbRelay,
/// Serial/COM port relay (taobao LCUS type)
Serial,
/// Disabled / Not configured /// Disabled / Not configured
#[default] #[default]
None, None,
@@ -48,7 +50,7 @@ pub enum ActiveLevel {
/// Configuration for a single ATX key (power or reset) /// Configuration for a single ATX key (power or reset)
/// This is the "four-tuple" configuration: (driver, device, pin/channel, level) /// This is the "four-tuple" configuration: (driver, device, pin/channel, level)
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)] #[serde(default)]
pub struct AtxKeyConfig { pub struct AtxKeyConfig {
/// Driver type (GPIO or USB Relay) /// Driver type (GPIO or USB Relay)
@@ -60,9 +62,24 @@ pub struct AtxKeyConfig {
/// Pin or channel number: /// Pin or channel number:
/// - For GPIO: GPIO pin number /// - For GPIO: GPIO pin number
/// - For USB Relay: relay channel (0-based) /// - For USB Relay: relay channel (0-based)
/// - For Serial Relay (LCUS): relay channel (1-based)
pub pin: u32, pub pin: u32,
/// Active level (only applicable to GPIO, ignored for USB Relay) /// Active level (only applicable to GPIO, ignored for USB Relay)
pub active_level: ActiveLevel, pub active_level: ActiveLevel,
/// Baud rate for serial relay
pub baud_rate: u32,
}
impl Default for AtxKeyConfig {
fn default() -> Self {
Self {
driver: AtxDriverType::None,
device: String::new(),
pin: 0,
active_level: ActiveLevel::High,
baud_rate: 9600,
}
}
} }
impl AtxKeyConfig { impl AtxKeyConfig {
@@ -130,12 +147,24 @@ pub enum AtxAction {
/// Available ATX devices for discovery /// Available ATX devices for discovery
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AtxDevices { pub struct AtxDevices {
/// Available GPIO chips (/dev/gpiochip*) /// Available GPIO chips (/dev/gpiochip*)
pub gpio_chips: Vec<String>, pub gpio_chips: Vec<String>,
/// Available USB HID relay devices (/dev/hidraw*) /// Available USB HID relay devices (/dev/hidraw*)
pub usb_relays: Vec<String>, pub usb_relays: Vec<String>,
/// Available Serial ports (/dev/ttyUSB* or /dev/ttyACM*)
pub serial_ports: Vec<String>,
}
impl Default for AtxDevices {
fn default() -> Self {
Self {
gpio_chips: Vec::new(),
usb_relays: Vec::new(),
serial_ports: Vec::new(),
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -34,7 +34,9 @@ const activeTab = ref('atx')
// ATX state // ATX state
const powerState = ref<'on' | 'off' | 'unknown'>('unknown') const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
const confirmAction = ref<'short' | 'long' | 'reset' | null>(null) // Decouple action data from dialog visibility to prevent race conditions
const pendingAction = ref<'short' | 'long' | 'reset' | null>(null)
const confirmDialogOpen = ref(false)
// WOL state // WOL state
const wolMacAddress = ref('') const wolMacAddress = ref('')
@@ -58,15 +60,21 @@ const powerStateText = computed(() => {
} }
}) })
function requestAction(action: 'short' | 'long' | 'reset') {
pendingAction.value = action
confirmDialogOpen.value = true
}
function handleAction() { function handleAction() {
if (confirmAction.value === 'short') emit('powerShort') console.log('[AtxPopover] Confirming action:', pendingAction.value)
else if (confirmAction.value === 'long') emit('powerLong') if (pendingAction.value === 'short') emit('powerShort')
else if (confirmAction.value === 'reset') emit('reset') else if (pendingAction.value === 'long') emit('powerLong')
confirmAction.value = null else if (pendingAction.value === 'reset') emit('reset')
confirmDialogOpen.value = false
} }
const confirmTitle = computed(() => { const confirmTitle = computed(() => {
switch (confirmAction.value) { switch (pendingAction.value) {
case 'short': return t('atx.confirmShortTitle') case 'short': return t('atx.confirmShortTitle')
case 'long': return t('atx.confirmLongTitle') case 'long': return t('atx.confirmLongTitle')
case 'reset': return t('atx.confirmResetTitle') case 'reset': return t('atx.confirmResetTitle')
@@ -75,7 +83,7 @@ const confirmTitle = computed(() => {
}) })
const confirmDescription = computed(() => { const confirmDescription = computed(() => {
switch (confirmAction.value) { switch (pendingAction.value) {
case 'short': return t('atx.confirmShortDesc') case 'short': return t('atx.confirmShortDesc')
case 'long': return t('atx.confirmLongDesc') case 'long': return t('atx.confirmLongDesc')
case 'reset': return t('atx.confirmResetDesc') case 'reset': return t('atx.confirmResetDesc')
@@ -178,7 +186,7 @@ watch(
variant="outline" variant="outline"
size="sm" size="sm"
class="w-full justify-start gap-2 h-8 text-xs" class="w-full justify-start gap-2 h-8 text-xs"
@click="confirmAction = 'short'" @click="requestAction('short')"
> >
<Power class="h-3.5 w-3.5" /> <Power class="h-3.5 w-3.5" />
{{ t('atx.shortPress') }} {{ t('atx.shortPress') }}
@@ -188,7 +196,7 @@ watch(
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" 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"
@click="confirmAction = 'long'" @click="requestAction('long')"
> >
<CircleDot class="h-3.5 w-3.5" /> <CircleDot class="h-3.5 w-3.5" />
{{ t('atx.longPress') }} {{ t('atx.longPress') }}
@@ -198,7 +206,7 @@ watch(
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" 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"
@click="confirmAction = 'reset'" @click="requestAction('reset')"
> >
<RotateCcw class="h-3.5 w-3.5" /> <RotateCcw class="h-3.5 w-3.5" />
{{ t('atx.reset') }} {{ t('atx.reset') }}
@@ -260,7 +268,7 @@ watch(
</div> </div>
<!-- Confirm Dialog --> <!-- Confirm Dialog -->
<AlertDialog :open="!!confirmAction" @update:open="confirmAction = null"> <AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{{ confirmTitle }}</AlertDialogTitle> <AlertDialogTitle>{{ confirmTitle }}</AlertDialogTitle>

View File

@@ -118,6 +118,8 @@ export enum AtxDriverType {
Gpio = "gpio", Gpio = "gpio",
/** USB HID relay module */ /** USB HID relay module */
UsbRelay = "usbrelay", UsbRelay = "usbrelay",
/** Serial/COM port relay (LCUS type) */
Serial = "serial",
/** Disabled / Not configured */ /** Disabled / Not configured */
None = "none", None = "none",
} }
@@ -151,6 +153,8 @@ export interface AtxKeyConfig {
pin: number; pin: number;
/** Active level (only applicable to GPIO, ignored for USB Relay) */ /** Active level (only applicable to GPIO, ignored for USB Relay) */
active_level: ActiveLevel; active_level: ActiveLevel;
/** Baud rate for serial relay (start with 9600) */
baud_rate: number;
} }
/** LED sensing configuration (optional) */ /** LED sensing configuration (optional) */
@@ -411,6 +415,7 @@ export interface AppConfig {
export interface AtxKeyConfigUpdate { export interface AtxKeyConfigUpdate {
driver?: AtxDriverType; driver?: AtxDriverType;
device?: string; device?: string;
baud_rate?: number;
pin?: number; pin?: number;
active_level?: ActiveLevel; active_level?: ActiveLevel;
} }
@@ -439,6 +444,8 @@ export interface AtxConfigUpdate {
/** Available ATX devices for discovery */ /** Available ATX devices for discovery */
export interface AtxDevices { export interface AtxDevices {
/** Available GPIO chips (/dev/gpiochip*) */ /** Available GPIO chips (/dev/gpiochip*) */
/** Available Serial ports (/dev/ttyUSB*) */
serial_ports: string[];
gpio_chips: string[]; gpio_chips: string[];
/** Available USB HID relay devices (/dev/hidraw*) */ /** Available USB HID relay devices (/dev/hidraw*) */
usb_relays: string[]; usb_relays: string[];
@@ -681,4 +688,3 @@ export interface WebConfigUpdate {
bind_address?: string; bind_address?: string;
https_enabled?: boolean; https_enabled?: boolean;
} }

View File

@@ -598,12 +598,14 @@ const atxConfig = ref({
device: '', device: '',
pin: 0, pin: 0,
active_level: 'high' as ActiveLevel, active_level: 'high' as ActiveLevel,
baud_rate: 9600,
}, },
reset: { reset: {
driver: 'none' as AtxDriverType, driver: 'none' as AtxDriverType,
device: '', device: '',
pin: 0, pin: 0,
active_level: 'high' as ActiveLevel, active_level: 'high' as ActiveLevel,
baud_rate: 9600,
}, },
led: { led: {
enabled: false, enabled: false,
@@ -618,6 +620,7 @@ const atxConfig = ref({
const atxDevices = ref<AtxDevices>({ const atxDevices = ref<AtxDevices>({
gpio_chips: [], gpio_chips: [],
usb_relays: [], usb_relays: [],
serial_ports: [],
}) })
// Encoder backend // Encoder backend
@@ -1175,12 +1178,14 @@ async function saveAtxConfig() {
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, driver: atxConfig.value.reset.driver,
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: atxConfig.value.reset.baud_rate,
}, },
led: { led: {
enabled: atxConfig.value.led.enabled, enabled: atxConfig.value.led.enabled,
@@ -1202,6 +1207,8 @@ async function saveAtxConfig() {
function getAtxDevicesForDriver(driver: string): string[] { function getAtxDevicesForDriver(driver: string): string[] {
if (driver === 'gpio') { if (driver === 'gpio') {
return atxDevices.value.gpio_chips return atxDevices.value.gpio_chips
} else if (driver === 'serial') {
return atxDevices.value.serial_ports
} else if (driver === 'usbrelay') { } else if (driver === 'usbrelay') {
return atxDevices.value.usb_relays return atxDevices.value.usb_relays
} }
@@ -2474,6 +2481,7 @@ watch(() => config.value.hid_backend, async () => {
<option value="none">{{ t('settings.atxDriverNone') }}</option> <option value="none">{{ t('settings.atxDriverNone') }}</option>
<option value="gpio">{{ t('settings.atxDriverGpio') }}</option> <option value="gpio">{{ t('settings.atxDriverGpio') }}</option>
<option value="usbrelay">{{ t('settings.atxDriverUsbRelay') }}</option> <option value="usbrelay">{{ t('settings.atxDriverUsbRelay') }}</option>
<option value="serial">Serial (LCUS)</option>
</select> </select>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
@@ -2486,7 +2494,7 @@ watch(() => config.value.hid_backend, async () => {
</div> </div>
<div class="grid gap-4 sm:grid-cols-2"> <div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2"> <div class="space-y-2">
<Label for="power-pin">{{ atxConfig.power.driver === 'usbrelay' ? t('settings.atxChannel') : t('settings.atxPin') }}</Label> <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="0" :disabled="atxConfig.power.driver === 'none'" /> <Input id="power-pin" type="number" v-model.number="atxConfig.power.pin" min="0" :disabled="atxConfig.power.driver === 'none'" />
</div> </div>
<div v-if="atxConfig.power.driver === 'gpio'" class="space-y-2"> <div v-if="atxConfig.power.driver === 'gpio'" class="space-y-2">
@@ -2496,6 +2504,16 @@ watch(() => config.value.hid_backend, async () => {
<option value="low">{{ t('settings.atxLevelLow') }}</option> <option value="low">{{ t('settings.atxLevelLow') }}</option>
</select> </select>
</div> </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="19200">19200</option>
<option :value="38400">38400</option>
<option :value="57600">57600</option>
<option :value="115200">115200</option>
</select>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -2514,6 +2532,7 @@ watch(() => config.value.hid_backend, async () => {
<option value="none">{{ t('settings.atxDriverNone') }}</option> <option value="none">{{ t('settings.atxDriverNone') }}</option>
<option value="gpio">{{ t('settings.atxDriverGpio') }}</option> <option value="gpio">{{ t('settings.atxDriverGpio') }}</option>
<option value="usbrelay">{{ t('settings.atxDriverUsbRelay') }}</option> <option value="usbrelay">{{ t('settings.atxDriverUsbRelay') }}</option>
<option value="serial">Serial (LCUS)</option>
</select> </select>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
@@ -2526,7 +2545,7 @@ watch(() => config.value.hid_backend, async () => {
</div> </div>
<div class="grid gap-4 sm:grid-cols-2"> <div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2"> <div class="space-y-2">
<Label for="reset-pin">{{ atxConfig.reset.driver === 'usbrelay' ? t('settings.atxChannel') : t('settings.atxPin') }}</Label> <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="0" :disabled="atxConfig.reset.driver === 'none'" /> <Input id="reset-pin" type="number" v-model.number="atxConfig.reset.pin" min="0" :disabled="atxConfig.reset.driver === 'none'" />
</div> </div>
<div v-if="atxConfig.reset.driver === 'gpio'" class="space-y-2"> <div v-if="atxConfig.reset.driver === 'gpio'" class="space-y-2">
@@ -2536,6 +2555,16 @@ watch(() => config.value.hid_backend, async () => {
<option value="low">{{ t('settings.atxLevelLow') }}</option> <option value="low">{{ t('settings.atxLevelLow') }}</option>
</select> </select>
</div> </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">
<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>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>