mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +08:00
Merge pull request #225 from mofeng-git/pr-223-atx-only
feat(atx): merge serial relay support from #223
This commit is contained in:
@@ -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());
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user