mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 10:34:24 +08:00
feat: 增加 OTG HID 远程唤醒和 OTG MSD 设备名称配置
- 在补丁内核上启用 HID 写入唤醒及 USB 远程唤醒描述符 - 支持全局配置 Flash 和 CD-ROM INQUIRY 字符串 - 兼容普通内核的通用 INQUIRY 属性 - 调整 OTG 功能布局并更新大容量 DVD 镜像提示
This commit is contained in:
@@ -49,18 +49,51 @@ impl Default for VideoConfig {
|
|||||||
pub struct MsdConfig {
|
pub struct MsdConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub msd_dir: String,
|
pub msd_dir: String,
|
||||||
|
pub flash_inquiry_string: String,
|
||||||
|
pub cdrom_inquiry_string: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const DEFAULT_FLASH_INQUIRY_STRING: &str = "One-KVM Virtual Flash";
|
||||||
|
pub const DEFAULT_CDROM_INQUIRY_STRING: &str = "One-KVM Virtual CD-ROM";
|
||||||
|
pub const MAX_INQUIRY_STRING_BYTES: usize = 28;
|
||||||
|
|
||||||
impl Default for MsdConfig {
|
impl Default for MsdConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
msd_dir: String::new(),
|
msd_dir: String::new(),
|
||||||
|
flash_inquiry_string: DEFAULT_FLASH_INQUIRY_STRING.to_string(),
|
||||||
|
cdrom_inquiry_string: DEFAULT_CDROM_INQUIRY_STRING.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MsdConfig {
|
impl MsdConfig {
|
||||||
|
pub fn validate(&self) -> crate::error::Result<()> {
|
||||||
|
Self::validate_inquiry_string("Flash", &self.flash_inquiry_string)?;
|
||||||
|
Self::validate_inquiry_string("CD-ROM", &self.cdrom_inquiry_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_inquiry_string(kind: &str, value: &str) -> crate::error::Result<()> {
|
||||||
|
let value = value.trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
return Err(crate::error::AppError::BadRequest(format!(
|
||||||
|
"MSD {kind} inquiry string cannot be empty"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if value.len() > MAX_INQUIRY_STRING_BYTES {
|
||||||
|
return Err(crate::error::AppError::BadRequest(format!(
|
||||||
|
"MSD {kind} inquiry string must be at most {MAX_INQUIRY_STRING_BYTES} bytes"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !value.bytes().all(|byte| (0x20..=0x7e).contains(&byte)) {
|
||||||
|
return Err(crate::error::AppError::BadRequest(format!(
|
||||||
|
"MSD {kind} inquiry string must contain printable ASCII characters only"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn msd_dir_path(&self) -> std::path::PathBuf {
|
pub fn msd_dir_path(&self) -> std::path::PathBuf {
|
||||||
std::path::PathBuf::from(&self.msd_dir)
|
std::path::PathBuf::from(&self.msd_dir)
|
||||||
}
|
}
|
||||||
@@ -123,3 +156,18 @@ impl Default for WebConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn msd_inquiry_strings_default_and_validate() {
|
||||||
|
assert!(MsdConfig::default().validate().is_ok());
|
||||||
|
assert!(MsdConfig::validate_inquiry_string("Flash", " Custom Drive ").is_ok());
|
||||||
|
assert!(MsdConfig::validate_inquiry_string("Flash", "").is_err());
|
||||||
|
assert!(MsdConfig::validate_inquiry_string("Flash", &"x".repeat(29)).is_err());
|
||||||
|
assert!(MsdConfig::validate_inquiry_string("CD-ROM", "虚拟光驱").is_err());
|
||||||
|
assert!(MsdConfig::validate_inquiry_string("CD-ROM", "bad\tname").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,6 +106,16 @@ pub fn write_file(path: &Path, content: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write an optional configfs/sysfs attribute when the running kernel exposes it.
|
||||||
|
/// This keeps newer kernel enhancements compatible with older kernels.
|
||||||
|
pub fn write_file_if_exists(path: &Path, content: &str) -> Result<bool> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
write_file(path, content)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write_bytes(path: &Path, data: &[u8]) -> Result<()> {
|
pub fn write_bytes(path: &Path, data: &[u8]) -> Result<()> {
|
||||||
let mut file = File::create(path)
|
let mut file = File::create(path)
|
||||||
.map_err(|e| AppError::Internal(format!("Failed to create {}: {}", path.display(), e)))?;
|
.map_err(|e| AppError::Internal(format!("Failed to create {}: {}", path.display(), e)))?;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use tracing::debug;
|
|||||||
|
|
||||||
use super::configfs::{
|
use super::configfs::{
|
||||||
create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file,
|
create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file,
|
||||||
|
write_file_if_exists,
|
||||||
};
|
};
|
||||||
use super::function::GadgetFunction;
|
use super::function::GadgetFunction;
|
||||||
use super::report_desc::{
|
use super::report_desc::{
|
||||||
@@ -143,6 +144,10 @@ impl GadgetFunction for HidFunction {
|
|||||||
self.func_type.report_desc(self.keyboard_leds),
|
self.func_type.report_desc(self.keyboard_leds),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Supported by the PiKVM HID kernel patch. Older kernels simply do
|
||||||
|
// not expose this ConfigFS attribute.
|
||||||
|
let _ = write_file_if_exists(&func_path.join("wakeup_on_write"), "1")?;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Created HID function: {} at {}",
|
"Created HID function: {} at {}",
|
||||||
self.name(),
|
self.name(),
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ use tracing::{debug, error, info, warn};
|
|||||||
|
|
||||||
use super::configfs::{
|
use super::configfs::{
|
||||||
configfs_path, create_dir, create_symlink, find_udc, is_configfs_available, remove_dir,
|
configfs_path, create_dir, create_symlink, find_udc, is_configfs_available, remove_dir,
|
||||||
remove_file, write_file, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, DEFAULT_USB_PRODUCT_ID,
|
remove_file, write_file, write_file_if_exists, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE,
|
||||||
DEFAULT_USB_VENDOR_ID, USB_BCD_USB,
|
DEFAULT_USB_PRODUCT_ID, DEFAULT_USB_VENDOR_ID, USB_BCD_USB,
|
||||||
};
|
};
|
||||||
use super::function::GadgetFunction;
|
use super::function::GadgetFunction;
|
||||||
use super::hid::HidFunction;
|
use super::hid::HidFunction;
|
||||||
use super::msd::MsdFunction;
|
use super::msd::{MsdFunction, MsdInquiryStrings};
|
||||||
use super::network::NetworkFunction;
|
use super::network::NetworkFunction;
|
||||||
use crate::config::OtgNetworkConfig;
|
use crate::config::OtgNetworkConfig;
|
||||||
use crate::error::{AppError, Result};
|
use crate::error::{AppError, Result};
|
||||||
@@ -134,8 +134,12 @@ impl OtgGadgetManager {
|
|||||||
Ok(device_path)
|
Ok(device_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_msd(&mut self, lun_capacity: u8) -> Result<MsdFunction> {
|
pub fn add_msd(
|
||||||
let func = MsdFunction::new(self.msd_instance, lun_capacity)?;
|
&mut self,
|
||||||
|
lun_capacity: u8,
|
||||||
|
inquiry_strings: MsdInquiryStrings,
|
||||||
|
) -> Result<MsdFunction> {
|
||||||
|
let func = MsdFunction::new(self.msd_instance, lun_capacity, inquiry_strings)?;
|
||||||
let func_clone = func.clone();
|
let func_clone = func.clone();
|
||||||
self.add_function(Box::new(func))?;
|
self.add_function(Box::new(func))?;
|
||||||
self.msd_instance += 1;
|
self.msd_instance += 1;
|
||||||
@@ -196,6 +200,22 @@ impl OtgGadgetManager {
|
|||||||
func.link(&self.config_path, &self.gadget_path)?;
|
func.link(&self.config_path, &self.gadget_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A host only enables USB remote wakeup when the configuration
|
||||||
|
// descriptor advertises it. Enable the descriptor bit only when the
|
||||||
|
// running kernel supports the HID wakeup_on_write attribute.
|
||||||
|
let hid_wakeup_supported = self.functions.iter().any(|func| {
|
||||||
|
func.name().starts_with("hid.")
|
||||||
|
&& self
|
||||||
|
.gadget_path
|
||||||
|
.join("functions")
|
||||||
|
.join(func.name())
|
||||||
|
.join("wakeup_on_write")
|
||||||
|
.exists()
|
||||||
|
});
|
||||||
|
if hid_wakeup_supported {
|
||||||
|
let _ = write_file_if_exists(&self.config_path.join("bmAttributes"), "0xA0")?;
|
||||||
|
}
|
||||||
|
|
||||||
debug!("OTG USB Gadget setup complete");
|
debug!("OTG USB Gadget setup complete");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
145
src/otg/msd.rs
145
src/otg/msd.rs
@@ -5,6 +5,7 @@ use tracing::{debug, info, warn};
|
|||||||
|
|
||||||
use super::configfs::{create_dir, create_symlink, remove_dir, remove_file, write_file};
|
use super::configfs::{create_dir, create_symlink, remove_dir, remove_file, write_file};
|
||||||
use super::function::GadgetFunction;
|
use super::function::GadgetFunction;
|
||||||
|
use crate::config::{MsdConfig, DEFAULT_CDROM_INQUIRY_STRING, DEFAULT_FLASH_INQUIRY_STRING};
|
||||||
use crate::error::{AppError, MsdErrorCode, Result};
|
use crate::error::{AppError, MsdErrorCode, Result};
|
||||||
|
|
||||||
const MEDIA_TYPE_REBIND_DELAY_MS: u64 = 300;
|
const MEDIA_TYPE_REBIND_DELAY_MS: u64 = 300;
|
||||||
@@ -56,14 +57,39 @@ impl MsdLunConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct MsdInquiryStrings {
|
||||||
|
pub flash: String,
|
||||||
|
pub cdrom: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MsdInquiryStrings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
flash: DEFAULT_FLASH_INQUIRY_STRING.to_string(),
|
||||||
|
cdrom: DEFAULT_CDROM_INQUIRY_STRING.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&MsdConfig> for MsdInquiryStrings {
|
||||||
|
fn from(config: &MsdConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
flash: config.flash_inquiry_string.clone(),
|
||||||
|
cdrom: config.cdrom_inquiry_string.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MsdFunction {
|
pub struct MsdFunction {
|
||||||
name: String,
|
name: String,
|
||||||
lun_capacity: u8,
|
lun_capacity: u8,
|
||||||
|
inquiry_strings: MsdInquiryStrings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MsdFunction {
|
impl MsdFunction {
|
||||||
pub fn new(instance: u8, lun_capacity: u8) -> Result<Self> {
|
pub fn new(instance: u8, lun_capacity: u8, inquiry_strings: MsdInquiryStrings) -> Result<Self> {
|
||||||
if lun_capacity != 1 && lun_capacity != 8 {
|
if lun_capacity != 1 && lun_capacity != 8 {
|
||||||
return Err(AppError::BadRequest(format!(
|
return Err(AppError::BadRequest(format!(
|
||||||
"MSD LUN capacity must be 1 or 8, got {lun_capacity}"
|
"MSD LUN capacity must be 1 or 8, got {lun_capacity}"
|
||||||
@@ -73,6 +99,7 @@ impl MsdFunction {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
name: format!("mass_storage.usb{}", instance),
|
name: format!("mass_storage.usb{}", instance),
|
||||||
lun_capacity,
|
lun_capacity,
|
||||||
|
inquiry_strings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +239,23 @@ impl MsdFunction {
|
|||||||
current_cdrom != if config.cdrom { "1" } else { "0" }
|
current_cdrom != if config.cdrom { "1" } else { "0" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inquiry_string_path(lun_path: &Path, cdrom: bool) -> Option<PathBuf> {
|
||||||
|
let cdrom_path = lun_path.join("inquiry_string_cdrom");
|
||||||
|
if cdrom && cdrom_path.exists() {
|
||||||
|
return Some(cdrom_path);
|
||||||
|
}
|
||||||
|
let generic_path = lun_path.join("inquiry_string");
|
||||||
|
generic_path.exists().then_some(generic_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inquiry_string(&self, cdrom: bool) -> &str {
|
||||||
|
if cdrom {
|
||||||
|
&self.inquiry_strings.cdrom
|
||||||
|
} else {
|
||||||
|
&self.inquiry_strings.flash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn configure_lun_attributes(
|
fn configure_lun_attributes(
|
||||||
&self,
|
&self,
|
||||||
lun_path: &Path,
|
lun_path: &Path,
|
||||||
@@ -256,6 +300,7 @@ impl MsdFunction {
|
|||||||
lun, current_cdrom, new_cdrom
|
lun, current_cdrom, new_cdrom
|
||||||
);
|
);
|
||||||
write_file(&lun_path.join("cdrom"), new_cdrom)?;
|
write_file(&lun_path.join("cdrom"), new_cdrom)?;
|
||||||
|
self.write_inquiry_string(lun_path, config.cdrom)?;
|
||||||
}
|
}
|
||||||
if current_ro != new_ro {
|
if current_ro != new_ro {
|
||||||
debug!("Updating LUN {} ro: {} -> {}", lun, current_ro, new_ro);
|
debug!("Updating LUN {} ro: {} -> {}", lun, current_ro, new_ro);
|
||||||
@@ -320,6 +365,26 @@ impl MsdFunction {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_inquiry_string(&self, lun_path: &Path, cdrom: bool) -> Result<()> {
|
||||||
|
if let Some(path) = Self::inquiry_string_path(lun_path, cdrom) {
|
||||||
|
write_file(&path, self.inquiry_string(cdrom))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_inquiry_strings(&self, lun_path: &Path) -> Result<()> {
|
||||||
|
let generic_path = lun_path.join("inquiry_string");
|
||||||
|
if generic_path.exists() {
|
||||||
|
write_file(&generic_path, &self.inquiry_strings.flash)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cdrom_path = lun_path.join("inquiry_string_cdrom");
|
||||||
|
if cdrom_path.exists() {
|
||||||
|
write_file(&cdrom_path, &self.inquiry_strings.cdrom)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn disconnect_lun_async(&self, gadget_path: &Path, lun: u8) -> Result<()> {
|
pub async fn disconnect_lun_async(&self, gadget_path: &Path, lun: u8) -> Result<()> {
|
||||||
let gadget_path = gadget_path.to_path_buf();
|
let gadget_path = gadget_path.to_path_buf();
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
@@ -455,6 +520,7 @@ impl GadgetFunction for MsdFunction {
|
|||||||
|
|
||||||
for lun in 0..self.lun_capacity {
|
for lun in 0..self.lun_capacity {
|
||||||
self.clear_lun_unbound(gadget_path, lun)?;
|
self.clear_lun_unbound(gadget_path, lun)?;
|
||||||
|
self.write_inquiry_strings(&self.lun_path(gadget_path, lun))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Created MSD function: {}", self.name());
|
debug!("Created MSD function: {}", self.name());
|
||||||
@@ -526,6 +592,10 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn test_msd(capacity: u8) -> MsdFunction {
|
||||||
|
MsdFunction::new(0, capacity, MsdInquiryStrings::default()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lun_config_cdrom() {
|
fn test_lun_config_cdrom() {
|
||||||
let config = MsdLunConfig::cdrom(PathBuf::from("/tmp/test.iso"));
|
let config = MsdLunConfig::cdrom(PathBuf::from("/tmp/test.iso"));
|
||||||
@@ -542,21 +612,62 @@ mod tests {
|
|||||||
assert!(config.removable);
|
assert!(config.removable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inquiry_string_uses_cdrom_attribute_with_stock_fallback() {
|
||||||
|
let msd = MsdFunction::new(
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
MsdInquiryStrings {
|
||||||
|
flash: "Custom Flash".into(),
|
||||||
|
cdrom: "Custom Optical".into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let patched = TempDir::new().unwrap();
|
||||||
|
std::fs::write(patched.path().join("inquiry_string"), b"generic\n").unwrap();
|
||||||
|
std::fs::write(patched.path().join("inquiry_string_cdrom"), b"cdrom\n").unwrap();
|
||||||
|
|
||||||
|
msd.write_inquiry_strings(patched.path()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(patched.path().join("inquiry_string_cdrom"))
|
||||||
|
.unwrap()
|
||||||
|
.trim(),
|
||||||
|
"Custom Optical"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(patched.path().join("inquiry_string"))
|
||||||
|
.unwrap()
|
||||||
|
.trim(),
|
||||||
|
"Custom Flash"
|
||||||
|
);
|
||||||
|
|
||||||
|
let stock = TempDir::new().unwrap();
|
||||||
|
std::fs::write(stock.path().join("inquiry_string"), b"generic\n").unwrap();
|
||||||
|
msd.write_inquiry_string(stock.path(), true).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(stock.path().join("inquiry_string"))
|
||||||
|
.unwrap()
|
||||||
|
.trim(),
|
||||||
|
"Custom Optical"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_msd_function_name() {
|
fn test_msd_function_name() {
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
assert_eq!(msd.name(), "mass_storage.usb0");
|
assert_eq!(msd.name(), "mass_storage.usb0");
|
||||||
assert_eq!(msd.lun_capacity, 1);
|
assert_eq!(msd.lun_capacity, 1);
|
||||||
|
|
||||||
let multi = MsdFunction::new(0, 8).unwrap();
|
let multi = test_msd(8);
|
||||||
assert_eq!(multi.lun_capacity, 8);
|
assert_eq!(multi.lun_capacity, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_msd_function_rejects_invalid_capacity() {
|
fn test_msd_function_rejects_invalid_capacity() {
|
||||||
assert!(MsdFunction::new(0, 0).is_err());
|
assert!(MsdFunction::new(0, 0, MsdInquiryStrings::default()).is_err());
|
||||||
assert!(MsdFunction::new(0, 2).is_err());
|
assert!(MsdFunction::new(0, 2, MsdInquiryStrings::default()).is_err());
|
||||||
assert!(MsdFunction::new(0, 9).is_err());
|
assert!(MsdFunction::new(0, 9, MsdInquiryStrings::default()).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -575,7 +686,7 @@ mod tests {
|
|||||||
std::fs::create_dir_all(&lun_path).unwrap();
|
std::fs::create_dir_all(&lun_path).unwrap();
|
||||||
std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap();
|
std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap();
|
||||||
std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap();
|
std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap();
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
msd.disconnect_lun(temp_dir.path(), 0).unwrap();
|
msd.disconnect_lun(temp_dir.path(), 0).unwrap();
|
||||||
|
|
||||||
@@ -595,7 +706,7 @@ mod tests {
|
|||||||
let lun_path = temp_dir.path().join("functions/mass_storage.usb0/lun.0");
|
let lun_path = temp_dir.path().join("functions/mass_storage.usb0/lun.0");
|
||||||
std::fs::create_dir_all(&lun_path).unwrap();
|
std::fs::create_dir_all(&lun_path).unwrap();
|
||||||
std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap();
|
std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap();
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
msd.disconnect_lun(temp_dir.path(), 0).unwrap();
|
msd.disconnect_lun(temp_dir.path(), 0).unwrap();
|
||||||
|
|
||||||
@@ -610,7 +721,7 @@ mod tests {
|
|||||||
let lun_path = temp_dir.path().join("functions/mass_storage.usb0/lun.0");
|
let lun_path = temp_dir.path().join("functions/mass_storage.usb0/lun.0");
|
||||||
std::fs::create_dir_all(lun_path.join("forced_eject")).unwrap();
|
std::fs::create_dir_all(lun_path.join("forced_eject")).unwrap();
|
||||||
std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap();
|
std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap();
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
msd.disconnect_lun(temp_dir.path(), 0).unwrap();
|
msd.disconnect_lun(temp_dir.path(), 0).unwrap();
|
||||||
|
|
||||||
@@ -629,7 +740,7 @@ mod tests {
|
|||||||
std::fs::write(lun_path.join("file"), format!("backing-{lun}.img\n")).unwrap();
|
std::fs::write(lun_path.join("file"), format!("backing-{lun}.img\n")).unwrap();
|
||||||
std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap();
|
std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap();
|
||||||
}
|
}
|
||||||
let msd = MsdFunction::new(0, 8).unwrap();
|
let msd = test_msd(8);
|
||||||
|
|
||||||
msd.disconnect_lun(temp_dir.path(), 1).unwrap();
|
msd.disconnect_lun(temp_dir.path(), 1).unwrap();
|
||||||
|
|
||||||
@@ -656,7 +767,7 @@ mod tests {
|
|||||||
for capacity in [1, 8] {
|
for capacity in [1, 8] {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
std::fs::create_dir_all(temp_dir.path().join("functions")).unwrap();
|
std::fs::create_dir_all(temp_dir.path().join("functions")).unwrap();
|
||||||
let msd = MsdFunction::new(0, capacity).unwrap();
|
let msd = test_msd(capacity);
|
||||||
|
|
||||||
msd.create(temp_dir.path()).unwrap();
|
msd.create(temp_dir.path()).unwrap();
|
||||||
|
|
||||||
@@ -678,7 +789,7 @@ mod tests {
|
|||||||
std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
|
std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
|
||||||
let image_path = temp_dir.path().join("test.img");
|
let image_path = temp_dir.path().join("test.img");
|
||||||
std::fs::write(&image_path, b"image").unwrap();
|
std::fs::write(&image_path, b"image").unwrap();
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::disk(image_path, false))
|
msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::disk(image_path, false))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -711,7 +822,7 @@ mod tests {
|
|||||||
std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
|
std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
|
||||||
let image_path = temp_dir.path().join("test.iso");
|
let image_path = temp_dir.path().join("test.iso");
|
||||||
std::fs::write(&image_path, b"iso").unwrap();
|
std::fs::write(&image_path, b"iso").unwrap();
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path.clone()))
|
msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -751,7 +862,7 @@ mod tests {
|
|||||||
std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
|
std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
|
||||||
let image_path = temp_dir.path().join("test.iso");
|
let image_path = temp_dir.path().join("test.iso");
|
||||||
std::fs::write(&image_path, b"iso").unwrap();
|
std::fs::write(&image_path, b"iso").unwrap();
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
assert!(msd
|
assert!(msd
|
||||||
.configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path),)
|
.configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path),)
|
||||||
@@ -771,7 +882,7 @@ mod tests {
|
|||||||
for lun in 1..8 {
|
for lun in 1..8 {
|
||||||
std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap();
|
std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap();
|
||||||
}
|
}
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
msd.cleanup(temp_dir.path()).unwrap();
|
msd.cleanup(temp_dir.path()).unwrap();
|
||||||
|
|
||||||
@@ -788,7 +899,7 @@ mod tests {
|
|||||||
std::fs::write(lun_path.join("file"), format!("backing-{lun}.img\n")).unwrap();
|
std::fs::write(lun_path.join("file"), format!("backing-{lun}.img\n")).unwrap();
|
||||||
std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap();
|
std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap();
|
||||||
}
|
}
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
// Ordinary files do not disappear with configfs groups, so cleanup is
|
// Ordinary files do not disappear with configfs groups, so cleanup is
|
||||||
// expected to report directory-removal failures in this test fixture.
|
// expected to report directory-removal failures in this test fixture.
|
||||||
@@ -809,7 +920,7 @@ mod tests {
|
|||||||
for lun in 0..2 {
|
for lun in 0..2 {
|
||||||
std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap();
|
std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap();
|
||||||
}
|
}
|
||||||
let msd = MsdFunction::new(0, 1).unwrap();
|
let msd = test_msd(1);
|
||||||
|
|
||||||
let error = msd.cleanup(temp_dir.path()).unwrap_err();
|
let error = msd.cleanup(temp_dir.path()).unwrap_err();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use typeshare::typeshare;
|
|||||||
|
|
||||||
use super::bridge::NetworkBridgeRuntime;
|
use super::bridge::NetworkBridgeRuntime;
|
||||||
use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager};
|
use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager};
|
||||||
use super::msd::{MsdFunction, MsdLunConfig};
|
use super::msd::{MsdFunction, MsdInquiryStrings, MsdLunConfig};
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
HidBackend, HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions, OtgNetworkConfig,
|
HidBackend, HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions, OtgNetworkConfig,
|
||||||
UacConfig,
|
UacConfig,
|
||||||
@@ -62,6 +62,7 @@ pub(crate) struct OtgDesiredState {
|
|||||||
pub keyboard_leds: bool,
|
pub keyboard_leds: bool,
|
||||||
pub msd_enabled: bool,
|
pub msd_enabled: bool,
|
||||||
pub msd_lun_capacity: u8,
|
pub msd_lun_capacity: u8,
|
||||||
|
pub msd_inquiry_strings: MsdInquiryStrings,
|
||||||
pub network: OtgNetworkConfig,
|
pub network: OtgNetworkConfig,
|
||||||
pub uac: UacConfig,
|
pub uac: UacConfig,
|
||||||
}
|
}
|
||||||
@@ -75,6 +76,7 @@ impl Default for OtgDesiredState {
|
|||||||
keyboard_leds: false,
|
keyboard_leds: false,
|
||||||
msd_enabled: false,
|
msd_enabled: false,
|
||||||
msd_lun_capacity: 1,
|
msd_lun_capacity: 1,
|
||||||
|
msd_inquiry_strings: MsdInquiryStrings::default(),
|
||||||
network: OtgNetworkConfig::default(),
|
network: OtgNetworkConfig::default(),
|
||||||
uac: UacConfig::default(),
|
uac: UacConfig::default(),
|
||||||
}
|
}
|
||||||
@@ -90,6 +92,7 @@ impl OtgDesiredState {
|
|||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
network.validate()?;
|
network.validate()?;
|
||||||
uac.validate()?;
|
uac.validate()?;
|
||||||
|
msd.validate()?;
|
||||||
let hid_functions = if hid.backend == HidBackend::Otg {
|
let hid_functions = if hid.backend == HidBackend::Otg {
|
||||||
let functions = hid.constrained_otg_functions();
|
let functions = hid.constrained_otg_functions();
|
||||||
Some(functions)
|
Some(functions)
|
||||||
@@ -115,6 +118,7 @@ impl OtgDesiredState {
|
|||||||
keyboard_leds: hid.effective_otg_keyboard_leds(),
|
keyboard_leds: hid.effective_otg_keyboard_leds(),
|
||||||
msd_enabled: msd.enabled,
|
msd_enabled: msd.enabled,
|
||||||
msd_lun_capacity: 1,
|
msd_lun_capacity: 1,
|
||||||
|
msd_inquiry_strings: MsdInquiryStrings::from(msd),
|
||||||
network: network.clone(),
|
network: network.clone(),
|
||||||
uac: if uac.enabled {
|
uac: if uac.enabled {
|
||||||
uac.clone()
|
uac.clone()
|
||||||
@@ -142,6 +146,7 @@ struct OtgServiceState {
|
|||||||
pub hid_enabled: bool,
|
pub hid_enabled: bool,
|
||||||
pub msd_enabled: bool,
|
pub msd_enabled: bool,
|
||||||
pub msd_lun_capacity: u8,
|
pub msd_lun_capacity: u8,
|
||||||
|
pub msd_inquiry_strings: MsdInquiryStrings,
|
||||||
pub network: OtgNetworkConfig,
|
pub network: OtgNetworkConfig,
|
||||||
pub uac: UacConfig,
|
pub uac: UacConfig,
|
||||||
pub configured_udc: Option<String>,
|
pub configured_udc: Option<String>,
|
||||||
@@ -160,6 +165,7 @@ impl Default for OtgServiceState {
|
|||||||
hid_enabled: false,
|
hid_enabled: false,
|
||||||
msd_enabled: false,
|
msd_enabled: false,
|
||||||
msd_lun_capacity: 1,
|
msd_lun_capacity: 1,
|
||||||
|
msd_inquiry_strings: MsdInquiryStrings::default(),
|
||||||
network: OtgNetworkConfig::default(),
|
network: OtgNetworkConfig::default(),
|
||||||
uac: UacConfig::default(),
|
uac: UacConfig::default(),
|
||||||
configured_udc: None,
|
configured_udc: None,
|
||||||
@@ -355,6 +361,7 @@ impl OtgService {
|
|||||||
&& state.hid_enabled == desired.hid_enabled()
|
&& state.hid_enabled == desired.hid_enabled()
|
||||||
&& state.msd_enabled == desired.msd_enabled
|
&& state.msd_enabled == desired.msd_enabled
|
||||||
&& state.msd_lun_capacity == desired.msd_lun_capacity
|
&& state.msd_lun_capacity == desired.msd_lun_capacity
|
||||||
|
&& state.msd_inquiry_strings == desired.msd_inquiry_strings
|
||||||
&& state.network == desired.network
|
&& state.network == desired.network
|
||||||
&& state.uac == desired.uac
|
&& state.uac == desired.uac
|
||||||
&& state.configured_udc == desired.udc
|
&& state.configured_udc == desired.udc
|
||||||
@@ -395,6 +402,7 @@ impl OtgService {
|
|||||||
state.hid_enabled = false;
|
state.hid_enabled = false;
|
||||||
state.msd_enabled = false;
|
state.msd_enabled = false;
|
||||||
state.msd_lun_capacity = 1;
|
state.msd_lun_capacity = 1;
|
||||||
|
state.msd_inquiry_strings = MsdInquiryStrings::default();
|
||||||
state.network = OtgNetworkConfig::default();
|
state.network = OtgNetworkConfig::default();
|
||||||
state.uac = UacConfig::default();
|
state.uac = UacConfig::default();
|
||||||
state.configured_udc = None;
|
state.configured_udc = None;
|
||||||
@@ -505,7 +513,10 @@ impl OtgService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let msd_func = if desired.msd_enabled {
|
let msd_func = if desired.msd_enabled {
|
||||||
match manager.add_msd(desired.msd_lun_capacity) {
|
match manager.add_msd(
|
||||||
|
desired.msd_lun_capacity,
|
||||||
|
desired.msd_inquiry_strings.clone(),
|
||||||
|
) {
|
||||||
Ok(func) => {
|
Ok(func) => {
|
||||||
debug!("MSD function added to gadget");
|
debug!("MSD function added to gadget");
|
||||||
Some(func)
|
Some(func)
|
||||||
@@ -592,6 +603,7 @@ impl OtgService {
|
|||||||
state.hid_enabled = desired.hid_enabled();
|
state.hid_enabled = desired.hid_enabled();
|
||||||
state.msd_enabled = desired.msd_enabled;
|
state.msd_enabled = desired.msd_enabled;
|
||||||
state.msd_lun_capacity = desired.msd_lun_capacity;
|
state.msd_lun_capacity = desired.msd_lun_capacity;
|
||||||
|
state.msd_inquiry_strings = desired.msd_inquiry_strings.clone();
|
||||||
state.network = desired.network.clone();
|
state.network = desired.network.clone();
|
||||||
state.uac = desired.uac.clone();
|
state.uac = desired.uac.clone();
|
||||||
state.configured_udc = Some(udc);
|
state.configured_udc = Some(udc);
|
||||||
@@ -715,6 +727,14 @@ mod tests {
|
|||||||
assert_ne!(single, multi);
|
assert_ne!(single, multi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inquiry_strings_participate_in_desired_state_equality() {
|
||||||
|
let original = OtgDesiredState::default();
|
||||||
|
let mut changed = original.clone();
|
||||||
|
changed.msd_inquiry_strings.flash = "Custom Flash".to_string();
|
||||||
|
assert_ne!(original, changed);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn onecloud_full_composite_is_not_rejected_before_configfs() {
|
fn onecloud_full_composite_is_not_rejected_before_configfs() {
|
||||||
let hid = HidConfig {
|
let hid = HidConfig {
|
||||||
|
|||||||
@@ -277,6 +277,9 @@ pub async fn apply_msd_config(
|
|||||||
let old_msd_enabled = old_config.enabled;
|
let old_msd_enabled = old_config.enabled;
|
||||||
let new_msd_enabled = effective_new_msd_enabled;
|
let new_msd_enabled = effective_new_msd_enabled;
|
||||||
let msd_dir_changed = old_config.msd_dir != new_config.msd_dir;
|
let msd_dir_changed = old_config.msd_dir != new_config.msd_dir;
|
||||||
|
let inquiry_strings_changed = old_config.flash_inquiry_string
|
||||||
|
!= new_config.flash_inquiry_string
|
||||||
|
|| old_config.cdrom_inquiry_string != new_config.cdrom_inquiry_string;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"MSD enabled: old={}, new={}",
|
"MSD enabled: old={}, new={}",
|
||||||
@@ -286,6 +289,9 @@ pub async fn apply_msd_config(
|
|||||||
if msd_dir_changed {
|
if msd_dir_changed {
|
||||||
tracing::info!("MSD directory changed: {}", new_config.msd_dir);
|
tracing::info!("MSD directory changed: {}", new_config.msd_dir);
|
||||||
}
|
}
|
||||||
|
if inquiry_strings_changed {
|
||||||
|
tracing::info!("MSD inquiry strings changed");
|
||||||
|
}
|
||||||
|
|
||||||
let msd_dir = new_config.msd_dir_path();
|
let msd_dir = new_config.msd_dir_path();
|
||||||
if let Err(e) = std::fs::create_dir_all(msd_dir.join("images")) {
|
if let Err(e) = std::fs::create_dir_all(msd_dir.join("images")) {
|
||||||
@@ -295,12 +301,12 @@ pub async fn apply_msd_config(
|
|||||||
tracing::warn!("Failed to create MSD ventoy directory: {}", e);
|
tracing::warn!("Failed to create MSD ventoy directory: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let needs_reload = options.force || old_msd_enabled != new_msd_enabled || msd_dir_changed;
|
let needs_reload = options.force
|
||||||
|
|| old_msd_enabled != new_msd_enabled
|
||||||
|
|| msd_dir_changed
|
||||||
|
|| inquiry_strings_changed;
|
||||||
if !needs_reload {
|
if !needs_reload {
|
||||||
tracing::info!(
|
tracing::info!("MSD configuration unchanged, no reload needed");
|
||||||
"MSD enabled state unchanged ({}) and directory unchanged, no reload needed",
|
|
||||||
new_msd_enabled
|
|
||||||
);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -476,6 +476,8 @@ impl OtgNetworkConfigUpdate {
|
|||||||
pub struct MsdConfigUpdate {
|
pub struct MsdConfigUpdate {
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
pub msd_dir: Option<String>,
|
pub msd_dir: Option<String>,
|
||||||
|
pub flash_inquiry_string: Option<String>,
|
||||||
|
pub cdrom_inquiry_string: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -492,6 +494,12 @@ impl MsdConfigUpdate {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(ref value) = self.flash_inquiry_string {
|
||||||
|
MsdConfig::validate_inquiry_string("Flash", value)?;
|
||||||
|
}
|
||||||
|
if let Some(ref value) = self.cdrom_inquiry_string {
|
||||||
|
MsdConfig::validate_inquiry_string("CD-ROM", value)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,6 +510,12 @@ impl MsdConfigUpdate {
|
|||||||
if let Some(ref dir) = self.msd_dir {
|
if let Some(ref dir) = self.msd_dir {
|
||||||
config.msd_dir = dir.trim().to_string();
|
config.msd_dir = dir.trim().to_string();
|
||||||
}
|
}
|
||||||
|
if let Some(ref value) = self.flash_inquiry_string {
|
||||||
|
config.flash_inquiry_string = value.trim().to_string();
|
||||||
|
}
|
||||||
|
if let Some(ref value) = self.cdrom_inquiry_string {
|
||||||
|
config.cdrom_inquiry_string = value.trim().to_string();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ export default {
|
|||||||
download: 'Download',
|
download: 'Download',
|
||||||
downloadComplete: 'Download complete',
|
downloadComplete: 'Download complete',
|
||||||
largeFileWarning: '>2.2GB',
|
largeFileWarning: '>2.2GB',
|
||||||
largeFileTooltip: 'File is larger than 2.2GB, please use Flash mode to mount',
|
largeFileTooltip: 'If DVD image support is not enabled in the kernel, only the first 2.2 GB is accessible after mounting.',
|
||||||
operationInProgress: 'Operation in progress, please wait',
|
operationInProgress: 'Operation in progress, please wait',
|
||||||
selectDriveSize: 'Select virtual drive size',
|
selectDriveSize: 'Select virtual drive size',
|
||||||
driveSpaceUnknown: 'Unable to read available space for the MSD directory filesystem',
|
driveSpaceUnknown: 'Unable to read available space for the MSD directory filesystem',
|
||||||
@@ -644,6 +644,8 @@ export default {
|
|||||||
username: 'Username',
|
username: 'Username',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
msdDir: 'MSD directory',
|
msdDir: 'MSD directory',
|
||||||
|
msdFlashInquiryString: 'Flash device name',
|
||||||
|
msdCdromInquiryString: 'CD-ROM device name',
|
||||||
atxPowerButton: 'Power Button',
|
atxPowerButton: 'Power Button',
|
||||||
atxResetButton: 'Reset Button',
|
atxResetButton: 'Reset Button',
|
||||||
atxPowerManagement: 'Power Management',
|
atxPowerManagement: 'Power Management',
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ export default {
|
|||||||
download: '下载',
|
download: '下载',
|
||||||
downloadComplete: '下载完成',
|
downloadComplete: '下载完成',
|
||||||
largeFileWarning: '>2.2GB',
|
largeFileWarning: '>2.2GB',
|
||||||
largeFileTooltip: '文件大于 2.2GB,请使用 Flash 模式挂载',
|
largeFileTooltip: '若内核未启用 DVD 镜像支持,挂载后仅前 2.2 GB 可访问。',
|
||||||
operationInProgress: '操作进行中,请稍候',
|
operationInProgress: '操作进行中,请稍候',
|
||||||
selectDriveSize: '选择虚拟驱动器大小',
|
selectDriveSize: '选择虚拟驱动器大小',
|
||||||
driveSpaceUnknown: '无法读取 MSD 目录所在储存空间的剩余可用空间',
|
driveSpaceUnknown: '无法读取 MSD 目录所在储存空间的剩余可用空间',
|
||||||
@@ -643,6 +643,8 @@ export default {
|
|||||||
username: '用户名',
|
username: '用户名',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
msdDir: 'MSD 目录',
|
msdDir: 'MSD 目录',
|
||||||
|
msdFlashInquiryString: 'Flash 设备名称',
|
||||||
|
msdCdromInquiryString: 'CD-ROM 设备名称',
|
||||||
atxPowerButton: '电源按钮',
|
atxPowerButton: '电源按钮',
|
||||||
atxResetButton: '重启按钮',
|
atxResetButton: '重启按钮',
|
||||||
atxPowerManagement: '电源管理',
|
atxPowerManagement: '电源管理',
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export interface OtgNetworkConfig {
|
|||||||
export interface MsdConfig {
|
export interface MsdConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
msd_dir: string;
|
msd_dir: string;
|
||||||
|
flash_inquiry_string: string;
|
||||||
|
cdrom_inquiry_string: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AtxDriverType {
|
export enum AtxDriverType {
|
||||||
@@ -546,6 +548,8 @@ export interface HidConfigUpdate {
|
|||||||
export interface MsdConfigUpdate {
|
export interface MsdConfigUpdate {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
msd_dir?: string;
|
msd_dir?: string;
|
||||||
|
flash_inquiry_string?: string;
|
||||||
|
cdrom_inquiry_string?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkInterfaceInfo {
|
export interface NetworkInterfaceInfo {
|
||||||
|
|||||||
@@ -675,6 +675,8 @@ const config = ref({
|
|||||||
hid_ch9329_hybrid_mouse: false,
|
hid_ch9329_hybrid_mouse: false,
|
||||||
msd_enabled: false,
|
msd_enabled: false,
|
||||||
msd_dir: '',
|
msd_dir: '',
|
||||||
|
msd_flash_inquiry_string: 'One-KVM Virtual Flash',
|
||||||
|
msd_cdrom_inquiry_string: 'One-KVM Virtual CD-ROM',
|
||||||
otg_network_enabled: false,
|
otg_network_enabled: false,
|
||||||
otg_network_driver: 'ncm' as 'ncm' | 'ecm' | 'rndis',
|
otg_network_driver: 'ncm' as 'ncm' | 'ecm' | 'rndis',
|
||||||
otg_network_interface: '',
|
otg_network_interface: '',
|
||||||
@@ -1013,6 +1015,18 @@ const isHidFunctionSelectionValid = computed(() => {
|
|||||||
return !!(f.keyboard || f.mouse_relative || f.mouse_absolute || f.consumer)
|
return !!(f.keyboard || f.mouse_relative || f.mouse_absolute || f.consumer)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isValidInquiryString(value: string): boolean {
|
||||||
|
return /^[\x20-\x7e]{1,28}$/.test(value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
const areMsdInquiryStringsValid = computed(() =>
|
||||||
|
!config.value.msd_enabled
|
||||||
|
|| (
|
||||||
|
isValidInquiryString(config.value.msd_flash_inquiry_string)
|
||||||
|
&& isValidInquiryString(config.value.msd_cdrom_inquiry_string)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const otgVendorIdHex = ref('1d6b')
|
const otgVendorIdHex = ref('1d6b')
|
||||||
const otgProductIdHex = ref('0104')
|
const otgProductIdHex = ref('0104')
|
||||||
const otgManufacturer = ref('One-KVM')
|
const otgManufacturer = ref('One-KVM')
|
||||||
@@ -1144,6 +1158,7 @@ const isCh9329DescriptorDirty = computed(() => {
|
|||||||
const isHidSettingsValid = computed(() =>
|
const isHidSettingsValid = computed(() =>
|
||||||
isHidFunctionSelectionValid.value
|
isHidFunctionSelectionValid.value
|
||||||
&& isCh9329DescriptorValid.value
|
&& isCh9329DescriptorValid.value
|
||||||
|
&& areMsdInquiryStringsValid.value
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(bindMode, (mode) => {
|
watch(bindMode, (mode) => {
|
||||||
@@ -1471,6 +1486,8 @@ async function saveConfig() {
|
|||||||
msd: {
|
msd: {
|
||||||
enabled: otgEnabled && config.value.msd_enabled,
|
enabled: otgEnabled && config.value.msd_enabled,
|
||||||
msd_dir: config.value.msd_dir || undefined,
|
msd_dir: config.value.msd_dir || undefined,
|
||||||
|
flash_inquiry_string: config.value.msd_flash_inquiry_string,
|
||||||
|
cdrom_inquiry_string: config.value.msd_cdrom_inquiry_string,
|
||||||
},
|
},
|
||||||
network: {
|
network: {
|
||||||
enabled: otgEnabled && config.value.otg_network_enabled,
|
enabled: otgEnabled && config.value.otg_network_enabled,
|
||||||
@@ -1541,6 +1558,8 @@ async function loadConfig() {
|
|||||||
hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false,
|
hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false,
|
||||||
msd_enabled: msd.enabled || false,
|
msd_enabled: msd.enabled || false,
|
||||||
msd_dir: msd.msd_dir || '',
|
msd_dir: msd.msd_dir || '',
|
||||||
|
msd_flash_inquiry_string: msd.flash_inquiry_string || 'One-KVM Virtual Flash',
|
||||||
|
msd_cdrom_inquiry_string: msd.cdrom_inquiry_string || 'One-KVM Virtual CD-ROM',
|
||||||
otg_network_enabled: otgNetwork.enabled,
|
otg_network_enabled: otgNetwork.enabled,
|
||||||
otg_network_driver: otgNetwork.driver_mode,
|
otg_network_driver: otgNetwork.driver_mode,
|
||||||
uac_enabled: uac.enabled,
|
uac_enabled: uac.enabled,
|
||||||
@@ -3299,41 +3318,48 @@ watch(isWindows, () => {
|
|||||||
<h4 class="text-sm font-medium">{{ t('settings.otgHidProfile') }}</h4>
|
<h4 class="text-sm font-medium">{{ t('settings.otgHidProfile') }}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||||
<div>
|
<div class="flex items-center justify-between gap-4">
|
||||||
<Label>{{ t('settings.otgFunctionMouseRelative') }}</Label>
|
<div>
|
||||||
|
<Label>{{ t('settings.otgFunctionKeyboard') }}</Label>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="config.hid_otg_functions.keyboard" />
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>{{ t('settings.otgFunctionConsumer') }}</Label>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="config.hid_otg_functions.consumer" />
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>{{ t('settings.otgKeyboardLeds') }}</Label>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="config.hid_otg_keyboard_leds" :disabled="isKeyboardLedToggleDisabled" />
|
||||||
</div>
|
</div>
|
||||||
<Switch v-model="config.hid_otg_functions.mouse_relative" />
|
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>{{ t('settings.otgFunctionMouseAbsolute') }}</Label>
|
<Label>{{ t('settings.otgFunctionMouseRelative') }}</Label>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="config.hid_otg_functions.mouse_relative" />
|
||||||
</div>
|
</div>
|
||||||
<Switch v-model="config.hid_otg_functions.mouse_absolute" />
|
<Separator />
|
||||||
</div>
|
<div class="flex items-center justify-between gap-4">
|
||||||
</div>
|
<div>
|
||||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
<Label>{{ t('settings.otgFunctionMouseAbsolute') }}</Label>
|
||||||
<div class="flex items-center justify-between gap-4">
|
</div>
|
||||||
<div>
|
<Switch v-model="config.hid_otg_functions.mouse_absolute" />
|
||||||
<Label>{{ t('settings.otgFunctionKeyboard') }}</Label>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch v-model="config.hid_otg_functions.keyboard" />
|
<Separator />
|
||||||
</div>
|
<div class="flex items-center justify-between gap-4">
|
||||||
<Separator />
|
<Label>{{ t('settings.uacMic') }}</Label>
|
||||||
<div class="flex items-center justify-between gap-4">
|
<Switch v-model="config.uac_enabled" />
|
||||||
<div>
|
|
||||||
<Label>{{ t('settings.otgFunctionConsumer') }}</Label>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch v-model="config.hid_otg_functions.consumer" />
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<Label>{{ t('settings.otgKeyboardLeds') }}</Label>
|
|
||||||
</div>
|
|
||||||
<Switch v-model="config.hid_otg_keyboard_leds" :disabled="isKeyboardLedToggleDisabled" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||||
@@ -3349,6 +3375,24 @@ watch(isWindows, () => {
|
|||||||
<Label for="msd-dir">{{ t('settings.msdDir') }}</Label>
|
<Label for="msd-dir">{{ t('settings.msdDir') }}</Label>
|
||||||
<Input id="msd-dir" v-model="config.msd_dir" placeholder="/etc/one-kvm/msd" />
|
<Input id="msd-dir" v-model="config.msd_dir" placeholder="/etc/one-kvm/msd" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="msd-flash-inquiry-string">{{ t('settings.msdFlashInquiryString') }}</Label>
|
||||||
|
<Input
|
||||||
|
id="msd-flash-inquiry-string"
|
||||||
|
v-model="config.msd_flash_inquiry_string"
|
||||||
|
placeholder="One-KVM Virtual Flash"
|
||||||
|
maxlength="28"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="msd-cdrom-inquiry-string">{{ t('settings.msdCdromInquiryString') }}</Label>
|
||||||
|
<Input
|
||||||
|
id="msd-cdrom-inquiry-string"
|
||||||
|
v-model="config.msd_cdrom_inquiry_string"
|
||||||
|
placeholder="One-KVM Virtual CD-ROM"
|
||||||
|
maxlength="28"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||||
@@ -3412,12 +3456,6 @@ watch(isWindows, () => {
|
|||||||
{{ t('settings.otgRuntimeDegraded') }}: {{ otgNetworkStatus.error || t('common.error') }}
|
{{ t('settings.otgRuntimeDegraded') }}: {{ otgNetworkStatus.error || t('common.error') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<Label>{{ t('settings.uacMic') }}</Label>
|
|
||||||
<Switch v-model="config.uac_enabled" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-warning">
|
<p class="text-xs text-warning">
|
||||||
{{ t('settings.otgProfileWarning') }}
|
{{ t('settings.otgProfileWarning') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user