mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +08:00
feat(web): 新增 HID OTG 自检接口与设置页环境诊断面板,并优化在线升级状态文案本地化及重启后自动刷新体验
This commit is contained in:
@@ -2242,6 +2242,763 @@ pub struct HidStatus {
|
||||
pub screen_resolution: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OtgSelfCheckLevel {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OtgSelfCheckItem {
|
||||
pub id: &'static str,
|
||||
pub ok: bool,
|
||||
pub level: OtgSelfCheckLevel,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hint: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OtgSelfCheckResponse {
|
||||
pub overall_ok: bool,
|
||||
pub error_count: usize,
|
||||
pub warning_count: usize,
|
||||
pub hid_backend: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub selected_udc: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bound_udc: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub udc_state: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub udc_speed: Option<String>,
|
||||
pub available_udcs: Vec<String>,
|
||||
pub other_gadgets: Vec<String>,
|
||||
pub checks: Vec<OtgSelfCheckItem>,
|
||||
}
|
||||
|
||||
fn push_otg_check(
|
||||
checks: &mut Vec<OtgSelfCheckItem>,
|
||||
id: &'static str,
|
||||
ok: bool,
|
||||
level: OtgSelfCheckLevel,
|
||||
message: impl Into<String>,
|
||||
hint: Option<impl Into<String>>,
|
||||
path: Option<impl Into<String>>,
|
||||
) {
|
||||
checks.push(OtgSelfCheckItem {
|
||||
id,
|
||||
ok,
|
||||
level,
|
||||
message: message.into(),
|
||||
hint: hint.map(|v| v.into()),
|
||||
path: path.map(|v| v.into()),
|
||||
});
|
||||
}
|
||||
|
||||
fn list_dir_names(path: &std::path::Path) -> Vec<String> {
|
||||
let mut names = std::fs::read_dir(path)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.collect::<Vec<_>>();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
fn read_trimmed(path: &std::path::Path) -> Option<String> {
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
}
|
||||
|
||||
fn proc_modules_has(module_name: &str) -> bool {
|
||||
std::fs::read_to_string("/proc/modules")
|
||||
.ok()
|
||||
.map(|content| {
|
||||
content
|
||||
.lines()
|
||||
.filter_map(|line| line.split_whitespace().next())
|
||||
.any(|name| name == module_name)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn modules_metadata_has(module_name: &str) -> bool {
|
||||
let kernel_release = match read_trimmed(std::path::Path::new("/proc/sys/kernel/osrelease")) {
|
||||
Some(value) if !value.is_empty() => value,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let module_dir = std::path::Path::new("/lib/modules").join(kernel_release);
|
||||
let candidates = ["modules.builtin", "modules.builtin.modinfo", "modules.dep"];
|
||||
|
||||
candidates.iter().any(|filename| {
|
||||
let path = module_dir.join(filename);
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.map(|content| {
|
||||
let module_token = format!("/{module_name}.ko");
|
||||
content.lines().any(|line| {
|
||||
line.contains(&module_token)
|
||||
|| line.contains(module_name)
|
||||
|| line.contains(&module_name.replace('_', "-"))
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn kernel_config_option_enabled(option_name: &str) -> bool {
|
||||
let kernel_release = match read_trimmed(std::path::Path::new("/proc/sys/kernel/osrelease")) {
|
||||
Some(value) if !value.is_empty() => value,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let config_paths = [
|
||||
std::path::PathBuf::from(format!("/boot/config-{kernel_release}")),
|
||||
std::path::PathBuf::from("/boot/config"),
|
||||
std::path::PathBuf::from(format!("/lib/modules/{kernel_release}/build/.config")),
|
||||
];
|
||||
|
||||
config_paths.iter().any(|path| {
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.map(|content| {
|
||||
let enabled_y = format!("{option_name}=y");
|
||||
let enabled_m = format!("{option_name}=m");
|
||||
content
|
||||
.lines()
|
||||
.any(|line| line == enabled_y || line == enabled_m)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn detect_libcomposite_available(gadget_root: &std::path::Path) -> bool {
|
||||
let sys_module = std::path::Path::new("/sys/module/libcomposite").exists();
|
||||
if sys_module {
|
||||
return true;
|
||||
}
|
||||
|
||||
if proc_modules_has("libcomposite") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if modules_metadata_has("libcomposite") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if kernel_config_option_enabled("CONFIG_USB_LIBCOMPOSITE")
|
||||
|| kernel_config_option_enabled("CONFIG_USB_CONFIGFS")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: if usb_gadget path exists, libcomposite may be built-in and already active.
|
||||
gadget_root.exists()
|
||||
}
|
||||
|
||||
/// OTG self-check status for troubleshooting USB gadget issues
|
||||
pub async fn hid_otg_self_check(State(state): State<Arc<AppState>>) -> Json<OtgSelfCheckResponse> {
|
||||
let config = state.config.get();
|
||||
let hid_backend_is_otg = matches!(config.hid.backend, crate::config::HidBackend::Otg);
|
||||
let mut checks = Vec::new();
|
||||
|
||||
let build_response = |
|
||||
checks: Vec<OtgSelfCheckItem>,
|
||||
selected_udc: Option<String>,
|
||||
bound_udc: Option<String>,
|
||||
udc_state: Option<String>,
|
||||
udc_speed: Option<String>,
|
||||
available_udcs: Vec<String>,
|
||||
other_gadgets: Vec<String>,
|
||||
| {
|
||||
let error_count = checks
|
||||
.iter()
|
||||
.filter(|item| item.level == OtgSelfCheckLevel::Error)
|
||||
.count();
|
||||
let warning_count = checks
|
||||
.iter()
|
||||
.filter(|item| item.level == OtgSelfCheckLevel::Warn)
|
||||
.count();
|
||||
|
||||
Json(OtgSelfCheckResponse {
|
||||
overall_ok: error_count == 0,
|
||||
error_count,
|
||||
warning_count,
|
||||
hid_backend: format!("{:?}", config.hid.backend).to_lowercase(),
|
||||
selected_udc,
|
||||
bound_udc,
|
||||
udc_state,
|
||||
udc_speed,
|
||||
available_udcs,
|
||||
other_gadgets,
|
||||
checks,
|
||||
})
|
||||
};
|
||||
|
||||
let udc_root = std::path::Path::new("/sys/class/udc");
|
||||
let available_udcs = list_dir_names(udc_root);
|
||||
let selected_udc = config
|
||||
.hid
|
||||
.otg_udc
|
||||
.clone()
|
||||
.filter(|udc| !udc.trim().is_empty())
|
||||
.or_else(|| available_udcs.first().cloned());
|
||||
let mut udc_stage_ok = true;
|
||||
if !udc_root.exists() {
|
||||
udc_stage_ok = false;
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_dir_exists",
|
||||
false,
|
||||
OtgSelfCheckLevel::Error,
|
||||
"Check /sys/class/udc existence",
|
||||
Some("Ensure UDC/OTG kernel drivers are enabled"),
|
||||
Some("/sys/class/udc"),
|
||||
);
|
||||
} else if available_udcs.is_empty() {
|
||||
udc_stage_ok = false;
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_has_entries",
|
||||
false,
|
||||
OtgSelfCheckLevel::Error,
|
||||
"Check available UDC entries",
|
||||
Some("Ensure OTG controller is enabled in device tree"),
|
||||
Some("/sys/class/udc"),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_has_entries",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check available UDC entries",
|
||||
None::<String>,
|
||||
Some("/sys/class/udc"),
|
||||
);
|
||||
}
|
||||
|
||||
let mut configured_udc_ok = true;
|
||||
if let Some(config_udc) = config
|
||||
.hid
|
||||
.otg_udc
|
||||
.clone()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
if available_udcs.iter().any(|item| item == &config_udc) {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"configured_udc_valid",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check configured UDC validity",
|
||||
None::<String>,
|
||||
Some("/sys/class/udc"),
|
||||
);
|
||||
} else {
|
||||
configured_udc_ok = false;
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"configured_udc_valid",
|
||||
false,
|
||||
OtgSelfCheckLevel::Error,
|
||||
"Check configured UDC validity",
|
||||
Some("Please reselect UDC in HID OTG settings"),
|
||||
Some("/sys/class/udc"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"configured_udc_valid",
|
||||
!available_udcs.is_empty(),
|
||||
if available_udcs.is_empty() {
|
||||
OtgSelfCheckLevel::Warn
|
||||
} else {
|
||||
OtgSelfCheckLevel::Info
|
||||
},
|
||||
"Check configured UDC validity",
|
||||
Some("You can set hid_otg_udc in settings to avoid ambiguity in multi-controller setups"),
|
||||
Some("/sys/class/udc"),
|
||||
);
|
||||
}
|
||||
|
||||
if !udc_stage_ok || !configured_udc_ok {
|
||||
return build_response(
|
||||
checks,
|
||||
selected_udc,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
available_udcs,
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
|
||||
let gadget_root = std::path::Path::new("/sys/kernel/config/usb_gadget");
|
||||
let configfs_mounted = std::fs::read_to_string("/proc/mounts")
|
||||
.ok()
|
||||
.map(|mounts| {
|
||||
mounts.lines().any(|line| {
|
||||
let mut parts = line.split_whitespace();
|
||||
let _src = parts.next();
|
||||
let mount_point = parts.next();
|
||||
let fs_type = parts.next();
|
||||
mount_point == Some("/sys/kernel/config") && fs_type == Some("configfs")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut gadget_config_ok = true;
|
||||
|
||||
if configfs_mounted {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"configfs_mounted",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check configfs mount status",
|
||||
None::<String>,
|
||||
Some("/sys/kernel/config"),
|
||||
);
|
||||
} else {
|
||||
gadget_config_ok = false;
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"configfs_mounted",
|
||||
false,
|
||||
OtgSelfCheckLevel::Error,
|
||||
"Check configfs mount status",
|
||||
Some("Try: mount -t configfs none /sys/kernel/config"),
|
||||
Some("/sys/kernel/config"),
|
||||
);
|
||||
}
|
||||
|
||||
if gadget_root.exists() {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"usb_gadget_dir_exists",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check /sys/kernel/config/usb_gadget access",
|
||||
None::<String>,
|
||||
Some("/sys/kernel/config/usb_gadget"),
|
||||
);
|
||||
} else {
|
||||
gadget_config_ok = false;
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"usb_gadget_dir_exists",
|
||||
false,
|
||||
OtgSelfCheckLevel::Error,
|
||||
"Check /sys/kernel/config/usb_gadget access",
|
||||
Some("Ensure configfs and USB gadget support are enabled"),
|
||||
Some("/sys/kernel/config/usb_gadget"),
|
||||
);
|
||||
}
|
||||
|
||||
let libcomposite_available = detect_libcomposite_available(gadget_root);
|
||||
if libcomposite_available {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"libcomposite_loaded",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check libcomposite module status",
|
||||
None::<String>,
|
||||
Some("/sys/module/libcomposite"),
|
||||
);
|
||||
} else {
|
||||
gadget_config_ok = false;
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"libcomposite_loaded",
|
||||
false,
|
||||
OtgSelfCheckLevel::Error,
|
||||
"Check libcomposite module status",
|
||||
Some("Try: modprobe libcomposite"),
|
||||
Some("/sys/module/libcomposite"),
|
||||
);
|
||||
}
|
||||
|
||||
if !gadget_config_ok {
|
||||
return build_response(
|
||||
checks,
|
||||
selected_udc,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
available_udcs,
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
|
||||
let gadget_names = list_dir_names(gadget_root);
|
||||
let one_kvm_path = gadget_root.join("one-kvm");
|
||||
let one_kvm_exists = one_kvm_path.exists();
|
||||
if one_kvm_exists {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"one_kvm_gadget_exists",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check one-kvm gadget presence",
|
||||
None::<String>,
|
||||
Some(one_kvm_path.display().to_string()),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"one_kvm_gadget_exists",
|
||||
false,
|
||||
if hid_backend_is_otg {
|
||||
OtgSelfCheckLevel::Error
|
||||
} else {
|
||||
OtgSelfCheckLevel::Warn
|
||||
},
|
||||
"Check one-kvm gadget presence",
|
||||
Some("Enable OTG HID or MSD to let one-kvm gadget be created automatically"),
|
||||
Some(one_kvm_path.display().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
let other_gadgets = gadget_names
|
||||
.iter()
|
||||
.filter(|name| name.as_str() != "one-kvm")
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if other_gadgets.is_empty() {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"other_gadgets",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check for other gadget services",
|
||||
None::<String>,
|
||||
Some("/sys/kernel/config/usb_gadget"),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"other_gadgets",
|
||||
false,
|
||||
OtgSelfCheckLevel::Warn,
|
||||
"Check for other gadget services",
|
||||
Some("Potential UDC contention with one-kvm; check other OTG services"),
|
||||
Some("/sys/kernel/config/usb_gadget"),
|
||||
);
|
||||
}
|
||||
|
||||
let mut bound_udc = None;
|
||||
|
||||
if one_kvm_exists {
|
||||
let one_kvm_udc_path = one_kvm_path.join("UDC");
|
||||
let current_udc = read_trimmed(&one_kvm_udc_path).unwrap_or_default();
|
||||
if current_udc.is_empty() {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"one_kvm_bound_udc",
|
||||
false,
|
||||
OtgSelfCheckLevel::Warn,
|
||||
"Check one-kvm UDC binding",
|
||||
Some("Ensure HID/MSD is enabled and initialized successfully"),
|
||||
Some(one_kvm_udc_path.display().to_string()),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"one_kvm_bound_udc",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check one-kvm UDC binding",
|
||||
None::<String>,
|
||||
Some(one_kvm_udc_path.display().to_string()),
|
||||
);
|
||||
bound_udc = Some(current_udc);
|
||||
}
|
||||
|
||||
let functions_path = one_kvm_path.join("functions");
|
||||
let function_names = list_dir_names(&functions_path)
|
||||
.into_iter()
|
||||
.filter(|name| name.contains(".usb"))
|
||||
.collect::<Vec<_>>();
|
||||
let hid_functions = function_names
|
||||
.iter()
|
||||
.filter(|name| name.starts_with("hid.usb"))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if hid_functions.is_empty() {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"hid_functions_present",
|
||||
false,
|
||||
if hid_backend_is_otg {
|
||||
OtgSelfCheckLevel::Error
|
||||
} else {
|
||||
OtgSelfCheckLevel::Warn
|
||||
},
|
||||
"Check HID function creation",
|
||||
Some("Check OTG HID config and enable at least one HID function"),
|
||||
Some(functions_path.display().to_string()),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"hid_functions_present",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check HID function creation",
|
||||
None::<String>,
|
||||
Some(functions_path.display().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
let config_path = one_kvm_path.join("configs/c.1");
|
||||
if !config_path.exists() {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"config_c1_exists",
|
||||
false,
|
||||
OtgSelfCheckLevel::Error,
|
||||
"Check configs/c.1 structure",
|
||||
Some("Gadget structure is incomplete; try restarting One-KVM"),
|
||||
Some(config_path.display().to_string()),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"config_c1_exists",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check configs/c.1 structure",
|
||||
None::<String>,
|
||||
Some(config_path.display().to_string()),
|
||||
);
|
||||
|
||||
let linked_functions = list_dir_names(&config_path)
|
||||
.into_iter()
|
||||
.filter(|name| name.contains(".usb"))
|
||||
.collect::<Vec<_>>();
|
||||
let missing_links = function_names
|
||||
.iter()
|
||||
.filter(|func| !linked_functions.iter().any(|link| link == *func))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if missing_links.is_empty() {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"function_links_ok",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check function links in configs/c.1",
|
||||
None::<String>,
|
||||
Some(config_path.display().to_string()),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"function_links_ok",
|
||||
false,
|
||||
OtgSelfCheckLevel::Warn,
|
||||
"Check function links in configs/c.1",
|
||||
Some("Reinitialize OTG (toggle HID backend once or restart service)"),
|
||||
Some(config_path.display().to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let missing_hid_devices = hid_functions
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
let index = name.strip_prefix("hid.usb")?.parse::<u8>().ok()?;
|
||||
let dev_path = std::path::PathBuf::from(format!("/dev/hidg{}", index));
|
||||
if dev_path.exists() {
|
||||
None
|
||||
} else {
|
||||
Some(dev_path.display().to_string())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !hid_functions.is_empty() {
|
||||
if missing_hid_devices.is_empty() {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"hid_device_nodes",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check /dev/hidg* device nodes",
|
||||
None::<String>,
|
||||
Some("/dev/hidg*"),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"hid_device_nodes",
|
||||
false,
|
||||
OtgSelfCheckLevel::Warn,
|
||||
"Check /dev/hidg* device nodes",
|
||||
Some("Ensure gadget is bound and check kernel logs"),
|
||||
Some("/dev/hidg*"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !other_gadgets.is_empty() {
|
||||
let check_udc = bound_udc.clone().or_else(|| selected_udc.clone());
|
||||
if let Some(target_udc) = check_udc {
|
||||
let conflicting_gadgets = other_gadgets
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
let udc_file = gadget_root.join(name).join("UDC");
|
||||
let udc = read_trimmed(&udc_file)?;
|
||||
if udc == target_udc {
|
||||
Some(name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if conflicting_gadgets.is_empty() {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_conflict",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check UDC binding conflicts",
|
||||
None::<String>,
|
||||
Some("/sys/kernel/config/usb_gadget/*/UDC"),
|
||||
);
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_conflict",
|
||||
false,
|
||||
OtgSelfCheckLevel::Error,
|
||||
"Check UDC binding conflicts",
|
||||
Some("Stop other OTG services or switch one-kvm to an idle UDC"),
|
||||
Some("/sys/kernel/config/usb_gadget/*/UDC"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let active_udc = bound_udc.clone().or_else(|| selected_udc.clone());
|
||||
let mut udc_state = None;
|
||||
let mut udc_speed = None;
|
||||
|
||||
if let Some(udc) = active_udc.clone() {
|
||||
let state_path = udc_root.join(&udc).join("state");
|
||||
match read_trimmed(&state_path) {
|
||||
Some(state_name) if state_name.eq_ignore_ascii_case("configured") => {
|
||||
udc_state = Some(state_name.clone());
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_state",
|
||||
true,
|
||||
OtgSelfCheckLevel::Info,
|
||||
"Check UDC connection state",
|
||||
None::<String>,
|
||||
Some(state_path.display().to_string()),
|
||||
);
|
||||
}
|
||||
Some(state_name) => {
|
||||
udc_state = Some(state_name.clone());
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_state",
|
||||
false,
|
||||
OtgSelfCheckLevel::Warn,
|
||||
"Check UDC connection state",
|
||||
Some("Ensure target host is connected and has recognized the USB device"),
|
||||
Some(state_path.display().to_string()),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_state",
|
||||
false,
|
||||
OtgSelfCheckLevel::Warn,
|
||||
"Check UDC connection state",
|
||||
Some("Ensure UDC name is valid and check kernel permissions"),
|
||||
Some(state_path.display().to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let speed_path = udc_root.join(&udc).join("current_speed");
|
||||
if let Some(speed) = read_trimmed(&speed_path) {
|
||||
udc_speed = Some(speed.clone());
|
||||
let is_unknown = speed.eq_ignore_ascii_case("unknown");
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_speed",
|
||||
!is_unknown,
|
||||
if is_unknown {
|
||||
OtgSelfCheckLevel::Warn
|
||||
} else {
|
||||
OtgSelfCheckLevel::Info
|
||||
},
|
||||
"Check UDC current link speed",
|
||||
if is_unknown {
|
||||
Some("Device may not be fully enumerated; try reconnecting USB".to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
Some(speed_path.display().to_string()),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
push_otg_check(
|
||||
&mut checks,
|
||||
"udc_state",
|
||||
false,
|
||||
OtgSelfCheckLevel::Warn,
|
||||
"Check UDC connection state",
|
||||
Some("Ensure UDC is available and one-kvm gadget is bound first"),
|
||||
Some("/sys/class/udc"),
|
||||
);
|
||||
}
|
||||
|
||||
let error_count = checks
|
||||
.iter()
|
||||
.filter(|item| item.level == OtgSelfCheckLevel::Error)
|
||||
.count();
|
||||
let warning_count = checks
|
||||
.iter()
|
||||
.filter(|item| item.level == OtgSelfCheckLevel::Warn)
|
||||
.count();
|
||||
|
||||
Json(OtgSelfCheckResponse {
|
||||
overall_ok: error_count == 0,
|
||||
error_count,
|
||||
warning_count,
|
||||
hid_backend: format!("{:?}", config.hid.backend).to_lowercase(),
|
||||
selected_udc,
|
||||
bound_udc,
|
||||
udc_state,
|
||||
udc_speed,
|
||||
available_udcs,
|
||||
other_gadgets,
|
||||
checks,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get HID status
|
||||
pub async fn hid_status(State(state): State<Arc<AppState>>) -> Json<HidStatus> {
|
||||
let info = state.hid.info().await;
|
||||
|
||||
@@ -60,6 +60,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
.route("/webrtc/close", post(handlers::webrtc_close_session))
|
||||
// HID endpoints
|
||||
.route("/hid/status", get(handlers::hid_status))
|
||||
.route("/hid/otg/self-check", get(handlers::hid_otg_self_check))
|
||||
.route("/hid/reset", post(handlers::hid_reset))
|
||||
// WebSocket HID endpoint (for MJPEG mode)
|
||||
.route("/ws/hid", any(ws_hid_handler))
|
||||
|
||||
@@ -303,6 +303,28 @@ export const hidApi = {
|
||||
screen_resolution: [number, number] | null
|
||||
}>('/hid/status'),
|
||||
|
||||
otgSelfCheck: () =>
|
||||
request<{
|
||||
overall_ok: boolean
|
||||
error_count: number
|
||||
warning_count: number
|
||||
hid_backend: string
|
||||
selected_udc: string | null
|
||||
bound_udc: string | null
|
||||
udc_state: string | null
|
||||
udc_speed: string | null
|
||||
available_udcs: string[]
|
||||
other_gadgets: string[]
|
||||
checks: Array<{
|
||||
id: string
|
||||
ok: boolean
|
||||
level: 'info' | 'warn' | 'error'
|
||||
message: string
|
||||
hint?: string
|
||||
path?: string
|
||||
}>
|
||||
}>('/hid/otg/self-check'),
|
||||
|
||||
keyboard: async (type: 'down' | 'up', key: number, modifiers?: {
|
||||
ctrl?: boolean
|
||||
shift?: boolean
|
||||
|
||||
@@ -440,6 +440,7 @@ export default {
|
||||
hid: 'HID',
|
||||
msd: 'MSD',
|
||||
atx: 'ATX',
|
||||
environment: 'Environment',
|
||||
network: 'Network',
|
||||
users: 'Users',
|
||||
hardware: 'Hardware',
|
||||
@@ -521,6 +522,11 @@ export default {
|
||||
updatePhaseRestarting: 'Restarting',
|
||||
updatePhaseSuccess: 'Success',
|
||||
updatePhaseFailed: 'Failed',
|
||||
updateMsgChecking: 'Checking for updates',
|
||||
updateMsgDownloading: 'Downloading binary',
|
||||
updateMsgVerifying: 'Verifying (SHA256)',
|
||||
updateMsgInstalling: 'Replacing binary',
|
||||
updateMsgRestarting: 'Restarting service',
|
||||
// Auth
|
||||
auth: 'Access',
|
||||
authSettings: 'Access Settings',
|
||||
@@ -650,6 +656,86 @@ export default {
|
||||
serialNumber: 'Serial Number',
|
||||
serialNumberAuto: 'Auto-generated',
|
||||
descriptorWarning: 'Changing these settings will reconnect the USB device',
|
||||
otgSelfCheck: {
|
||||
title: 'OTG Self-Check',
|
||||
desc: 'Check UDC, gadget binding, and link status',
|
||||
run: 'Run Self-Check',
|
||||
failed: 'Failed to run OTG self-check',
|
||||
overall: 'Overall Status',
|
||||
ok: 'Healthy',
|
||||
hasIssues: 'Issues Found',
|
||||
summary: 'Issue Summary',
|
||||
counts: '{errors} errors, {warnings} warnings',
|
||||
groupCounts: '{ok} passed, {warnings} warnings, {errors} errors',
|
||||
notRun: 'Not run',
|
||||
status: {
|
||||
ok: 'Healthy',
|
||||
warn: 'Warning',
|
||||
error: 'Error',
|
||||
skipped: 'Skipped',
|
||||
},
|
||||
groups: {
|
||||
udc: 'UDC Basics',
|
||||
gadgetConfig: 'Gadget Config',
|
||||
oneKvm: 'one-kvm Gadget',
|
||||
functions: 'Functions & Nodes',
|
||||
link: 'Link State',
|
||||
},
|
||||
values: {
|
||||
missing: 'Missing',
|
||||
notConfigured: 'Not configured',
|
||||
mounted: 'Mounted',
|
||||
unmounted: 'Unmounted',
|
||||
available: 'Available',
|
||||
unavailable: 'Unavailable',
|
||||
exists: 'Exists',
|
||||
none: 'None',
|
||||
unbound: 'Unbound',
|
||||
noConflict: 'No conflict',
|
||||
conflict: 'Conflict',
|
||||
unknown: 'Unknown',
|
||||
normal: 'Normal',
|
||||
abnormal: 'Abnormal',
|
||||
},
|
||||
selectedUdc: 'Target UDC',
|
||||
boundUdc: 'Bound UDC',
|
||||
messages: {
|
||||
udc_dir_exists: 'UDC directory check',
|
||||
udc_has_entries: 'UDC check',
|
||||
configfs_mounted: 'configfs check',
|
||||
usb_gadget_dir_exists: 'usb_gadget check',
|
||||
libcomposite_loaded: 'libcomposite check',
|
||||
one_kvm_gadget_exists: 'one-kvm gadget check',
|
||||
other_gadgets: 'Other gadget check',
|
||||
configured_udc_valid: 'Configured UDC check',
|
||||
one_kvm_bound_udc: 'Bound UDC check',
|
||||
hid_functions_present: 'HID function check',
|
||||
config_c1_exists: 'configs/c.1 check',
|
||||
function_links_ok: 'Function link check',
|
||||
hid_device_nodes: 'HID node check',
|
||||
udc_conflict: 'UDC conflict check',
|
||||
udc_state: 'UDC state check',
|
||||
udc_speed: 'UDC speed check',
|
||||
},
|
||||
hints: {
|
||||
udc_dir_exists: 'Ensure UDC/OTG kernel drivers are enabled',
|
||||
udc_has_entries: 'Ensure OTG controller is enabled in device tree',
|
||||
configfs_mounted: 'Try: mount -t configfs none /sys/kernel/config',
|
||||
usb_gadget_dir_exists: 'Ensure configfs and USB gadget support are enabled',
|
||||
libcomposite_loaded: 'Try: modprobe libcomposite',
|
||||
one_kvm_gadget_exists: 'Enable OTG HID or MSD to let one-kvm gadget be created automatically',
|
||||
other_gadgets: 'Potential UDC contention with one-kvm; check other OTG services',
|
||||
configured_udc_valid: 'Please reselect UDC in HID OTG settings',
|
||||
one_kvm_bound_udc: 'Ensure HID/MSD is enabled and initialized successfully',
|
||||
hid_functions_present: 'Check OTG HID config and enable at least one HID function',
|
||||
config_c1_exists: 'Gadget structure is incomplete; try restarting One-KVM',
|
||||
function_links_ok: 'Reinitialize OTG (toggle HID backend once or restart service)',
|
||||
hid_device_nodes: 'Ensure gadget is bound and check kernel logs',
|
||||
udc_conflict: 'Stop other OTG services or switch one-kvm to an idle UDC',
|
||||
udc_state: 'Ensure target host is connected and has recognized the USB device',
|
||||
udc_speed: 'Device may not be fully enumerated; try reconnecting USB',
|
||||
},
|
||||
},
|
||||
// WebRTC / ICE
|
||||
webrtcSettings: 'WebRTC Settings',
|
||||
webrtcSettingsDesc: 'Configure STUN/TURN servers for NAT traversal',
|
||||
|
||||
@@ -440,6 +440,7 @@ export default {
|
||||
hid: 'HID',
|
||||
msd: 'MSD',
|
||||
atx: 'ATX',
|
||||
environment: '环境',
|
||||
network: '网络',
|
||||
users: '用户',
|
||||
hardware: '硬件',
|
||||
@@ -521,6 +522,11 @@ export default {
|
||||
updatePhaseRestarting: '重启中',
|
||||
updatePhaseSuccess: '成功',
|
||||
updatePhaseFailed: '失败',
|
||||
updateMsgChecking: '检查更新中',
|
||||
updateMsgDownloading: '下载中',
|
||||
updateMsgVerifying: '校验中(SHA256)',
|
||||
updateMsgInstalling: '替换程序中',
|
||||
updateMsgRestarting: '服务重启中',
|
||||
// Auth
|
||||
auth: '访问控制',
|
||||
authSettings: '访问设置',
|
||||
@@ -650,6 +656,86 @@ export default {
|
||||
serialNumber: '序列号',
|
||||
serialNumberAuto: '自动生成',
|
||||
descriptorWarning: '修改这些设置将导致 USB 设备重新连接',
|
||||
otgSelfCheck: {
|
||||
title: 'OTG 自检',
|
||||
desc: '检查 UDC、gadget 绑定和连接状态',
|
||||
run: '运行自检',
|
||||
failed: '执行 OTG 自检失败',
|
||||
overall: '总体状态',
|
||||
ok: '正常',
|
||||
hasIssues: '存在问题',
|
||||
summary: '问题统计',
|
||||
counts: '错误 {errors},警告 {warnings}',
|
||||
groupCounts: '通过 {ok},警告 {warnings},错误 {errors}',
|
||||
notRun: '未执行',
|
||||
status: {
|
||||
ok: '正常',
|
||||
warn: '告警',
|
||||
error: '异常',
|
||||
skipped: '跳过',
|
||||
},
|
||||
groups: {
|
||||
udc: 'UDC 基础',
|
||||
gadgetConfig: 'gadget 配置',
|
||||
oneKvm: 'one-kvm gadget',
|
||||
functions: '功能与设备节点',
|
||||
link: '连接状态',
|
||||
},
|
||||
values: {
|
||||
missing: '不存在',
|
||||
notConfigured: '未配置',
|
||||
mounted: '已挂载',
|
||||
unmounted: '未挂载',
|
||||
available: '可用',
|
||||
unavailable: '不可用',
|
||||
exists: '存在',
|
||||
none: '无',
|
||||
unbound: '未绑定',
|
||||
noConflict: '无冲突',
|
||||
conflict: '冲突',
|
||||
unknown: '未知',
|
||||
normal: '正常',
|
||||
abnormal: '异常',
|
||||
},
|
||||
selectedUdc: '目标 UDC',
|
||||
boundUdc: '已绑定 UDC',
|
||||
messages: {
|
||||
udc_dir_exists: 'UDC 目录检查',
|
||||
udc_has_entries: 'UDC 检查',
|
||||
configfs_mounted: 'configfs 检查',
|
||||
usb_gadget_dir_exists: 'usb_gadget 检查',
|
||||
libcomposite_loaded: 'libcomposite 检查',
|
||||
one_kvm_gadget_exists: 'one-kvm gadget 检查',
|
||||
other_gadgets: '其他 gadget 检查',
|
||||
configured_udc_valid: '配置 UDC 检查',
|
||||
one_kvm_bound_udc: 'gadget 绑定 UDC 检查',
|
||||
hid_functions_present: 'HID 函数检查',
|
||||
config_c1_exists: 'configs/c.1 检查',
|
||||
function_links_ok: 'functions 链接检查',
|
||||
hid_device_nodes: 'HID 设备节点检查',
|
||||
udc_conflict: 'UDC 冲突检查',
|
||||
udc_state: 'UDC 状态检查',
|
||||
udc_speed: 'UDC 速率检查',
|
||||
},
|
||||
hints: {
|
||||
udc_dir_exists: '请确认内核已启用 UDC/OTG 驱动',
|
||||
udc_has_entries: '请确认 OTG 控制器已在设备树中启用',
|
||||
configfs_mounted: '可执行: mount -t configfs none /sys/kernel/config',
|
||||
usb_gadget_dir_exists: '请确认 configfs 与 USB gadget 支持已启用',
|
||||
libcomposite_loaded: '可执行: modprobe libcomposite',
|
||||
one_kvm_gadget_exists: '启用 OTG HID 或 MSD 后会自动创建 one-kvm gadget',
|
||||
other_gadgets: '可能与 one-kvm 抢占 UDC,请检查是否有其他 OTG 服务',
|
||||
configured_udc_valid: '请在 HID OTG 设置中重新选择 UDC',
|
||||
one_kvm_bound_udc: '请确认 HID/MSD 已启用并成功初始化',
|
||||
hid_functions_present: '请检查 OTG HID 配置是否至少启用了一个 HID 功能',
|
||||
config_c1_exists: 'gadget 结构不完整,请尝试重启 One-KVM',
|
||||
function_links_ok: '建议重新初始化 OTG(切换一次 HID 后端或重启服务)',
|
||||
hid_device_nodes: '请确认 gadget 已绑定并检查内核日志',
|
||||
udc_conflict: '请停用其他 OTG 服务或切换 one-kvm 到空闲 UDC',
|
||||
udc_state: '请确认已连接被控机,且被控机已识别 USB 设备',
|
||||
udc_speed: '设备可能未完成枚举,可尝试重插 USB',
|
||||
},
|
||||
},
|
||||
// WebRTC / ICE
|
||||
webrtcSettings: 'WebRTC 设置',
|
||||
webrtcSettingsDesc: '配置 STUN/TURN 服务器以实现 NAT 穿透',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
authApi,
|
||||
configApi,
|
||||
hidApi,
|
||||
streamApi,
|
||||
atxConfigApi,
|
||||
extensionsApi,
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
Check,
|
||||
HardDrive,
|
||||
Power,
|
||||
Server,
|
||||
Menu,
|
||||
Lock,
|
||||
User,
|
||||
@@ -79,7 +81,7 @@ import {
|
||||
Radio,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { t, te, locale } = useI18n()
|
||||
const systemStore = useSystemStore()
|
||||
const configStore = useConfigStore()
|
||||
const authStore = useAuthStore()
|
||||
@@ -107,15 +109,16 @@ const navGroups = computed(() => [
|
||||
{ id: 'hid', label: t('settings.hid'), icon: Keyboard, status: config.value.hid_backend.toUpperCase() },
|
||||
...(config.value.msd_enabled ? [{ id: 'msd', label: t('settings.msd'), icon: HardDrive }] : []),
|
||||
{ id: 'atx', label: t('settings.atx'), icon: Power },
|
||||
{ id: 'environment', label: t('settings.environment'), icon: Server },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('settings.extensions'),
|
||||
items: [
|
||||
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
|
||||
{ id: 'ext-rustdesk', label: t('extensions.rustdesk.title'), icon: ScreenShare },
|
||||
{ id: 'ext-rtsp', label: t('extensions.rtsp.title'), icon: Radio },
|
||||
{ id: 'ext-remote-access', label: t('extensions.remoteAccess.title'), icon: ExternalLink },
|
||||
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -230,6 +233,9 @@ const updateChannel = ref<UpdateChannel>('stable')
|
||||
const updateOverview = ref<UpdateOverviewResponse | null>(null)
|
||||
const updateStatus = ref<UpdateStatusResponse | null>(null)
|
||||
const updateLoading = ref(false)
|
||||
const updateSawRestarting = ref(false)
|
||||
const updateSawRequestFailure = ref(false)
|
||||
const updateAutoReloadTriggered = ref(false)
|
||||
const updateRunning = computed(() => {
|
||||
const phase = updateStatus.value?.phase
|
||||
return phase === 'checking'
|
||||
@@ -332,6 +338,207 @@ const showLowEndpointHint = computed(() =>
|
||||
config.value.hid_backend === 'otg' && isLowEndpointUdc.value
|
||||
)
|
||||
|
||||
type OtgSelfCheckLevel = 'info' | 'warn' | 'error'
|
||||
type OtgCheckGroupStatus = 'ok' | 'warn' | 'error' | 'skipped'
|
||||
|
||||
interface OtgSelfCheckItem {
|
||||
id: string
|
||||
ok: boolean
|
||||
level: OtgSelfCheckLevel
|
||||
message: string
|
||||
hint?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
interface OtgSelfCheckResult {
|
||||
overall_ok: boolean
|
||||
error_count: number
|
||||
warning_count: number
|
||||
hid_backend: string
|
||||
selected_udc: string | null
|
||||
bound_udc: string | null
|
||||
udc_state: string | null
|
||||
udc_speed: string | null
|
||||
available_udcs: string[]
|
||||
other_gadgets: string[]
|
||||
checks: OtgSelfCheckItem[]
|
||||
}
|
||||
|
||||
interface OtgCheckGroupDef {
|
||||
id: string
|
||||
titleKey: string
|
||||
checkIds: string[]
|
||||
}
|
||||
|
||||
interface OtgCheckGroup {
|
||||
id: string
|
||||
titleKey: string
|
||||
status: OtgCheckGroupStatus
|
||||
okCount: number
|
||||
warningCount: number
|
||||
errorCount: number
|
||||
items: OtgSelfCheckItem[]
|
||||
}
|
||||
|
||||
const otgSelfCheckLoading = ref(false)
|
||||
const otgSelfCheckResult = ref<OtgSelfCheckResult | null>(null)
|
||||
const otgSelfCheckError = ref('')
|
||||
const otgRunButtonPressed = ref(false)
|
||||
|
||||
const otgCheckGroupDefs: OtgCheckGroupDef[] = [
|
||||
{
|
||||
id: 'udc',
|
||||
titleKey: 'settings.otgSelfCheck.groups.udc',
|
||||
checkIds: ['udc_dir_exists', 'udc_has_entries', 'configured_udc_valid'],
|
||||
},
|
||||
{
|
||||
id: 'gadget_config',
|
||||
titleKey: 'settings.otgSelfCheck.groups.gadgetConfig',
|
||||
checkIds: ['configfs_mounted', 'usb_gadget_dir_exists', 'libcomposite_loaded'],
|
||||
},
|
||||
{
|
||||
id: 'one_kvm',
|
||||
titleKey: 'settings.otgSelfCheck.groups.oneKvm',
|
||||
checkIds: ['one_kvm_gadget_exists', 'one_kvm_bound_udc', 'other_gadgets', 'udc_conflict'],
|
||||
},
|
||||
{
|
||||
id: 'functions',
|
||||
titleKey: 'settings.otgSelfCheck.groups.functions',
|
||||
checkIds: ['hid_functions_present', 'config_c1_exists', 'function_links_ok', 'hid_device_nodes'],
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
titleKey: 'settings.otgSelfCheck.groups.link',
|
||||
checkIds: ['udc_state', 'udc_speed'],
|
||||
},
|
||||
]
|
||||
|
||||
const otgCheckGroups = computed<OtgCheckGroup[]>(() => {
|
||||
const items = otgSelfCheckResult.value?.checks || []
|
||||
return otgCheckGroupDefs.map((group) => {
|
||||
const groupItems = items.filter(item => group.checkIds.includes(item.id))
|
||||
const errorCount = groupItems.filter(item => item.level === 'error').length
|
||||
const warningCount = groupItems.filter(item => item.level === 'warn').length
|
||||
const okCount = Math.max(0, groupItems.length - errorCount - warningCount)
|
||||
let status: OtgCheckGroupStatus = 'skipped'
|
||||
if (groupItems.length > 0) {
|
||||
if (errorCount > 0) status = 'error'
|
||||
else if (warningCount > 0) status = 'warn'
|
||||
else status = 'ok'
|
||||
}
|
||||
|
||||
return {
|
||||
id: group.id,
|
||||
titleKey: group.titleKey,
|
||||
status,
|
||||
okCount,
|
||||
warningCount,
|
||||
errorCount,
|
||||
items: groupItems,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function otgCheckLevelClass(level: OtgSelfCheckLevel): string {
|
||||
if (level === 'error') return 'bg-red-500'
|
||||
if (level === 'warn') return 'bg-amber-500'
|
||||
return 'bg-blue-500'
|
||||
}
|
||||
|
||||
function otgCheckStatusText(level: OtgSelfCheckLevel): string {
|
||||
if (level === 'error') return t('common.error')
|
||||
if (level === 'warn') return t('common.warning')
|
||||
return t('common.info')
|
||||
}
|
||||
|
||||
function otgGroupStatusClass(status: OtgCheckGroupStatus): string {
|
||||
if (status === 'error') return 'bg-red-500'
|
||||
if (status === 'warn') return 'bg-amber-500'
|
||||
if (status === 'ok') return 'bg-emerald-500'
|
||||
return 'bg-muted-foreground/40'
|
||||
}
|
||||
|
||||
function otgGroupStatusText(status: OtgCheckGroupStatus): string {
|
||||
return t(`settings.otgSelfCheck.status.${status}`)
|
||||
}
|
||||
|
||||
function otgGroupSummary(group: OtgCheckGroup): string {
|
||||
if (group.items.length === 0) {
|
||||
return t('settings.otgSelfCheck.notRun')
|
||||
}
|
||||
return t('settings.otgSelfCheck.groupCounts', {
|
||||
ok: group.okCount,
|
||||
warnings: group.warningCount,
|
||||
errors: group.errorCount,
|
||||
})
|
||||
}
|
||||
|
||||
function otgCheckMessage(item: OtgSelfCheckItem): string {
|
||||
const key = `settings.otgSelfCheck.messages.${item.id}`
|
||||
const label = te(key) ? t(key) : item.message
|
||||
const result = otgSelfCheckResult.value
|
||||
if (!result) return label
|
||||
|
||||
const value = (name: string) => t(`settings.otgSelfCheck.values.${name}`)
|
||||
|
||||
switch (item.id) {
|
||||
case 'udc_has_entries':
|
||||
return `${label}:${result.available_udcs.length ? result.available_udcs.join(', ') : value('missing')}`
|
||||
case 'configured_udc_valid':
|
||||
if (!result.selected_udc) return `${label}:${value('notConfigured')}`
|
||||
return `${label}:${item.ok ? result.selected_udc : `${value('missing')}/${result.selected_udc}`}`
|
||||
case 'configfs_mounted':
|
||||
return `${label}:${item.ok ? value('mounted') : value('unmounted')}`
|
||||
case 'usb_gadget_dir_exists':
|
||||
return `${label}:${item.ok ? value('available') : value('unavailable')}`
|
||||
case 'libcomposite_loaded':
|
||||
return `${label}:${item.ok ? value('available') : value('unavailable')}`
|
||||
case 'one_kvm_gadget_exists':
|
||||
return `${label}:${item.ok ? value('exists') : value('missing')}`
|
||||
case 'other_gadgets':
|
||||
return `${label}:${result.other_gadgets.length ? result.other_gadgets.join(', ') : value('none')}`
|
||||
case 'one_kvm_bound_udc':
|
||||
return `${label}:${result.bound_udc || value('unbound')}`
|
||||
case 'udc_conflict':
|
||||
return `${label}:${item.ok ? value('noConflict') : value('conflict')}`
|
||||
case 'udc_state':
|
||||
return `${label}:${result.udc_state || value('unknown')}`
|
||||
case 'udc_speed':
|
||||
return `${label}:${result.udc_speed || value('unknown')}`
|
||||
default:
|
||||
return `${label}:${item.ok ? value('normal') : value('abnormal')}`
|
||||
}
|
||||
}
|
||||
|
||||
function otgCheckHint(item: OtgSelfCheckItem): string {
|
||||
if (!item.hint) return ''
|
||||
const key = `settings.otgSelfCheck.hints.${item.id}`
|
||||
return te(key) ? t(key) : item.hint
|
||||
}
|
||||
|
||||
async function runOtgSelfCheck() {
|
||||
otgSelfCheckLoading.value = true
|
||||
otgSelfCheckError.value = ''
|
||||
try {
|
||||
otgSelfCheckResult.value = await hidApi.otgSelfCheck()
|
||||
} catch (e) {
|
||||
console.error('Failed to run OTG self-check:', e)
|
||||
otgSelfCheckError.value = t('settings.otgSelfCheck.failed')
|
||||
} finally {
|
||||
otgSelfCheckLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onRunOtgSelfCheckClick() {
|
||||
if (!otgSelfCheckLoading.value) {
|
||||
otgRunButtonPressed.value = true
|
||||
window.setTimeout(() => {
|
||||
otgRunButtonPressed.value = false
|
||||
}, 160)
|
||||
}
|
||||
await runOtgSelfCheck()
|
||||
}
|
||||
|
||||
function alignHidProfileForLowEndpoint() {
|
||||
if (hidProfileAligned.value) return
|
||||
if (!configLoaded.value || !devicesLoaded.value) return
|
||||
@@ -1148,8 +1355,18 @@ async function loadUpdateOverview() {
|
||||
async function refreshUpdateStatus() {
|
||||
try {
|
||||
updateStatus.value = await updateApi.status()
|
||||
|
||||
if (updateSawRestarting.value && !updateAutoReloadTriggered.value) {
|
||||
if (updateSawRequestFailure.value || updateStatus.value.phase === 'idle') {
|
||||
updateAutoReloadTriggered.value = true
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh update status:', e)
|
||||
if (updateSawRestarting.value) {
|
||||
updateSawRequestFailure.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,6 +1381,9 @@ function startUpdatePolling() {
|
||||
if (updateStatusTimer !== null) return
|
||||
updateStatusTimer = window.setInterval(async () => {
|
||||
await refreshUpdateStatus()
|
||||
if (updateStatus.value?.phase === 'restarting') {
|
||||
updateSawRestarting.value = true
|
||||
}
|
||||
if (!updateRunning.value) {
|
||||
stopUpdatePolling()
|
||||
await loadUpdateOverview()
|
||||
@@ -1173,6 +1393,9 @@ function startUpdatePolling() {
|
||||
|
||||
async function startOnlineUpgrade() {
|
||||
try {
|
||||
updateSawRestarting.value = false
|
||||
updateSawRequestFailure.value = false
|
||||
updateAutoReloadTriggered.value = false
|
||||
await updateApi.upgrade({ channel: updateChannel.value })
|
||||
await refreshUpdateStatus()
|
||||
startUpdatePolling()
|
||||
@@ -1195,6 +1418,25 @@ function updatePhaseText(phase?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function localizeUpdateMessage(message?: string): string | null {
|
||||
if (!message) return null
|
||||
|
||||
if (message === 'Checking for updates') return t('settings.updateMsgChecking')
|
||||
if (message.startsWith('Downloading binary')) {
|
||||
return message.replace('Downloading binary', t('settings.updateMsgDownloading'))
|
||||
}
|
||||
if (message === 'Verifying sha256') return t('settings.updateMsgVerifying')
|
||||
if (message === 'Replacing binary') return t('settings.updateMsgInstalling')
|
||||
if (message === 'Restarting service') return t('settings.updateMsgRestarting')
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
function updateStatusBadgeText(): string {
|
||||
return localizeUpdateMessage(updateStatus.value?.message)
|
||||
|| updatePhaseText(updateStatus.value?.phase)
|
||||
}
|
||||
|
||||
async function saveRustdeskConfig() {
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
@@ -1462,11 +1704,17 @@ onMounted(async () => {
|
||||
if (updateRunning.value) {
|
||||
startUpdatePolling()
|
||||
}
|
||||
|
||||
await runOtgSelfCheck()
|
||||
})
|
||||
|
||||
watch(updateChannel, async () => {
|
||||
await loadUpdateOverview()
|
||||
})
|
||||
|
||||
watch(() => config.value.hid_backend, async () => {
|
||||
await runOtgSelfCheck()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1939,6 +2187,105 @@ watch(updateChannel, async () => {
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Environment Section -->
|
||||
<div v-show="activeSection === 'environment'" class="space-y-4 max-w-3xl">
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
||||
<div class="space-y-1.5">
|
||||
<CardTitle>{{ t('settings.otgSelfCheck.title') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.otgSelfCheck.desc') }}</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="otgSelfCheckLoading"
|
||||
:class="[
|
||||
'transition-all duration-150 active:scale-95 active:brightness-95',
|
||||
otgRunButtonPressed ? 'scale-95 brightness-95' : ''
|
||||
]"
|
||||
@click="onRunOtgSelfCheckClick"
|
||||
>
|
||||
<RefreshCw class="h-4 w-4 mr-2" :class="{ 'animate-spin': otgSelfCheckLoading }" />
|
||||
{{ t('settings.otgSelfCheck.run') }}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<p v-if="otgSelfCheckError" class="text-xs text-red-600 dark:text-red-400">
|
||||
{{ otgSelfCheckError }}
|
||||
</p>
|
||||
|
||||
<template v-if="otgSelfCheckResult">
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<Badge
|
||||
:variant="otgSelfCheckResult.overall_ok ? 'default' : 'destructive'"
|
||||
class="font-medium"
|
||||
>
|
||||
{{ t('settings.otgSelfCheck.overall') }}:{{ otgSelfCheckResult.overall_ok ? t('settings.otgSelfCheck.ok') : t('settings.otgSelfCheck.hasIssues') }}
|
||||
</Badge>
|
||||
<Badge variant="outline" class="font-normal">
|
||||
{{ t('settings.otgSelfCheck.counts', { errors: otgSelfCheckResult.error_count, warnings: otgSelfCheckResult.warning_count }) }}
|
||||
</Badge>
|
||||
<Badge variant="secondary" class="font-normal">
|
||||
{{ t('settings.otgSelfCheck.selectedUdc') }}:{{ otgSelfCheckResult.selected_udc || '-' }}
|
||||
</Badge>
|
||||
<Badge variant="secondary" class="font-normal">
|
||||
{{ t('settings.otgSelfCheck.boundUdc') }}:{{ otgSelfCheckResult.bound_udc || '-' }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border divide-y">
|
||||
<details
|
||||
v-for="group in otgCheckGroups"
|
||||
:key="group.id"
|
||||
:open="group.status === 'error' || group.status === 'warn'"
|
||||
class="group"
|
||||
>
|
||||
<summary class="list-none cursor-pointer px-4 py-3 flex items-center justify-between gap-3 hover:bg-muted/40">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="inline-block h-2 w-2 rounded-full shrink-0" :class="otgGroupStatusClass(group.status)" />
|
||||
<span class="text-sm font-medium truncate leading-6">{{ t(group.titleKey) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="text-xs text-muted-foreground">{{ otgGroupSummary(group) }}</span>
|
||||
<Badge variant="outline" class="text-[10px] h-5 px-1.5">{{ otgGroupStatusText(group.status) }}</Badge>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div v-if="group.items.length > 0" class="border-t bg-muted/20">
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
class="px-4 py-3 border-b last:border-b-0"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="inline-block h-2 w-2 rounded-full mt-1.5 shrink-0" :class="otgCheckLevelClass(item.level)" />
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm leading-5">{{ otgCheckMessage(item) }}</p>
|
||||
<span class="text-[11px] text-muted-foreground shrink-0">{{ otgCheckStatusText(item.level) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
|
||||
<span v-if="item.hint">{{ otgCheckHint(item) }}</span>
|
||||
<code v-if="item.path" class="font-mono break-all">{{ item.path }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="border-t bg-muted/20 px-4 py-3 text-xs text-muted-foreground">
|
||||
{{ t('settings.otgSelfCheck.notRun') }}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else-if="otgSelfCheckLoading" class="text-xs text-muted-foreground">
|
||||
{{ t('common.loading') }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Access Section -->
|
||||
@@ -2843,9 +3190,21 @@ watch(updateChannel, async () => {
|
||||
<!-- About Section -->
|
||||
<div v-show="activeSection === 'about'" class="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.onlineUpgrade') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.onlineUpgradeDesc') }}</CardDescription>
|
||||
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
||||
<div class="space-y-1.5">
|
||||
<CardTitle>{{ t('settings.onlineUpgrade') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.onlineUpgradeDesc') }}</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:aria-label="t('common.refresh')"
|
||||
:disabled="updateRunning || updateLoading"
|
||||
@click="loadUpdateOverview"
|
||||
>
|
||||
<RefreshCw :class="['h-4 w-4', (updateLoading || updateRunning) ? 'animate-spin' : '']" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
@@ -2876,9 +3235,9 @@ watch(updateChannel, async () => {
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="max-w-[60%] truncate"
|
||||
:title="updateStatus?.message || updatePhaseText(updateStatus?.phase)"
|
||||
:title="updateStatusBadgeText()"
|
||||
>
|
||||
{{ updateStatus?.message || updatePhaseText(updateStatus?.phase) }}
|
||||
{{ updateStatusBadgeText() }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div v-if="updateRunning || updateStatus?.phase === 'failed' || updateStatus?.phase === 'success'" class="w-full h-2 bg-muted rounded overflow-hidden">
|
||||
@@ -2905,10 +3264,6 @@ watch(updateChannel, async () => {
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" :disabled="updateRunning" @click="loadUpdateOverview">
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
{{ t('common.refresh') }}
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="updateRunning || !updateOverview?.upgrade_available"
|
||||
@click="startOnlineUpgrade"
|
||||
|
||||
Reference in New Issue
Block a user