2026-04-23 07:00:06 -03:00
|
|
|
{
|
2026-04-30 11:38:16 -03:00
|
|
|
fn default_client_pki_dir() -> PathBuf {
|
|
|
|
|
if let Some(home) = std::env::var_os("HOME") {
|
|
|
|
|
return PathBuf::from(home).join(".config/lesavka/pki");
|
|
|
|
|
}
|
|
|
|
|
std::env::current_dir()
|
|
|
|
|
.unwrap_or_else(|_| PathBuf::from("."))
|
|
|
|
|
.join(".config/lesavka/pki")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn install_client_pki_bundle(bundle: &Path) -> Result<PathBuf, String> {
|
|
|
|
|
let target = default_client_pki_dir();
|
|
|
|
|
let scratch = std::env::temp_dir().join(format!(
|
|
|
|
|
"lesavka-client-pki-{}",
|
|
|
|
|
SystemTime::now()
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_millis()
|
|
|
|
|
));
|
|
|
|
|
std::fs::create_dir_all(&scratch)
|
|
|
|
|
.map_err(|err| format!("could not create extraction folder: {err}"))?;
|
|
|
|
|
let extract = Command::new("tar")
|
|
|
|
|
.arg("-xzf")
|
|
|
|
|
.arg(bundle)
|
|
|
|
|
.arg("-C")
|
|
|
|
|
.arg(&scratch)
|
|
|
|
|
.status()
|
|
|
|
|
.map_err(|err| format!("tar is unavailable: {err}"))?;
|
|
|
|
|
if !extract.success() {
|
|
|
|
|
let _ = std::fs::remove_dir_all(&scratch);
|
|
|
|
|
return Err(format!("could not extract {}", bundle.display()));
|
|
|
|
|
}
|
|
|
|
|
for item in ["ca.crt", "client.crt", "client.key"] {
|
|
|
|
|
if !scratch.join(item).is_file() {
|
|
|
|
|
let _ = std::fs::remove_dir_all(&scratch);
|
|
|
|
|
return Err(format!("bundle is missing {item}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::fs::create_dir_all(&target)
|
|
|
|
|
.map_err(|err| format!("could not create {}: {err}", target.display()))?;
|
|
|
|
|
for item in ["ca.crt", "client.crt", "client.key"] {
|
|
|
|
|
std::fs::copy(scratch.join(item), target.join(item))
|
|
|
|
|
.map_err(|err| format!("could not install {item}: {err}"))?;
|
|
|
|
|
}
|
|
|
|
|
tighten_client_key_permissions(&target.join("client.key"));
|
|
|
|
|
let _ = std::fs::remove_dir_all(&scratch);
|
|
|
|
|
Ok(target)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
fn tighten_client_key_permissions(path: &Path) {
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
if let Ok(mut permissions) = std::fs::metadata(path).map(|metadata| metadata.permissions()) {
|
|
|
|
|
permissions.set_mode(0o600);
|
|
|
|
|
let _ = std::fs::set_permissions(path, permissions);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(unix))]
|
|
|
|
|
fn tighten_client_key_permissions(_path: &Path) {}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let window = window.clone();
|
|
|
|
|
let certs_button = widgets.certs_button.clone();
|
|
|
|
|
certs_button.connect_clicked(move |_| {
|
|
|
|
|
let chooser = gtk::FileChooserNative::new(
|
|
|
|
|
Some("Choose Lesavka Client TLS Bundle"),
|
|
|
|
|
Some(&window),
|
|
|
|
|
gtk::FileChooserAction::Open,
|
|
|
|
|
Some("Install"),
|
|
|
|
|
Some("Cancel"),
|
|
|
|
|
);
|
|
|
|
|
chooser.set_modal(true);
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
chooser.connect_response(move |dialog, response| {
|
|
|
|
|
if response == gtk::ResponseType::Accept {
|
|
|
|
|
if let Some(bundle) = dialog.file().and_then(|file| file.path()) {
|
|
|
|
|
widgets.status_label.set_text(&format!(
|
|
|
|
|
"Installing Lesavka client TLS bundle from {}...",
|
|
|
|
|
bundle.display()
|
|
|
|
|
));
|
|
|
|
|
match install_client_pki_bundle(&bundle) {
|
|
|
|
|
Ok(target) => widgets.status_label.set_text(&format!(
|
|
|
|
|
"Client TLS certs installed at {}. Reconnect the relay to use them.",
|
|
|
|
|
target.display()
|
|
|
|
|
)),
|
|
|
|
|
Err(err) => widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text(&format!("Client TLS bundle install failed: {err}")),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Selected TLS bundle did not provide a filesystem path.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dialog.destroy();
|
|
|
|
|
});
|
|
|
|
|
chooser.show();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 20:52:55 -03:00
|
|
|
{
|
|
|
|
|
let child_proc = Rc::clone(&child_proc);
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let server_entry = server_entry.clone();
|
|
|
|
|
let server_addr_fallback = Rc::clone(&server_addr);
|
|
|
|
|
let clipboard_tx = clipboard_tx.clone();
|
|
|
|
|
widgets.clipboard_button.connect_clicked(move |_| {
|
|
|
|
|
if child_proc.borrow().is_none() {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Start the relay before sending clipboard text.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
let Some(display) = gtk::gdk::Display::default() else {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("No desktop clipboard is available in this session.");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Reading the local clipboard and preparing remote paste...");
|
|
|
|
|
let clipboard = display.clipboard();
|
|
|
|
|
let clipboard_tx = clipboard_tx.clone();
|
|
|
|
|
clipboard.read_text_async(None::<>k::gio::Cancellable>, move |result| match result {
|
|
|
|
|
Ok(Some(text)) => {
|
|
|
|
|
let text = text.trim_end_matches(['\r', '\n']).to_string();
|
|
|
|
|
if text.is_empty() {
|
|
|
|
|
let _ = clipboard_tx
|
|
|
|
|
.send(ClipboardMessage::Finished(Err("clipboard is empty".to_string())));
|
2026-04-23 07:00:06 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let clipboard_tx = clipboard_tx.clone();
|
2026-04-29 20:52:55 -03:00
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
let result = send_clipboard_text_to_remote(&server_addr, &text)
|
|
|
|
|
.map_err(|err| err.to_string());
|
|
|
|
|
let _ = clipboard_tx.send(ClipboardMessage::Finished(result));
|
2026-04-23 07:00:06 -03:00
|
|
|
});
|
2026-04-29 20:52:55 -03:00
|
|
|
}
|
|
|
|
|
Ok(None) => {
|
|
|
|
|
let _ = clipboard_tx
|
|
|
|
|
.send(ClipboardMessage::Finished(Err("clipboard is empty".to_string())));
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
let _ = clipboard_tx.send(ClipboardMessage::Finished(Err(format!(
|
|
|
|
|
"clipboard read failed: {err}"
|
|
|
|
|
))));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-05-18 12:44:23 -03:00
|
|
|
{
|
|
|
|
|
let child_proc = Rc::clone(&child_proc);
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
widgets.wake_combo.connect_changed(move |combo| {
|
|
|
|
|
let id = combo
|
|
|
|
|
.active_id()
|
|
|
|
|
.map(|value| value.to_string())
|
|
|
|
|
.unwrap_or_else(|| "off".to_string());
|
|
|
|
|
let minutes = match id.as_str() {
|
|
|
|
|
"5" => Some(5),
|
|
|
|
|
"10" => Some(10),
|
|
|
|
|
"20" => Some(20),
|
|
|
|
|
"30" => Some(30),
|
|
|
|
|
"60" => Some(60),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
let path = wake_control_path();
|
|
|
|
|
match write_wake_control_request(&path, minutes) {
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
let relay_live = child_proc
|
|
|
|
|
.try_borrow()
|
|
|
|
|
.map(|child| child.is_some())
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
match minutes {
|
|
|
|
|
Some(minutes) if relay_live => widgets.status_label.set_text(&format!(
|
|
|
|
|
"Wake armed: after {minutes}m without relayed HID input, the live relay will send a tiny random mouse nudge."
|
|
|
|
|
)),
|
|
|
|
|
Some(minutes) => widgets.status_label.set_text(&format!(
|
|
|
|
|
"Wake staged: the next relay will nudge after {minutes}m without relayed HID input."
|
|
|
|
|
)),
|
|
|
|
|
None if relay_live => widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Wake disabled for the live relay."),
|
|
|
|
|
None => widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Wake disabled for the next relay launch."),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text(&format!("Wake setting could not be written: {err}")),
|
|
|
|
|
}
|
|
|
|
|
combo.set_tooltip_text(Some(match minutes {
|
|
|
|
|
Some(5) => "Wake is on: nudge the RCT after 5 minutes without relayed keyboard or mouse input.",
|
|
|
|
|
Some(10) => "Wake is on: nudge the RCT after 10 minutes without relayed keyboard or mouse input.",
|
|
|
|
|
Some(20) => "Wake is on: nudge the RCT after 20 minutes without relayed keyboard or mouse input.",
|
|
|
|
|
Some(30) => "Wake is on: nudge the RCT after 30 minutes without relayed keyboard or mouse input.",
|
|
|
|
|
Some(60) => "Wake is on: nudge the RCT after 60 minutes without relayed keyboard or mouse input.",
|
|
|
|
|
_ => "Wake is off: Lesavka will not synthesize mouse movement.",
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-29 20:52:55 -03:00
|
|
|
{
|
|
|
|
|
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(
|
2026-05-09 17:41:39 -03:00
|
|
|
"Recover HID 1/3: reopening keyboard/mouse handles and checking enumeration...",
|
2026-04-29 20:52:55 -03:00
|
|
|
);
|
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
|
std::thread::spawn(move || {
|
2026-04-30 18:38:34 -03:00
|
|
|
let result = recover_usb_soft(&server_addr).map_err(|err| format!("{err:#}"));
|
2026-04-29 20:52:55 -03:00
|
|
|
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(
|
2026-05-09 17:41:39 -03:00
|
|
|
"Recover HID 2/3: handles reopened. Recover HID 3/3: watching chips for stable enumeration.",
|
2026-04-29 20:52:55 -03:00
|
|
|
);
|
|
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
Ok(Err(err)) => {
|
2026-04-23 07:00:06 -03:00
|
|
|
widgets
|
|
|
|
|
.status_label
|
2026-05-09 17:41:39 -03:00
|
|
|
.set_text(&format!("Recover HID failed: {err}"));
|
2026-04-29 20:52:55 -03:00
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
|
|
|
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
|
|
|
|
widgets.status_label.set_text(
|
2026-05-09 17:41:39 -03:00
|
|
|
"Recover HID failed: relay stopped responding before completion.",
|
2026-04-29 20:52:55 -03:00
|
|
|
);
|
|
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
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.uac_recover_button.connect_clicked(move |_| {
|
|
|
|
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
widgets_for_click
|
|
|
|
|
.status_label
|
2026-05-09 17:41:39 -03:00
|
|
|
.set_text("Recover Audio 1/3: retiring the stale upstream audio epoch cleanly...");
|
2026-04-29 20:52:55 -03:00
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
|
std::thread::spawn(move || {
|
2026-04-30 18:38:34 -03:00
|
|
|
let result = recover_uac_soft(&server_addr).map_err(|err| format!("{err:#}"));
|
2026-04-29 20:52:55 -03:00
|
|
|
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(
|
2026-05-09 17:41:39 -03:00
|
|
|
"Recover Audio 2/3: old epoch released. Recover Audio 3/3: bundled media will reconnect without resetting USB or calibration.",
|
2026-04-29 20:52:55 -03:00
|
|
|
);
|
|
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
Ok(Err(err)) => {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
2026-05-09 17:41:39 -03:00
|
|
|
.set_text(&format!("Recover Audio failed: {err}"));
|
2026-04-29 20:52:55 -03:00
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
|
|
|
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
|
|
|
|
widgets.status_label.set_text(
|
2026-05-09 17:41:39 -03:00
|
|
|
"Recover Audio failed: relay stopped responding before completion.",
|
2026-04-29 20:52:55 -03:00
|
|
|
);
|
|
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
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.uvc_recover_button.connect_clicked(move |_| {
|
|
|
|
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
widgets_for_click
|
|
|
|
|
.status_label
|
2026-05-09 17:41:39 -03:00
|
|
|
.set_text("Recover Video 1/3: retiring the webcam spool pipeline safely...");
|
2026-04-29 20:52:55 -03:00
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
|
std::thread::spawn(move || {
|
2026-04-30 18:38:34 -03:00
|
|
|
let result = recover_uvc_soft(&server_addr).map_err(|err| format!("{err:#}"));
|
2026-04-29 20:52:55 -03:00
|
|
|
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(
|
2026-05-09 17:41:39 -03:00
|
|
|
"Recover Video 2/3: webcam sink retired. Recover Video 3/3: client will recreate the UVC spool on reconnect.",
|
2026-04-29 20:52:55 -03:00
|
|
|
);
|
|
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
Ok(Err(err)) => {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
2026-05-09 17:41:39 -03:00
|
|
|
.set_text(&format!("Recover Video failed: {err}"));
|
2026-04-29 20:52:55 -03:00
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
|
|
|
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
|
|
|
|
widgets.status_label.set_text(
|
2026-05-09 17:41:39 -03:00
|
|
|
"Recover Video failed: relay stopped responding before completion.",
|
2026-04-29 20:52:55 -03:00
|
|
|
);
|
|
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 08:16:57 -03:00
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let server_entry = server_entry.clone();
|
|
|
|
|
let server_addr_fallback = Rc::clone(&server_addr);
|
|
|
|
|
let calibration_tx = calibration_tx.clone();
|
|
|
|
|
let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight);
|
|
|
|
|
widgets.calibration_default_button.connect_clicked(move |_| {
|
|
|
|
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Calibration 1/2: restoring saved upstream A/V default...");
|
|
|
|
|
calibration_request_in_flight.set(true);
|
|
|
|
|
request_calibration_command(calibration_tx.clone(), server_addr, |server_addr| {
|
|
|
|
|
restore_default_calibration(server_addr)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let server_entry = server_entry.clone();
|
|
|
|
|
let server_addr_fallback = Rc::clone(&server_addr);
|
|
|
|
|
let calibration_tx = calibration_tx.clone();
|
|
|
|
|
let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight);
|
|
|
|
|
widgets.calibration_factory_button.connect_clicked(move |_| {
|
|
|
|
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Calibration 1/2: restoring factory MJPEG upstream A/V baseline...");
|
|
|
|
|
calibration_request_in_flight.set(true);
|
|
|
|
|
request_calibration_command(calibration_tx.clone(), server_addr, |server_addr| {
|
|
|
|
|
restore_factory_calibration(server_addr)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let server_entry = server_entry.clone();
|
|
|
|
|
let server_addr_fallback = Rc::clone(&server_addr);
|
|
|
|
|
let calibration_tx = calibration_tx.clone();
|
|
|
|
|
let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight);
|
|
|
|
|
widgets.calibration_minus_button.connect_clicked(move |_| {
|
|
|
|
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Calibration 1/2: nudging upstream audio 5 ms earlier...");
|
|
|
|
|
calibration_request_in_flight.set(true);
|
|
|
|
|
request_calibration_command(calibration_tx.clone(), server_addr, |server_addr| {
|
|
|
|
|
nudge_audio_calibration(server_addr, -5_000)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let server_entry = server_entry.clone();
|
|
|
|
|
let server_addr_fallback = Rc::clone(&server_addr);
|
|
|
|
|
let calibration_tx = calibration_tx.clone();
|
|
|
|
|
let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight);
|
|
|
|
|
widgets.calibration_plus_button.connect_clicked(move |_| {
|
|
|
|
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Calibration 1/2: nudging upstream audio 5 ms later...");
|
|
|
|
|
calibration_request_in_flight.set(true);
|
|
|
|
|
request_calibration_command(calibration_tx.clone(), server_addr, |server_addr| {
|
|
|
|
|
nudge_audio_calibration(server_addr, 5_000)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let server_entry = server_entry.clone();
|
|
|
|
|
let server_addr_fallback = Rc::clone(&server_addr);
|
|
|
|
|
let calibration_tx = calibration_tx.clone();
|
|
|
|
|
let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight);
|
|
|
|
|
widgets.calibration_blind_button.connect_clicked(move |_| {
|
|
|
|
|
let Some(sample) = widgets.diagnostics_log.borrow().latest().cloned() else {
|
|
|
|
|
widgets.status_label.set_text(
|
|
|
|
|
"Blind calibration needs a live upstream camera and microphone sample first.",
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let camera = sample.upstream_camera;
|
|
|
|
|
let microphone = sample.upstream_microphone;
|
|
|
|
|
if !camera.connected || !microphone.connected {
|
|
|
|
|
widgets.status_label.set_text(
|
|
|
|
|
"Blind calibration refused: upstream camera and microphone are not both live.",
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let delivery_delta_ms =
|
|
|
|
|
microphone.latest_delivery_age_ms - camera.latest_delivery_age_ms;
|
|
|
|
|
let delivery_skew_ms = delivery_delta_ms.abs();
|
|
|
|
|
let enqueue_skew_ms =
|
|
|
|
|
(microphone.latest_enqueue_age_ms - camera.latest_enqueue_age_ms).abs();
|
|
|
|
|
if camera.queue_depth >= 28 || microphone.queue_depth >= 14 || delivery_skew_ms > 80.0
|
|
|
|
|
{
|
|
|
|
|
widgets.status_label.set_text(
|
|
|
|
|
"Blind calibration refused: live queues are too backed up to make a safe timing estimate. Use the test rig or fix queue churn first.",
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let audio_delta_us = (-(delivery_delta_ms as f64) * 500.0)
|
|
|
|
|
.round()
|
|
|
|
|
.clamp(-10_000.0, 10_000.0) as i64;
|
|
|
|
|
let note = format!(
|
|
|
|
|
"blind estimate from live telemetry: mic-camera delivery delta {delivery_delta_ms:+.1}ms, enqueue skew {enqueue_skew_ms:.1}ms; applying half-step audio delta {:+.1}ms",
|
|
|
|
|
audio_delta_us as f64 / 1000.0
|
|
|
|
|
);
|
|
|
|
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Calibration 1/2: applying blind upstream A/V estimate...");
|
|
|
|
|
calibration_request_in_flight.set(true);
|
|
|
|
|
request_calibration_command(calibration_tx.clone(), server_addr, move |server_addr| {
|
|
|
|
|
blind_calibration_estimate(
|
|
|
|
|
server_addr,
|
|
|
|
|
audio_delta_us,
|
|
|
|
|
delivery_skew_ms,
|
|
|
|
|
enqueue_skew_ms,
|
|
|
|
|
¬e,
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
widgets.calibration_rig_button.connect_clicked(move |_| {
|
|
|
|
|
widgets.status_label.set_text(
|
2026-05-01 19:16:40 -03:00
|
|
|
"Rig calibration wizard is queued for lab tooling; for now the mirrored Tethys sync probe remains the measured-default path.",
|
2026-04-30 08:16:57 -03:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 20:52:55 -03:00
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
widgets.diagnostics_copy_button.connect_clicked(move |_| {
|
|
|
|
|
if let Err(err) = copy_plain_text(&widgets.diagnostics_rendered_text.borrow()) {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text(&format!("Could not copy the diagnostics report: {err}"));
|
|
|
|
|
} else {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Diagnostics report copied to the local clipboard.");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let app = app.clone();
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let diagnostics_popout = Rc::clone(&diagnostics_popout);
|
|
|
|
|
widgets.diagnostics_popout_button.connect_clicked(move |_| {
|
|
|
|
|
open_diagnostics_popout(
|
|
|
|
|
&app,
|
|
|
|
|
&diagnostics_popout,
|
|
|
|
|
&widgets.diagnostics_popout_label,
|
|
|
|
|
&widgets.diagnostics_popout_scroll,
|
|
|
|
|
&widgets.diagnostics_rendered_text,
|
|
|
|
|
);
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Diagnostics report moved into its own window.");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
widgets.console_level_combo.connect_changed(move |combo| {
|
|
|
|
|
let level = combo
|
|
|
|
|
.active_id()
|
|
|
|
|
.as_deref()
|
|
|
|
|
.and_then(ConsoleLogLevel::from_id)
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
*widgets.session_log_level.borrow_mut() = level;
|
|
|
|
|
widgets.status_label.set_text(&format!(
|
|
|
|
|
"Console now shows {} relay logs and higher.",
|
|
|
|
|
level.label()
|
|
|
|
|
));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
widgets.console_copy_button.connect_clicked(move |_| {
|
|
|
|
|
if let Err(err) = copy_session_log(&widgets.session_log_buffer) {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text(&format!("Could not copy the session log: {err}"));
|
|
|
|
|
} else {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Session log copied to the local clipboard.");
|
2026-04-23 07:00:06 -03:00
|
|
|
}
|
2026-04-29 20:52:55 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let app = app.clone();
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let log_popout = Rc::clone(&log_popout);
|
|
|
|
|
widgets.console_popout_button.connect_clicked(move |_| {
|
|
|
|
|
open_session_log_popout(&app, &log_popout, &widgets.session_log_buffer);
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Session log moved into its own window.");
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-23 07:00:06 -03:00
|
|
|
}
|