mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 09:41:45 +08:00
fix: 修复 LCUS HID 继电器协议错误 #263
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
@@ -9,12 +8,10 @@ use tokio::time::sleep;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use super::traits::AtxKeyBackend;
|
||||
use super::types::AtxKeyConfig;
|
||||
use super::types::{AtxKeyConfig, LCUS_RELAY_MAX_CHANNEL};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
const USB_RELAY_MAX_CHANNEL: u8 = 8;
|
||||
const USB_RELAY_REPORT_LEN: usize = 9;
|
||||
const HIDIOCSFEATURE_9: libc::c_ulong = 0xC009_4806;
|
||||
const LCUS_RELAY_COMMAND_LEN: usize = 4;
|
||||
|
||||
pub struct HidrawLinuxRelayBackend {
|
||||
config: AtxKeyConfig,
|
||||
@@ -34,13 +31,13 @@ impl HidrawLinuxRelayBackend {
|
||||
fn validate_config(&self) -> Result<()> {
|
||||
if self.config.pin == 0 {
|
||||
return Err(AppError::Config(
|
||||
"USB relay channel must be 1-based (>= 1)".to_string(),
|
||||
"LCUS HID relay channel must be 1-based (>= 1)".to_string(),
|
||||
));
|
||||
}
|
||||
if self.config.pin > USB_RELAY_MAX_CHANNEL as u32 {
|
||||
if self.config.pin > LCUS_RELAY_MAX_CHANNEL as u32 {
|
||||
return Err(AppError::Config(format!(
|
||||
"USB HID relay channel must be <= {}",
|
||||
USB_RELAY_MAX_CHANNEL
|
||||
"LCUS HID relay channel must be <= {}",
|
||||
LCUS_RELAY_MAX_CHANNEL
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
@@ -49,20 +46,19 @@ impl HidrawLinuxRelayBackend {
|
||||
fn send_command(&self, on: bool) -> Result<()> {
|
||||
let channel = u8::try_from(self.config.pin).map_err(|_| {
|
||||
AppError::Config(format!(
|
||||
"USB relay channel {} exceeds max {}",
|
||||
self.config.pin,
|
||||
u8::MAX
|
||||
"LCUS HID relay channel {} exceeds max {}",
|
||||
self.config.pin, LCUS_RELAY_MAX_CHANNEL
|
||||
))
|
||||
})?;
|
||||
if channel == 0 {
|
||||
return Err(AppError::Config(
|
||||
"USB relay channel must be 1-based (>= 1)".to_string(),
|
||||
"LCUS HID relay channel must be 1-based (>= 1)".to_string(),
|
||||
));
|
||||
}
|
||||
if channel > USB_RELAY_MAX_CHANNEL {
|
||||
if channel > LCUS_RELAY_MAX_CHANNEL {
|
||||
return Err(AppError::Config(format!(
|
||||
"USB HID relay channel must be <= {}",
|
||||
USB_RELAY_MAX_CHANNEL
|
||||
"LCUS HID relay channel must be <= {}",
|
||||
LCUS_RELAY_MAX_CHANNEL
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -70,44 +66,22 @@ impl HidrawLinuxRelayBackend {
|
||||
let mut guard = self.handle.lock().unwrap();
|
||||
let device = guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| AppError::Internal("USB relay not initialized".to_string()))?;
|
||||
.ok_or_else(|| AppError::Internal("LCUS HID relay not initialized".to_string()))?;
|
||||
|
||||
if let Err(feature_err) = Self::send_feature_report(device, &cmd) {
|
||||
debug!(
|
||||
"USB relay feature report failed ({}), falling back to hidraw write",
|
||||
feature_err
|
||||
);
|
||||
device.write_all(&cmd).map_err(|write_err| {
|
||||
AppError::Internal(format!(
|
||||
"USB relay feature report failed: {}; raw write failed: {}",
|
||||
feature_err, write_err
|
||||
))
|
||||
})?;
|
||||
device
|
||||
.flush()
|
||||
.map_err(|e| AppError::Internal(format!("USB relay flush failed: {}", e)))?;
|
||||
}
|
||||
device
|
||||
.write_all(&cmd)
|
||||
.map_err(|e| AppError::Internal(format!("LCUS HID relay write failed: {}", e)))?;
|
||||
device
|
||||
.flush()
|
||||
.map_err(|e| AppError::Internal(format!("LCUS HID relay flush failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_command(channel: u8, on: bool) -> [u8; USB_RELAY_REPORT_LEN] {
|
||||
let mut cmd = [0x00; USB_RELAY_REPORT_LEN];
|
||||
cmd[1] = if on { 0xFF } else { 0xFD };
|
||||
cmd[2] = channel;
|
||||
cmd
|
||||
}
|
||||
|
||||
fn send_feature_report(
|
||||
device: &File,
|
||||
report: &[u8; USB_RELAY_REPORT_LEN],
|
||||
) -> std::io::Result<()> {
|
||||
let rc = unsafe { libc::ioctl(device.as_raw_fd(), HIDIOCSFEATURE_9 as _, report.as_ptr()) };
|
||||
if rc < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
pub fn build_command(channel: u8, on: bool) -> [u8; LCUS_RELAY_COMMAND_LEN] {
|
||||
let state = if on { 1 } else { 0 };
|
||||
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
|
||||
[0xA0, channel, state, checksum]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +91,7 @@ impl AtxKeyBackend for HidrawLinuxRelayBackend {
|
||||
self.validate_config()?;
|
||||
|
||||
info!(
|
||||
"Initializing USB relay ATX backend on {} channel {}",
|
||||
"Initializing LCUS HID relay ATX backend on {} channel {}",
|
||||
self.config.device, self.config.pin
|
||||
);
|
||||
|
||||
@@ -125,14 +99,14 @@ impl AtxKeyBackend for HidrawLinuxRelayBackend {
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&self.config.device)
|
||||
.map_err(|e| AppError::Internal(format!("USB relay device open failed: {}", e)))?;
|
||||
.map_err(|e| AppError::Internal(format!("LCUS HID relay device open failed: {}", e)))?;
|
||||
|
||||
*self.handle.lock().unwrap() = Some(device);
|
||||
self.send_command(false)?;
|
||||
self.initialized.store(true, Ordering::Relaxed);
|
||||
|
||||
debug!(
|
||||
"USB relay channel {} configured successfully",
|
||||
"LCUS HID relay channel {} configured successfully",
|
||||
self.config.pin
|
||||
);
|
||||
Ok(())
|
||||
@@ -140,7 +114,9 @@ impl AtxKeyBackend for HidrawLinuxRelayBackend {
|
||||
|
||||
async fn pulse(&self, duration: Duration) -> Result<()> {
|
||||
if !self.is_initialized() {
|
||||
return Err(AppError::Internal("USB relay not initialized".to_string()));
|
||||
return Err(AppError::Internal(
|
||||
"LCUS HID relay not initialized".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.send_command(true)?;
|
||||
@@ -177,14 +153,14 @@ mod tests {
|
||||
use super::HidrawLinuxRelayBackend;
|
||||
|
||||
#[test]
|
||||
fn usb_relay_command_format() {
|
||||
fn lcus_hid_relay_command_format() {
|
||||
assert_eq!(
|
||||
HidrawLinuxRelayBackend::build_command(1, true),
|
||||
[0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
[0xA0, 0x01, 0x01, 0xA2]
|
||||
);
|
||||
assert_eq!(
|
||||
HidrawLinuxRelayBackend::build_command(1, false),
|
||||
[0x00, 0xFD, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
[0xA0, 0x01, 0x00, 0xA1]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,21 +25,16 @@ pub use controller::{AtxController, AtxControllerConfig};
|
||||
pub use executor::timing;
|
||||
pub use types::{
|
||||
ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxInputBinding, AtxKeyConfig,
|
||||
AtxOutputBinding, AtxPowerRequest, AtxState, HddStatus, PowerStatus,
|
||||
AtxOutputBinding, AtxPowerRequest, AtxState, HddStatus, PowerStatus, LCUS_RELAY_MAX_CHANNEL,
|
||||
};
|
||||
pub use wol::{list_wol_history, record_wol_history, send_wol};
|
||||
|
||||
#[cfg(any(unix, test))]
|
||||
fn hidraw_uevent_is_usb_relay(uevent: &str) -> bool {
|
||||
let upper = uevent.to_ascii_uppercase();
|
||||
upper.contains("000016C0:000005DF")
|
||||
|| upper.contains("00005131:00002007")
|
||||
|| upper.contains("16C0:05DF")
|
||||
upper.contains("00005131:00002007")
|
||||
|| upper.contains("5131:2007")
|
||||
|| upper.contains("PRODUCT=16C0/5DF")
|
||||
|| upper.contains("PRODUCT=5131/2007")
|
||||
|| upper.contains("USBRELAY")
|
||||
|| upper.contains("USB RELAY")
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -94,14 +89,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidraw_uevent_detects_usb_relay_id() {
|
||||
assert!(hidraw_uevent_is_usb_relay(
|
||||
fn test_hidraw_uevent_rejects_non_lcus_usb_relay_id() {
|
||||
assert!(!hidraw_uevent_is_usb_relay(
|
||||
"HID_ID=0003:000016C0:000005DF\nHID_NAME=www.dcttech.com USBRelay2\n"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidraw_uevent_detects_5131_usb_relay_id() {
|
||||
fn test_hidraw_uevent_detects_lcus_hid_relay_id() {
|
||||
assert!(hidraw_uevent_is_usb_relay(
|
||||
"HID_ID=0003:00005131:00002007\n"
|
||||
));
|
||||
|
||||
@@ -7,7 +7,7 @@ use tokio::time::sleep;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use super::traits::{validate_serial_config, AtxKeyBackend, SharedSerialHandle};
|
||||
use super::types::AtxKeyConfig;
|
||||
use super::types::{AtxKeyConfig, LCUS_RELAY_MAX_CHANNEL};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
pub struct SerialRelayBackend {
|
||||
@@ -44,15 +44,23 @@ impl SerialRelayBackend {
|
||||
fn send_command(&self, on: bool) -> Result<()> {
|
||||
let channel = u8::try_from(self.config.pin).map_err(|_| {
|
||||
AppError::Config(format!(
|
||||
"Serial relay channel {} exceeds max {}",
|
||||
self.config.pin,
|
||||
u8::MAX
|
||||
"LCUS serial relay channel {} exceeds max {}",
|
||||
self.config.pin, LCUS_RELAY_MAX_CHANNEL
|
||||
))
|
||||
})?;
|
||||
if channel == 0 {
|
||||
return Err(AppError::Config(
|
||||
"LCUS serial relay channel must be 1-based (>= 1)".to_string(),
|
||||
));
|
||||
}
|
||||
if channel > LCUS_RELAY_MAX_CHANNEL {
|
||||
return Err(AppError::Config(format!(
|
||||
"LCUS serial relay channel must be <= {}",
|
||||
LCUS_RELAY_MAX_CHANNEL
|
||||
)));
|
||||
}
|
||||
|
||||
let state = if on { 1 } else { 0 };
|
||||
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
|
||||
let cmd = [0xA0, channel, state, checksum];
|
||||
let cmd = Self::build_command(channel, on);
|
||||
|
||||
let serial_handle = self
|
||||
.serial_handle
|
||||
@@ -60,16 +68,22 @@ impl SerialRelayBackend {
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.ok_or_else(|| AppError::Internal("Serial relay not initialized".to_string()))?;
|
||||
.ok_or_else(|| AppError::Internal("LCUS serial relay not initialized".to_string()))?;
|
||||
let mut port = serial_handle.lock().unwrap();
|
||||
|
||||
port.write_all(&cmd)
|
||||
.map_err(|e| AppError::Internal(format!("Serial relay write failed: {}", e)))?;
|
||||
.map_err(|e| AppError::Internal(format!("LCUS serial relay write failed: {}", e)))?;
|
||||
port.flush()
|
||||
.map_err(|e| AppError::Internal(format!("Serial relay flush failed: {}", e)))?;
|
||||
.map_err(|e| AppError::Internal(format!("LCUS serial relay flush failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_command(channel: u8, on: bool) -> [u8; 4] {
|
||||
let state = if on { 1 } else { 0 };
|
||||
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
|
||||
[0xA0, channel, state, checksum]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -78,7 +92,7 @@ impl AtxKeyBackend for SerialRelayBackend {
|
||||
validate_serial_config(&self.config)?;
|
||||
|
||||
info!(
|
||||
"Initializing Serial relay ATX backend on {} channel {}",
|
||||
"Initializing LCUS serial relay ATX backend on {} channel {}",
|
||||
self.config.device, self.config.pin
|
||||
);
|
||||
|
||||
@@ -92,7 +106,7 @@ impl AtxKeyBackend for SerialRelayBackend {
|
||||
self.initialized.store(true, Ordering::Relaxed);
|
||||
|
||||
debug!(
|
||||
"Serial relay channel {} configured successfully",
|
||||
"LCUS serial relay channel {} configured successfully",
|
||||
self.config.pin
|
||||
);
|
||||
Ok(())
|
||||
@@ -101,12 +115,12 @@ impl AtxKeyBackend for SerialRelayBackend {
|
||||
async fn pulse(&self, duration: Duration) -> Result<()> {
|
||||
if !self.is_initialized() {
|
||||
return Err(AppError::Internal(
|
||||
"Serial relay not initialized".to_string(),
|
||||
"LCUS serial relay not initialized".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
info!(
|
||||
"Pulse serial relay on {} pin {}",
|
||||
"Pulse LCUS serial relay on {} channel {}",
|
||||
self.config.device, self.config.pin
|
||||
);
|
||||
self.send_command(true)?;
|
||||
@@ -131,6 +145,23 @@ impl AtxKeyBackend for SerialRelayBackend {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SerialRelayBackend;
|
||||
|
||||
#[test]
|
||||
fn lcus_serial_relay_command_format() {
|
||||
assert_eq!(
|
||||
SerialRelayBackend::build_command(1, true),
|
||||
[0xA0, 0x01, 0x01, 0xA2]
|
||||
);
|
||||
assert_eq!(
|
||||
SerialRelayBackend::build_command(1, false),
|
||||
[0xA0, 0x01, 0x00, 0xA1]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SerialRelayBackend {
|
||||
fn drop(&mut self) {
|
||||
if self.is_initialized() {
|
||||
|
||||
@@ -3,7 +3,7 @@ use serialport::SerialPort;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::types::AtxKeyConfig;
|
||||
use super::types::{AtxKeyConfig, LCUS_RELAY_MAX_CHANNEL};
|
||||
use crate::error::Result;
|
||||
|
||||
pub type SharedSerialHandle = Arc<Mutex<Box<dyn SerialPort>>>;
|
||||
@@ -36,10 +36,10 @@ pub fn validate_serial_config(config: &AtxKeyConfig) -> Result<()> {
|
||||
"Serial ATX channel must be 1-based (>= 1)".to_string(),
|
||||
));
|
||||
}
|
||||
if config.pin > u8::MAX as u32 {
|
||||
if config.pin > LCUS_RELAY_MAX_CHANNEL as u32 {
|
||||
return Err(crate::error::AppError::Config(format!(
|
||||
"Serial ATX channel must be <= {}",
|
||||
u8::MAX
|
||||
"LCUS serial relay channel must be <= {}",
|
||||
LCUS_RELAY_MAX_CHANNEL
|
||||
)));
|
||||
}
|
||||
if config.baud_rate == 0 {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
pub const LCUS_RELAY_MAX_CHANNEL: u8 = 8;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PowerStatus {
|
||||
|
||||
@@ -608,19 +608,39 @@ impl AtxConfigUpdate {
|
||||
}
|
||||
}
|
||||
crate::atx::AtxDriverType::UsbRelay => {
|
||||
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)?;
|
||||
Self::validate_device_path(&config.device, "ATX LCUS HID relay")?;
|
||||
Self::validate_output_channel(
|
||||
&config.power,
|
||||
"power",
|
||||
1,
|
||||
crate::atx::LCUS_RELAY_MAX_CHANNEL as u32,
|
||||
)?;
|
||||
Self::validate_output_channel(
|
||||
&config.reset,
|
||||
"reset",
|
||||
1,
|
||||
crate::atx::LCUS_RELAY_MAX_CHANNEL as u32,
|
||||
)?;
|
||||
}
|
||||
crate::atx::AtxDriverType::Serial => {
|
||||
Self::validate_device_path(&config.device, "ATX serial")?;
|
||||
Self::validate_device_path(&config.device, "ATX LCUS serial relay")?;
|
||||
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)?;
|
||||
Self::validate_output_channel(
|
||||
&config.power,
|
||||
"power",
|
||||
1,
|
||||
crate::atx::LCUS_RELAY_MAX_CHANNEL as u32,
|
||||
)?;
|
||||
Self::validate_output_channel(
|
||||
&config.reset,
|
||||
"reset",
|
||||
1,
|
||||
crate::atx::LCUS_RELAY_MAX_CHANNEL as u32,
|
||||
)?;
|
||||
}
|
||||
crate::atx::AtxDriverType::None => {}
|
||||
};
|
||||
@@ -1295,6 +1315,27 @@ mod tests {
|
||||
assert!(update.validate_with_current(¤t).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atx_validate_with_current_rejects_lcus_serial_channel_over_8() {
|
||||
let mut current = AtxConfig::default();
|
||||
current.enabled = true;
|
||||
current.driver = crate::atx::AtxDriverType::Serial;
|
||||
current.device = "/dev/null".to_string();
|
||||
current.power.enabled = true;
|
||||
current.power.pin = 1;
|
||||
current.baud_rate = 9600;
|
||||
|
||||
let mut update = empty_atx_update();
|
||||
update.power = Some(AtxOutputBindingUpdate {
|
||||
enabled: None,
|
||||
device: None,
|
||||
pin: Some(9),
|
||||
active_level: None,
|
||||
});
|
||||
|
||||
assert!(update.validate_with_current(¤t).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atx_validate_with_current_rejects_enabled_none_driver() {
|
||||
let mut current = AtxConfig::default();
|
||||
|
||||
Reference in New Issue
Block a user