lesavka: add USB recovery controls
This commit is contained in:
parent
50b5f54d27
commit
c6f9001323
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.11.22"
|
version = "0.11.23"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -10,6 +10,7 @@ enum CommandKind {
|
|||||||
Auto,
|
Auto,
|
||||||
On,
|
On,
|
||||||
Off,
|
Off,
|
||||||
|
ResetUsb,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandKind {
|
impl CommandKind {
|
||||||
@ -19,6 +20,7 @@ impl CommandKind {
|
|||||||
"auto" => Some(Self::Auto),
|
"auto" => Some(Self::Auto),
|
||||||
"on" | "force-on" => Some(Self::On),
|
"on" | "force-on" => Some(Self::On),
|
||||||
"off" | "force-off" => Some(Self::Off),
|
"off" | "force-off" => Some(Self::Off),
|
||||||
|
"reset-usb" | "recover-usb" => Some(Self::ResetUsb),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -30,7 +32,7 @@ struct Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> &'static str {
|
fn usage() -> &'static str {
|
||||||
"Usage: lesavka-relayctl [--server http://HOST:50051] <status|auto|on|off>"
|
"Usage: lesavka-relayctl [--server http://HOST:50051] <status|auto|on|off|reset-usb>"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_args() -> Result<Config> {
|
fn parse_args() -> Result<Config> {
|
||||||
@ -122,6 +124,15 @@ async fn main() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
.context("forcing capture power off")?
|
.context("forcing capture power off")?
|
||||||
.into_inner(),
|
.into_inner(),
|
||||||
|
CommandKind::ResetUsb => {
|
||||||
|
let reply = client
|
||||||
|
.reset_usb(Request::new(Empty {}))
|
||||||
|
.await
|
||||||
|
.context("forcing USB gadget recovery")?
|
||||||
|
.into_inner();
|
||||||
|
println!("ok={}", reply.ok);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
print_state(reply);
|
print_state(reply);
|
||||||
|
|||||||
@ -590,6 +590,8 @@ fn parse_caps_fraction_numerator(caps: &str, needle: &str) -> Option<u32> {
|
|||||||
|
|
||||||
fn recommendations_for(state: &LauncherState, log: &DiagnosticsLog) -> Vec<String> {
|
fn recommendations_for(state: &LauncherState, log: &DiagnosticsLog) -> Vec<String> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
let hardware_decode_active = log.latest().is_some_and(sample_uses_hardware_decode);
|
||||||
|
let software_decode_active = log.latest().is_some_and(sample_uses_software_decode);
|
||||||
if !state.server_available {
|
if !state.server_available {
|
||||||
items.push(
|
items.push(
|
||||||
"The server is not reachable from this launcher yet, so stream-quality results would not be meaningful."
|
"The server is not reachable from this launcher yet, so stream-quality results would not be meaningful."
|
||||||
@ -618,10 +620,13 @@ fn recommendations_for(state: &LauncherState, log: &DiagnosticsLog) -> Vec<Strin
|
|||||||
if sample.left_present_fps + 1.0 < sample.left_receive_fps
|
if sample.left_present_fps + 1.0 < sample.left_receive_fps
|
||||||
|| sample.right_present_fps + 1.0 < sample.right_receive_fps
|
|| sample.right_present_fps + 1.0 < sample.right_receive_fps
|
||||||
{
|
{
|
||||||
items.push(
|
items.push(if hardware_decode_active {
|
||||||
|
"The client is receiving more frames than it is presenting. That points at local decode/render pressure, so prefer lighter breakout sizes or a cheaper source mode before adding bitrate."
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
"The client is receiving more frames than it is presenting. That points at local decode/render pressure, so prefer lighter breakout sizes or hardware decode."
|
"The client is receiving more frames than it is presenting. That points at local decode/render pressure, so prefer lighter breakout sizes or hardware decode."
|
||||||
.to_string(),
|
.to_string()
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
if (sample.left_present_gap_peak_ms - sample.left_packet_gap_peak_ms) > 40.0
|
if (sample.left_present_gap_peak_ms - sample.left_packet_gap_peak_ms) > 40.0
|
||||||
|| (sample.right_present_gap_peak_ms - sample.right_packet_gap_peak_ms) > 40.0
|
|| (sample.right_present_gap_peak_ms - sample.right_packet_gap_peak_ms) > 40.0
|
||||||
@ -666,10 +671,13 @@ fn recommendations_for(state: &LauncherState, log: &DiagnosticsLog) -> Vec<Strin
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if sample.client_process_cpu_pct >= 85.0 {
|
if sample.client_process_cpu_pct >= 85.0 {
|
||||||
items.push(
|
items.push(if hardware_decode_active {
|
||||||
|
"Client process CPU is high even though hardware decode is active. If motion still looks rough, favor lighter breakout layouts or a cheaper source mode before adding more bitrate."
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
"Client process CPU is high. If motion still looks rough, favor lighter breakout layouts or a hardware decoder before adding more bitrate."
|
"Client process CPU is high. If motion still looks rough, favor lighter breakout layouts or a hardware decoder before adding more bitrate."
|
||||||
.to_string(),
|
.to_string()
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
if sample.server_process_cpu_pct >= 85.0 {
|
if sample.server_process_cpu_pct >= 85.0 {
|
||||||
items.push(
|
items.push(
|
||||||
@ -710,6 +718,7 @@ fn recommendations_for(state: &LauncherState, log: &DiagnosticsLog) -> Vec<Strin
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(sample) = log.latest()
|
if let Some(sample) = log.latest()
|
||||||
|
&& software_decode_active
|
||||||
&& ((sample.left_decoder_label.contains("avdec")
|
&& ((sample.left_decoder_label.contains("avdec")
|
||||||
&& sample.left_present_fps + 1.0 < sample.left_receive_fps)
|
&& sample.left_present_fps + 1.0 < sample.left_receive_fps)
|
||||||
|| (sample.right_decoder_label.contains("avdec")
|
|| (sample.right_decoder_label.contains("avdec")
|
||||||
@ -742,6 +751,26 @@ fn recommendations_for(state: &LauncherState, log: &DiagnosticsLog) -> Vec<Strin
|
|||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sample_uses_hardware_decode(sample: &PerformanceSample) -> bool {
|
||||||
|
decoder_label_is_hardware(&sample.left_decoder_label)
|
||||||
|
|| decoder_label_is_hardware(&sample.right_decoder_label)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_uses_software_decode(sample: &PerformanceSample) -> bool {
|
||||||
|
sample.left_decoder_label.contains("avdec") || sample.right_decoder_label.contains("avdec")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decoder_label_is_hardware(label: &str) -> bool {
|
||||||
|
let lower = label.to_ascii_lowercase();
|
||||||
|
lower.contains("nvh264dec")
|
||||||
|
|| lower.contains("nvdec")
|
||||||
|
|| lower.contains("vah264dec")
|
||||||
|
|| lower.contains("vaapih264dec")
|
||||||
|
|| lower.contains("v4l2slh264dec")
|
||||||
|
|| lower.contains("d3d11")
|
||||||
|
|| lower.contains("vtdec")
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -959,4 +988,30 @@ mod tests {
|
|||||||
"1080p | 1920x1080 | 60 fps | bitrate est ~18000 kbit"
|
"1080p | 1920x1080 | 60 fps | bitrate est ~18000 kbit"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recommendations_do_not_suggest_hardware_decode_when_nvdec_is_active() {
|
||||||
|
let mut log = DiagnosticsLog::new(1);
|
||||||
|
let mut sample = sample(1);
|
||||||
|
sample.client_process_cpu_pct = 96.0;
|
||||||
|
sample.left_receive_fps = 40.0;
|
||||||
|
sample.left_present_fps = 30.0;
|
||||||
|
sample.left_decoder_label = "nvh264dec".to_string();
|
||||||
|
sample.right_decoder_label = "nvh264dec".to_string();
|
||||||
|
log.record(sample);
|
||||||
|
|
||||||
|
let items = recommendations_for(&LauncherState::new(), &log);
|
||||||
|
let joined = items.join("\n");
|
||||||
|
assert!(!joined.contains("hardware decoder before adding more bitrate"));
|
||||||
|
assert!(!joined.contains("lighter breakout sizes or hardware decode"));
|
||||||
|
assert!(joined.contains("cheaper source mode"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hardware_decoder_detection_recognizes_nvdec_labels() {
|
||||||
|
let mut sample = sample(1);
|
||||||
|
sample.left_decoder_label = "nvh264dec".to_string();
|
||||||
|
assert!(sample_uses_hardware_decode(&sample));
|
||||||
|
assert!(!sample_uses_software_decode(&sample));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,9 +36,25 @@ pub fn set_capture_power_mode(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_runtime<F>(future: F) -> Result<CapturePowerStatus>
|
pub fn reset_usb_gadget(server_addr: &str) -> Result<()> {
|
||||||
|
with_runtime(async move {
|
||||||
|
let mut client = connect(server_addr).await?;
|
||||||
|
let reply = client
|
||||||
|
.reset_usb(Request::new(Empty {}))
|
||||||
|
.await
|
||||||
|
.context("requesting USB gadget reset")?
|
||||||
|
.into_inner();
|
||||||
|
if reply.ok {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("relay reported USB reset failure");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_runtime<F, T>(future: F) -> Result<T>
|
||||||
where
|
where
|
||||||
F: std::future::Future<Output = Result<CapturePowerStatus>>,
|
F: std::future::Future<Output = Result<T>>,
|
||||||
{
|
{
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use {
|
|||||||
super::diagnostics::{PerformanceSample, quality_probe_command},
|
super::diagnostics::{PerformanceSample, quality_probe_command},
|
||||||
super::launcher_clipboard_control_path,
|
super::launcher_clipboard_control_path,
|
||||||
super::launcher_focus_signal_path,
|
super::launcher_focus_signal_path,
|
||||||
super::power::{fetch_capture_power, set_capture_power_mode},
|
super::power::{fetch_capture_power, reset_usb_gadget, set_capture_power_mode},
|
||||||
super::state::{
|
super::state::{
|
||||||
BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface,
|
BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface,
|
||||||
FeedSourcePreset, InputRouting, LauncherState,
|
FeedSourcePreset, InputRouting, LauncherState,
|
||||||
@ -1296,6 +1296,49 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let widgets = widgets.clone();
|
||||||
|
let server_entry = server_entry.clone();
|
||||||
|
let server_addr_fallback = Rc::clone(&server_addr);
|
||||||
|
let widgets_for_click = widgets.clone();
|
||||||
|
widgets.usb_recover_button.connect_clicked(move |_| {
|
||||||
|
let server_addr =
|
||||||
|
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
||||||
|
widgets_for_click.status_label.set_text(
|
||||||
|
"Requesting a forced USB gadget re-enumeration on the relay host...",
|
||||||
|
);
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result = reset_usb_gadget(&server_addr).map_err(|err| err.to_string());
|
||||||
|
let _ = tx.send(result);
|
||||||
|
});
|
||||||
|
let widgets = widgets_for_click.clone();
|
||||||
|
glib::timeout_add_local(Duration::from_millis(100), move || {
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(Ok(())) => {
|
||||||
|
widgets.status_label.set_text(
|
||||||
|
"USB gadget recovery requested. Give the host a few seconds to re-enumerate keyboard, mouse, webcam, and audio.",
|
||||||
|
);
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
widgets
|
||||||
|
.status_label
|
||||||
|
.set_text(&format!("USB gadget recovery failed: {err}"));
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
}
|
||||||
|
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
||||||
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||||
|
widgets.status_label.set_text(
|
||||||
|
"USB gadget recovery ended unexpectedly before the relay answered.",
|
||||||
|
);
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
widgets.diagnostics_copy_button.connect_clicked(move |_| {
|
widgets.diagnostics_copy_button.connect_clicked(move |_| {
|
||||||
|
|||||||
@ -53,6 +53,7 @@ pub struct LauncherWidgets {
|
|||||||
pub status_label: gtk::Label,
|
pub status_label: gtk::Label,
|
||||||
pub diagnostics_log: Rc<RefCell<DiagnosticsLog>>,
|
pub diagnostics_log: Rc<RefCell<DiagnosticsLog>>,
|
||||||
pub diagnostics_buffer: gtk::TextBuffer,
|
pub diagnostics_buffer: gtk::TextBuffer,
|
||||||
|
pub diagnostics_scroll: gtk::ScrolledWindow,
|
||||||
pub session_log_buffer: gtk::TextBuffer,
|
pub session_log_buffer: gtk::TextBuffer,
|
||||||
pub session_log_view: gtk::TextView,
|
pub session_log_view: gtk::TextView,
|
||||||
pub summary: SummaryWidgets,
|
pub summary: SummaryWidgets,
|
||||||
@ -67,6 +68,7 @@ pub struct LauncherWidgets {
|
|||||||
pub input_toggle_button: gtk::Button,
|
pub input_toggle_button: gtk::Button,
|
||||||
pub clipboard_button: gtk::Button,
|
pub clipboard_button: gtk::Button,
|
||||||
pub probe_button: gtk::Button,
|
pub probe_button: gtk::Button,
|
||||||
|
pub usb_recover_button: gtk::Button,
|
||||||
pub swap_key_button: gtk::Button,
|
pub swap_key_button: gtk::Button,
|
||||||
pub camera_test_button: gtk::Button,
|
pub camera_test_button: gtk::Button,
|
||||||
pub microphone_test_button: gtk::Button,
|
pub microphone_test_button: gtk::Button,
|
||||||
@ -386,8 +388,15 @@ pub fn build_launcher_view(
|
|||||||
probe_button.set_tooltip_text(Some(
|
probe_button.set_tooltip_text(Some(
|
||||||
"Copy the hygiene/quality probe command into the local clipboard.",
|
"Copy the hygiene/quality probe command into the local clipboard.",
|
||||||
));
|
));
|
||||||
|
let usb_recover_button = gtk::Button::with_label("Recover USB");
|
||||||
|
usb_recover_button.set_hexpand(true);
|
||||||
|
stabilize_button(&usb_recover_button, 108);
|
||||||
|
usb_recover_button.set_tooltip_text(Some(
|
||||||
|
"Force the remote USB gadget to re-enumerate when keyboard, mouse, webcam, or audio stop showing up on the host.",
|
||||||
|
));
|
||||||
live_actions_row.append(&clipboard_button);
|
live_actions_row.append(&clipboard_button);
|
||||||
live_actions_row.append(&probe_button);
|
live_actions_row.append(&probe_button);
|
||||||
|
live_actions_row.append(&usb_recover_button);
|
||||||
connection_body.append(&live_actions_row);
|
connection_body.append(&live_actions_row);
|
||||||
|
|
||||||
connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));
|
connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));
|
||||||
@ -616,6 +625,7 @@ pub fn build_launcher_view(
|
|||||||
status_label: status_label.clone(),
|
status_label: status_label.clone(),
|
||||||
diagnostics_log: diagnostics_log.clone(),
|
diagnostics_log: diagnostics_log.clone(),
|
||||||
diagnostics_buffer: diagnostics_buffer.clone(),
|
diagnostics_buffer: diagnostics_buffer.clone(),
|
||||||
|
diagnostics_scroll: diagnostics_scroll.clone(),
|
||||||
session_log_buffer: session_log_buffer.clone(),
|
session_log_buffer: session_log_buffer.clone(),
|
||||||
session_log_view: session_log_view.clone(),
|
session_log_view: session_log_view.clone(),
|
||||||
summary: SummaryWidgets {
|
summary: SummaryWidgets {
|
||||||
@ -638,6 +648,7 @@ pub fn build_launcher_view(
|
|||||||
input_toggle_button: input_toggle_button.clone(),
|
input_toggle_button: input_toggle_button.clone(),
|
||||||
clipboard_button: clipboard_button.clone(),
|
clipboard_button: clipboard_button.clone(),
|
||||||
probe_button: probe_button.clone(),
|
probe_button: probe_button.clone(),
|
||||||
|
usb_recover_button: usb_recover_button.clone(),
|
||||||
swap_key_button: swap_key_button.clone(),
|
swap_key_button: swap_key_button.clone(),
|
||||||
camera_test_button: camera_test_button.clone(),
|
camera_test_button: camera_test_button.clone(),
|
||||||
microphone_test_button: microphone_test_button.clone(),
|
microphone_test_button: microphone_test_button.clone(),
|
||||||
|
|||||||
@ -86,6 +86,9 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
}));
|
}));
|
||||||
widgets.clipboard_button.set_sensitive(relay_live);
|
widgets.clipboard_button.set_sensitive(relay_live);
|
||||||
widgets.probe_button.set_sensitive(true);
|
widgets.probe_button.set_sensitive(true);
|
||||||
|
widgets
|
||||||
|
.usb_recover_button
|
||||||
|
.set_sensitive(state.server_available);
|
||||||
widgets.input_toggle_button.set_label("Change Routing");
|
widgets.input_toggle_button.set_label("Change Routing");
|
||||||
widgets
|
widgets
|
||||||
.input_toggle_button
|
.input_toggle_button
|
||||||
@ -944,6 +947,16 @@ pub fn refresh_diagnostics_report(
|
|||||||
state: &LauncherState,
|
state: &LauncherState,
|
||||||
child_running: bool,
|
child_running: bool,
|
||||||
) {
|
) {
|
||||||
|
let diagnostics_adjustment = widgets.diagnostics_scroll.vadjustment();
|
||||||
|
let previous_max =
|
||||||
|
(diagnostics_adjustment.upper() - diagnostics_adjustment.page_size()).max(0.0);
|
||||||
|
let was_at_bottom =
|
||||||
|
previous_max <= 0.0 || diagnostics_adjustment.value() >= (previous_max - 4.0);
|
||||||
|
let previous_ratio = if previous_max > 0.0 {
|
||||||
|
(diagnostics_adjustment.value() / previous_max).clamp(0.0, 1.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
let mut snapshot = SnapshotReport::from_state(
|
let mut snapshot = SnapshotReport::from_state(
|
||||||
state,
|
state,
|
||||||
&widgets.diagnostics_log.borrow(),
|
&widgets.diagnostics_log.borrow(),
|
||||||
@ -958,6 +971,15 @@ pub fn refresh_diagnostics_report(
|
|||||||
widgets
|
widgets
|
||||||
.diagnostics_buffer
|
.diagnostics_buffer
|
||||||
.set_text(&snapshot.to_pretty_text());
|
.set_text(&snapshot.to_pretty_text());
|
||||||
|
glib::idle_add_local_once(move || {
|
||||||
|
let max = (diagnostics_adjustment.upper() - diagnostics_adjustment.page_size()).max(0.0);
|
||||||
|
let target = if was_at_bottom {
|
||||||
|
max
|
||||||
|
} else {
|
||||||
|
(previous_ratio * max).clamp(0.0, max)
|
||||||
|
};
|
||||||
|
diagnostics_adjustment.set_value(target);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_session_log_popout(
|
pub fn open_session_log_popout(
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.11.22"
|
version = "0.11.23"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn banner_includes_version() {
|
fn banner_includes_version() {
|
||||||
assert_eq!(banner("0.11.22"), "lesavka-common CLI (v0.11.22)");
|
assert_eq!(banner("0.11.23"), "lesavka-common CLI (v0.11.23)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.11.22"
|
version = "0.11.23"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -122,10 +122,19 @@ impl UsbGadget {
|
|||||||
/// Hard-reset the gadget → identical to a physical cable re-plug
|
/// Hard-reset the gadget → identical to a physical cable re-plug
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
pub fn cycle(&self) -> Result<()> {
|
pub fn cycle(&self) -> Result<()> {
|
||||||
|
self.cycle_internal(env::var("LESAVKA_GADGET_FORCE_CYCLE").is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
pub fn cycle_forced(&self) -> Result<()> {
|
||||||
|
self.cycle_internal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn cycle_internal(&self, force_cycle: bool) -> Result<()> {
|
||||||
let ctrl = Self::find_controller().or_else(|_| {
|
let ctrl = Self::find_controller().or_else(|_| {
|
||||||
Self::probe_platform_udc()?.ok_or_else(|| anyhow::anyhow!("no UDC present"))
|
Self::probe_platform_udc()?.ok_or_else(|| anyhow::anyhow!("no UDC present"))
|
||||||
})?;
|
})?;
|
||||||
let force_cycle = env::var("LESAVKA_GADGET_FORCE_CYCLE").is_ok();
|
|
||||||
|
|
||||||
if !force_cycle {
|
if !force_cycle {
|
||||||
match Self::state(&ctrl) {
|
match Self::state(&ctrl) {
|
||||||
@ -153,11 +162,20 @@ impl UsbGadget {
|
|||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
pub fn cycle(&self) -> Result<()> {
|
pub fn cycle(&self) -> Result<()> {
|
||||||
|
self.cycle_internal(env::var("LESAVKA_GADGET_FORCE_CYCLE").is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
pub fn cycle_forced(&self) -> Result<()> {
|
||||||
|
self.cycle_internal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn cycle_internal(&self, force_cycle: bool) -> Result<()> {
|
||||||
/* 0 - ensure we *know* the controller even after a previous crash */
|
/* 0 - ensure we *know* the controller even after a previous crash */
|
||||||
let ctrl = Self::find_controller().or_else(|_| {
|
let ctrl = Self::find_controller().or_else(|_| {
|
||||||
Self::probe_platform_udc()?.ok_or_else(|| anyhow::anyhow!("no UDC present"))
|
Self::probe_platform_udc()?.ok_or_else(|| anyhow::anyhow!("no UDC present"))
|
||||||
})?;
|
})?;
|
||||||
let force_cycle = env::var("LESAVKA_GADGET_FORCE_CYCLE").is_ok();
|
|
||||||
match Self::state(&ctrl) {
|
match Self::state(&ctrl) {
|
||||||
Ok(state)
|
Ok(state)
|
||||||
if !force_cycle
|
if !force_cycle
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
// server/src/main.rs
|
// server/src/main.rs
|
||||||
#[allow(clippy::useless_attribute)]
|
#[allow(clippy::useless_attribute)]
|
||||||
#[forbid(unsafe_code)]
|
#[forbid(unsafe_code)]
|
||||||
|
use anyhow::Context;
|
||||||
use futures_util::{Stream, StreamExt};
|
use futures_util::{Stream, StreamExt};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use std::{backtrace::Backtrace, panic, pin::Pin, sync::Arc, time::Duration};
|
use std::{backtrace::Backtrace, panic, pin::Pin, sync::Arc, time::Duration};
|
||||||
use tokio::sync::{Mutex, broadcast};
|
use tokio::sync::{Mutex, broadcast};
|
||||||
@ -321,13 +323,18 @@ impl Handler {
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
info!("🔴 explicit ResetUsb() called");
|
info!("🔴 explicit ResetUsb() called");
|
||||||
|
|
||||||
match self.gadget.cycle() {
|
match self.gadget.cycle_forced() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
if let Err(e) = self.reopen_hid().await {
|
if let Err(e) = self.reopen_hid().await {
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
error!("💥 reopen HID failed: {e:#}");
|
error!("💥 reopen HID failed: {e:#}");
|
||||||
return Err(Status::internal(e.to_string()));
|
return Err(Status::internal(e.to_string()));
|
||||||
}
|
}
|
||||||
|
if let Err(e) = restart_uvc_helper() {
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
error!("💥 restart UVC helper failed: {e:#}");
|
||||||
|
return Err(Status::internal(e.to_string()));
|
||||||
|
}
|
||||||
Ok(Response::new(ResetUsbReply { ok: true }))
|
Ok(Response::new(ResetUsbReply { ok: true }))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -369,6 +376,37 @@ impl Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn restart_uvc_helper() -> anyhow::Result<()> {
|
||||||
|
for args in [
|
||||||
|
["reset-failed", "lesavka-uvc.service"].as_slice(),
|
||||||
|
["restart", "lesavka-uvc.service"].as_slice(),
|
||||||
|
] {
|
||||||
|
let output = Command::new("systemctl")
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.with_context(|| format!("running systemctl {}", args.join(" ")))?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
anyhow::bail!(
|
||||||
|
"systemctl {} failed: {}{}",
|
||||||
|
args.join(" "),
|
||||||
|
if stderr.is_empty() {
|
||||||
|
stdout.as_str()
|
||||||
|
} else {
|
||||||
|
stderr.as_str()
|
||||||
|
},
|
||||||
|
if stderr.is_empty() || stdout.is_empty() || stderr == stdout {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
" / also see stdout"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl EyeHub {
|
impl EyeHub {
|
||||||
fn spawn<S>(mut stream: S, lease: lesavka_server::capture_power::CapturePowerLease) -> Arc<Self>
|
fn spawn<S>(mut stream: S, lease: lesavka_server::capture_power::CapturePowerLease) -> Arc<Self>
|
||||||
where
|
where
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user