lesavka/client/src/launcher/ui/control_requests.rs

268 lines
8.8 KiB
Rust
Raw Normal View History

fn network_spread_ms(samples: &VecDeque<(Instant, f32)>) -> f32 {
if samples.len() < 2 {
return 0.0;
}
let mut values = samples.iter().map(|(_, value)| *value).collect::<Vec<_>>();
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = values[values.len() / 2];
let mut deviations = values
.into_iter()
.map(|value| (value - median).abs())
.collect::<Vec<_>>();
deviations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
deviations[deviations.len() / 2]
}
#[cfg(not(coverage))]
/// Apply a remote-audio gain slider update without unwinding through GTK callbacks.
fn apply_audio_gain_change(
scale: &gtk::Scale,
state: &Rc<RefCell<LauncherState>>,
widgets: &super::ui_components::LauncherWidgets,
child_proc: &Rc<RefCell<Option<RelayChild>>>,
) -> bool {
let percent = scale
.value()
.round()
.clamp(0.0, MAX_AUDIO_GAIN_PERCENT as f64) as u32;
let label = {
let Ok(mut state) = state.try_borrow_mut() else {
return false;
};
if state.audio_gain_percent == percent {
widgets.audio_gain_value.set_text(&state.audio_gain_label());
return true;
}
state.set_audio_gain_percent(percent);
state.audio_gain_label()
};
widgets.audio_gain_value.set_text(&label);
let relay_live = child_proc
.try_borrow()
.map(|child| child.is_some())
.unwrap_or(false);
if relay_live {
let path = audio_gain_control_path();
match write_audio_gain_request(&path, percent) {
Ok(()) => widgets
.status_label
.set_text(&format!("Remote audio gain set to {label}.")),
Err(err) => widgets.status_label.set_text(&format!(
"Remote audio gain set to {label} for the next relay launch, but live gain control could not be written: {err}"
)),
}
} else {
widgets.status_label.set_text(&format!(
"Remote audio gain set to {label} for the next relay launch."
));
}
true
}
#[cfg(not(coverage))]
/// Apply a microphone uplink gain slider update without unwinding through GTK callbacks.
fn apply_mic_gain_change(
scale: &gtk::Scale,
state: &Rc<RefCell<LauncherState>>,
widgets: &super::ui_components::LauncherWidgets,
child_proc: &Rc<RefCell<Option<RelayChild>>>,
) -> bool {
let percent = scale
.value()
.round()
.clamp(0.0, MAX_MIC_GAIN_PERCENT as f64) as u32;
let label = {
let Ok(mut state) = state.try_borrow_mut() else {
return false;
};
if state.mic_gain_percent == percent {
widgets.mic_gain_value.set_text(&state.mic_gain_label());
return true;
}
state.set_mic_gain_percent(percent);
state.mic_gain_label()
};
widgets.mic_gain_value.set_text(&label);
let relay_live = child_proc
.try_borrow()
.map(|child| child.is_some())
.unwrap_or(false);
if relay_live {
let path = mic_gain_control_path();
match write_mic_gain_request(&path, percent) {
Ok(()) => widgets.status_label.set_text(&format!("Mic gain set to {label}.")),
Err(err) => widgets.status_label.set_text(&format!(
"Mic gain set to {label} for the next relay launch, but live gain control could not be written: {err}"
)),
}
} else {
widgets.status_label.set_text(&format!(
"Mic gain set to {label} for the next relay launch."
));
}
true
}
2026-04-30 15:04:00 -03:00
#[cfg(not(coverage))]
/// Apply a live media soft-pause/resume request without restarting USB gadget functions.
fn apply_media_control_change(
state_snapshot: &LauncherState,
widgets: &super::ui_components::LauncherWidgets,
child_proc: &Rc<RefCell<Option<RelayChild>>>,
feed_label: &str,
enabled: bool,
) {
let relay_live = child_proc
.try_borrow()
.map(|child| child.is_some())
.unwrap_or(false);
let action = if enabled { "resumed" } else { "soft-paused" };
if relay_live {
let path = media_control_path();
match write_media_control_request(&path, state_snapshot) {
Ok(()) => widgets
.status_label
.set_text(&format!("{feed_label} {action} for the live relay.")),
Err(err) => widgets.status_label.set_text(&format!(
"{feed_label} will be {action} on the next relay launch, but the live soft-pause control could not be written: {err}"
)),
}
} else {
widgets.status_label.set_text(&format!(
"{feed_label} will start {} on the next relay launch.",
if enabled { "enabled" } else { "paused" }
));
}
}
#[cfg(not(coverage))]
2026-04-30 08:16:57 -03:00
/// Refresh relay capture-power state in the background so GTK stays responsive.
fn request_capture_power_refresh(
power_tx: std::sync::mpsc::Sender<PowerMessage>,
server_addr: String,
delay: Duration,
) {
std::thread::spawn(move || {
if !delay.is_zero() {
std::thread::sleep(delay);
}
let result = fetch_capture_power(&server_addr).map_err(|err| err.to_string());
let _ = power_tx.send(PowerMessage::Refresh(result));
});
}
#[cfg(not(coverage))]
fn request_capture_power_command(
power_tx: std::sync::mpsc::Sender<PowerMessage>,
server_addr: String,
command: CapturePowerCommand,
) {
std::thread::spawn(move || {
let result = set_capture_power_mode(&server_addr, command).map_err(|err| err.to_string());
let _ = power_tx.send(PowerMessage::Command(result));
});
}
#[cfg(not(coverage))]
2026-04-30 08:16:57 -03:00
/// Refresh upstream calibration state in the background so the UI can poll safely.
fn request_calibration_refresh(
calibration_tx: std::sync::mpsc::Sender<CalibrationMessage>,
server_addr: String,
delay: Duration,
) {
std::thread::spawn(move || {
if !delay.is_zero() {
std::thread::sleep(delay);
}
let result = fetch_calibration(&server_addr).map_err(|err| err.to_string());
let _ = calibration_tx.send(CalibrationMessage::Refresh(result));
});
}
#[cfg(not(coverage))]
/// Refresh authoritative upstream sync planner state in the background.
fn request_upstream_sync_refresh(
upstream_sync_tx: std::sync::mpsc::Sender<UpstreamSyncMessage>,
server_addr: String,
delay: Duration,
) {
std::thread::spawn(move || {
if !delay.is_zero() {
std::thread::sleep(delay);
}
let result = fetch_upstream_sync(&server_addr).map_err(|err| err.to_string());
let _ = upstream_sync_tx.send(UpstreamSyncMessage::Refresh(result));
});
}
2026-04-30 08:16:57 -03:00
#[cfg(not(coverage))]
fn request_calibration_command<F>(
calibration_tx: std::sync::mpsc::Sender<CalibrationMessage>,
server_addr: String,
action: F,
) where
F: FnOnce(&str) -> anyhow::Result<CalibrationStatus> + Send + 'static,
{
std::thread::spawn(move || {
let result = action(&server_addr).map_err(|err| err.to_string());
let _ = calibration_tx.send(CalibrationMessage::Command(result));
});
}
#[cfg(not(coverage))]
/// Probe server capabilities on a short-lived runtime without blocking the UI thread.
fn request_handshake_caps(
caps_tx: std::sync::mpsc::Sender<CapsMessage>,
server_addr: String,
delay: Duration,
) {
std::thread::spawn(move || {
if !delay.is_zero() {
std::thread::sleep(delay);
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
let probe = match runtime {
Ok(runtime) => runtime.block_on(probe(&server_addr)),
Err(_) => HandshakeProbe::default(),
};
let _ = caps_tx.send(CapsMessage::Refresh(probe));
});
}
#[cfg(not(coverage))]
fn unavailable_capture_power(detail: String) -> CapturePowerStatus {
CapturePowerStatus {
available: false,
enabled: false,
unit: "relay.service".to_string(),
detail,
active_leases: 0,
mode: "auto".to_string(),
detected_devices: 0,
}
}
2026-04-30 08:16:57 -03:00
#[cfg(not(coverage))]
fn unavailable_calibration(detail: String) -> CalibrationStatus {
CalibrationStatus::unavailable(detail)
}
#[cfg(not(coverage))]
fn unavailable_upstream_sync(detail: String) -> UpstreamSyncStatus {
UpstreamSyncStatus::unavailable(detail)
}
2026-04-30 08:16:57 -03:00
#[cfg(not(coverage))]
fn calibration_summary(calibration: &CalibrationStatus) -> String {
format!(
"Upstream A/V calibration: {} audio {:+.1} ms, video {:+.1} ms ({}, {}).",
calibration.profile,
calibration.active_audio_offset_us as f64 / 1000.0,
calibration.active_video_offset_us as f64 / 1000.0,
calibration.source,
calibration.confidence
)
}