ui: add eye health chips
This commit is contained in:
parent
628b506b64
commit
5a5990d593
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.22.23"
|
version = "0.22.24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.22.23"
|
version = "0.22.24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.22.23"
|
version = "0.22.24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.22.23"
|
version = "0.22.24"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::launcher::{
|
use crate::launcher::{
|
||||||
devices::{CameraMode, DeviceCatalog},
|
devices::{CameraMode, DeviceCatalog},
|
||||||
|
diagnostics::PerformanceSample,
|
||||||
preview::PreviewBinding,
|
preview::PreviewBinding,
|
||||||
state::{
|
state::{
|
||||||
BreakoutSizePreset, LauncherState, PreviewSourceSize, UpstreamAudioTransport,
|
BreakoutSizePreset, LauncherState, PreviewSourceSize, UpstreamAudioTransport,
|
||||||
@ -45,19 +46,101 @@ fn local_test_detail_mentions_idle_and_running_modes() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn gpio_power_label_tracks_detected_devices() {
|
fn gpio_power_label_tracks_detected_devices() {
|
||||||
let mut power = CapturePowerStatus::default();
|
let mut power = CapturePowerStatus::default();
|
||||||
assert_eq!(gpio_power_label(&power), "Unavailable");
|
assert_eq!(gpio_power_label(&power), "Off");
|
||||||
|
|
||||||
power.available = true;
|
power.available = true;
|
||||||
assert_eq!(gpio_power_label(&power), "Power Off");
|
assert_eq!(gpio_power_label(&power), "Off");
|
||||||
|
|
||||||
power.enabled = true;
|
power.enabled = true;
|
||||||
assert_eq!(gpio_power_label(&power), "No Eyes");
|
assert_eq!(gpio_power_label(&power), "On");
|
||||||
|
|
||||||
power.detected_devices = 1;
|
power.detected_devices = 1;
|
||||||
assert_eq!(gpio_power_label(&power), "1 Eye");
|
assert_eq!(gpio_power_label(&power), "On");
|
||||||
|
|
||||||
power.detected_devices = 2;
|
power.detected_devices = 2;
|
||||||
assert_eq!(gpio_power_label(&power), "2 Eyes");
|
assert_eq!(gpio_power_label(&power), "On");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eye_health_sample() -> PerformanceSample {
|
||||||
|
PerformanceSample {
|
||||||
|
rtt_ms: 20.0,
|
||||||
|
probe_spread_ms: 2.0,
|
||||||
|
input_latency_ms: 12.0,
|
||||||
|
probe_loss_pct: 0.0,
|
||||||
|
client_process_cpu_pct: 10.0,
|
||||||
|
server_process_cpu_pct: 8.0,
|
||||||
|
video_loss_pct: 0.0,
|
||||||
|
left_receive_fps: 30.0,
|
||||||
|
left_present_fps: 30.0,
|
||||||
|
left_server_fps: 30.0,
|
||||||
|
left_stream_spread_ms: 3.0,
|
||||||
|
left_packet_gap_peak_ms: 20.0,
|
||||||
|
left_present_gap_peak_ms: 20.0,
|
||||||
|
left_queue_depth: 0,
|
||||||
|
left_queue_peak: 0,
|
||||||
|
left_server_source_gap_peak_ms: 20.0,
|
||||||
|
left_server_send_gap_peak_ms: 20.0,
|
||||||
|
left_server_queue_peak: 0,
|
||||||
|
left_server_encoder_label: "source-pass-through".to_string(),
|
||||||
|
left_decoder_label: "nvidia-h264".to_string(),
|
||||||
|
left_stream_caps_label: "video/x-h264".to_string(),
|
||||||
|
left_decoded_caps_label: "video/x-raw".to_string(),
|
||||||
|
left_rendered_caps_label: "video/x-raw".to_string(),
|
||||||
|
right_receive_fps: 30.0,
|
||||||
|
right_present_fps: 30.0,
|
||||||
|
right_server_fps: 30.0,
|
||||||
|
right_stream_spread_ms: 3.0,
|
||||||
|
right_packet_gap_peak_ms: 20.0,
|
||||||
|
right_present_gap_peak_ms: 20.0,
|
||||||
|
right_queue_depth: 0,
|
||||||
|
right_queue_peak: 0,
|
||||||
|
right_server_source_gap_peak_ms: 20.0,
|
||||||
|
right_server_send_gap_peak_ms: 20.0,
|
||||||
|
right_server_queue_peak: 0,
|
||||||
|
right_server_encoder_label: "source-pass-through".to_string(),
|
||||||
|
right_decoder_label: "nvidia-h264".to_string(),
|
||||||
|
right_stream_caps_label: "video/x-h264".to_string(),
|
||||||
|
right_decoded_caps_label: "video/x-raw".to_string(),
|
||||||
|
right_rendered_caps_label: "video/x-raw".to_string(),
|
||||||
|
upstream_camera: UpstreamStreamTelemetry::default(),
|
||||||
|
upstream_microphone: UpstreamStreamTelemetry::default(),
|
||||||
|
upstream_mode: "bundled-webcam-media".to_string(),
|
||||||
|
dropped_frames: 0,
|
||||||
|
queue_depth: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eye_stream_chip_reports_per_eye_fps_quality() {
|
||||||
|
let sample = eye_health_sample();
|
||||||
|
assert_eq!(
|
||||||
|
eye_stream_health(Some(&sample), EyeChipSide::Left, false),
|
||||||
|
(
|
||||||
|
StatusLightState::Idle,
|
||||||
|
"Off".to_string(),
|
||||||
|
"Relay is not connected.".to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let (state, label, tooltip) = eye_stream_health(Some(&sample), EyeChipSide::Left, true);
|
||||||
|
assert_eq!(state, StatusLightState::Connected);
|
||||||
|
assert_eq!(label, "30 fps");
|
||||||
|
assert!(tooltip.contains("present=30.0 fps"));
|
||||||
|
|
||||||
|
let mut degraded = sample.clone();
|
||||||
|
degraded.right_present_fps = 20.0;
|
||||||
|
degraded.right_queue_peak = 3;
|
||||||
|
let (state, label, tooltip) = eye_stream_health(Some(°raded), EyeChipSide::Right, true);
|
||||||
|
assert_eq!(state, StatusLightState::Caution);
|
||||||
|
assert_eq!(label, "20 fps");
|
||||||
|
assert!(tooltip.contains("queue=0/3"));
|
||||||
|
|
||||||
|
let mut broken = sample;
|
||||||
|
broken.left_receive_fps = 0.0;
|
||||||
|
broken.left_present_fps = 0.0;
|
||||||
|
let (state, label, _) = eye_stream_health(Some(&broken), EyeChipSide::Left, true);
|
||||||
|
assert_eq!(state, StatusLightState::Warning);
|
||||||
|
assert_eq!(label, "0 fps");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gtk::test]
|
#[gtk::test]
|
||||||
@ -624,7 +707,7 @@ fn uac_chip_uses_live_microphone_flow_not_only_server_caps() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
recovery_uac_health(&state, false, None),
|
recovery_uac_health(&state, false, None),
|
||||||
(StatusLightState::Live, "Opus".to_string())
|
(StatusLightState::Live, "PCM".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
recovery_uac_health(&state, true, None),
|
recovery_uac_health(&state, true, None),
|
||||||
@ -642,7 +725,7 @@ fn uac_chip_uses_live_microphone_flow_not_only_server_caps() {
|
|||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
recovery_uac_health(&state, true, Some(&healthy)),
|
recovery_uac_health(&state, true, Some(&healthy)),
|
||||||
(StatusLightState::Live, "Opus".to_string())
|
(StatusLightState::Live, "PCM".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
state.set_server_media_caps(None, None, None, None);
|
state.set_server_media_caps(None, None, None, None);
|
||||||
@ -652,7 +735,7 @@ fn uac_chip_uses_live_microphone_flow_not_only_server_caps() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
recovery_uac_health(&state, true, Some(&healthy)),
|
recovery_uac_health(&state, true, Some(&healthy)),
|
||||||
(StatusLightState::Live, "Opus".to_string())
|
(StatusLightState::Live, "PCM".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
state.select_upstream_audio_transport(UpstreamAudioTransport::Pcm);
|
state.select_upstream_audio_transport(UpstreamAudioTransport::Pcm);
|
||||||
|
|||||||
@ -41,6 +41,10 @@ pub fn build_launcher_view(
|
|||||||
routing_value,
|
routing_value,
|
||||||
gpio_light,
|
gpio_light,
|
||||||
gpio_value,
|
gpio_value,
|
||||||
|
left_eye_light,
|
||||||
|
left_eye_value,
|
||||||
|
right_eye_light,
|
||||||
|
right_eye_value,
|
||||||
usb_light,
|
usb_light,
|
||||||
usb_value,
|
usb_value,
|
||||||
uac_light,
|
uac_light,
|
||||||
|
|||||||
@ -110,6 +110,10 @@
|
|||||||
routing_value,
|
routing_value,
|
||||||
gpio_light,
|
gpio_light,
|
||||||
gpio_value,
|
gpio_value,
|
||||||
|
left_eye_light,
|
||||||
|
left_eye_value,
|
||||||
|
right_eye_light,
|
||||||
|
right_eye_value,
|
||||||
usb_light,
|
usb_light,
|
||||||
usb_value,
|
usb_value,
|
||||||
uac_light,
|
uac_light,
|
||||||
|
|||||||
@ -11,6 +11,10 @@ struct LauncherShellContext {
|
|||||||
routing_value: gtk::Label,
|
routing_value: gtk::Label,
|
||||||
gpio_light: gtk::Box,
|
gpio_light: gtk::Box,
|
||||||
gpio_value: gtk::Label,
|
gpio_value: gtk::Label,
|
||||||
|
left_eye_light: gtk::Box,
|
||||||
|
left_eye_value: gtk::Label,
|
||||||
|
right_eye_light: gtk::Box,
|
||||||
|
right_eye_value: gtk::Label,
|
||||||
usb_light: gtk::Box,
|
usb_light: gtk::Box,
|
||||||
usb_value: gtk::Label,
|
usb_value: gtk::Label,
|
||||||
uac_light: gtk::Box,
|
uac_light: gtk::Box,
|
||||||
|
|||||||
@ -49,14 +49,20 @@
|
|||||||
let (relay_chip, relay_light, relay_value) = build_status_chip_with_light("Server", "");
|
let (relay_chip, relay_light, relay_value) = build_status_chip_with_light("Server", "");
|
||||||
let (routing_chip, routing_light, routing_value) =
|
let (routing_chip, routing_light, routing_value) =
|
||||||
build_status_chip_with_light("Inputs", "Local");
|
build_status_chip_with_light("Inputs", "Local");
|
||||||
let (gpio_chip, gpio_light, gpio_value) = build_status_chip_with_light("GPIO", "Unknown");
|
let (gpio_chip, gpio_light, gpio_value) = build_status_chip_with_light("GPIO", "Off");
|
||||||
|
let (left_eye_chip, left_eye_light, left_eye_value) =
|
||||||
|
build_status_chip_with_light("Left", "Off");
|
||||||
|
let (right_eye_chip, right_eye_light, right_eye_value) =
|
||||||
|
build_status_chip_with_light("Right", "Off");
|
||||||
let (usb_chip, usb_light, usb_value) = build_status_chip_with_light("HID", "Unknown");
|
let (usb_chip, usb_light, usb_value) = build_status_chip_with_light("HID", "Unknown");
|
||||||
let (uac_chip, uac_light, uac_value) = build_status_chip_with_light("UAC", "Unknown");
|
let (uac_chip, uac_light, uac_value) = build_status_chip_with_light("UAC", "Unknown");
|
||||||
let (uvc_chip, uvc_light, uvc_value) = build_status_chip_with_light("UVC", "Unknown");
|
let (uvc_chip, uvc_light, uvc_value) = build_status_chip_with_light("UVC", "Unknown");
|
||||||
let (shortcut_chip, shortcut_value) = build_status_chip("Swap Key", "Pause");
|
let (shortcut_chip, shortcut_value) = build_status_chip("Key", "Pause");
|
||||||
chips.append(&relay_chip);
|
chips.append(&relay_chip);
|
||||||
chips.append(&routing_chip);
|
chips.append(&routing_chip);
|
||||||
chips.append(&gpio_chip);
|
chips.append(&gpio_chip);
|
||||||
|
chips.append(&left_eye_chip);
|
||||||
|
chips.append(&right_eye_chip);
|
||||||
chips.append(&usb_chip);
|
chips.append(&usb_chip);
|
||||||
chips.append(&uac_chip);
|
chips.append(&uac_chip);
|
||||||
chips.append(&uvc_chip);
|
chips.append(&uvc_chip);
|
||||||
@ -70,6 +76,7 @@
|
|||||||
chips_shell.set_has_frame(false);
|
chips_shell.set_has_frame(false);
|
||||||
chips_shell.set_propagate_natural_width(false);
|
chips_shell.set_propagate_natural_width(false);
|
||||||
chips_shell.set_min_content_width(0);
|
chips_shell.set_min_content_width(0);
|
||||||
|
chips_shell.set_size_request(0, -1);
|
||||||
hero.append(&chips_shell);
|
hero.append(&chips_shell);
|
||||||
root.append(&hero);
|
root.append(&hero);
|
||||||
|
|
||||||
@ -121,6 +128,10 @@
|
|||||||
routing_value,
|
routing_value,
|
||||||
gpio_light,
|
gpio_light,
|
||||||
gpio_value,
|
gpio_value,
|
||||||
|
left_eye_light,
|
||||||
|
left_eye_value,
|
||||||
|
right_eye_light,
|
||||||
|
right_eye_value,
|
||||||
usb_light,
|
usb_light,
|
||||||
usb_value,
|
usb_value,
|
||||||
uac_light,
|
uac_light,
|
||||||
|
|||||||
@ -294,7 +294,7 @@ fn human_audio_node_label(value: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 24;
|
const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 18;
|
||||||
|
|
||||||
fn shorten_label_with_limit(value: &str, max: usize) -> String {
|
fn shorten_label_with_limit(value: &str, max: usize) -> String {
|
||||||
let compact = value.replace('_', " ");
|
let compact = value.replace('_', " ");
|
||||||
|
|||||||
@ -47,12 +47,15 @@ fn build_subgroup_with_action(title: &str, action: Option<>k::Widget>) -> gtk:
|
|||||||
group
|
group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STATUS_CHIP_WIDTH: i32 = 84;
|
||||||
|
const STATUS_CHIP_VALUE_CHARS: i32 = 8;
|
||||||
|
|
||||||
/// Build a fixed-width status chip so changing labels do not move the header row.
|
/// Build a fixed-width status chip so changing labels do not move the header row.
|
||||||
fn build_status_chip(label: &str, value: &str) -> (gtk::Box, gtk::Label) {
|
fn build_status_chip(label: &str, value: &str) -> (gtk::Box, gtk::Label) {
|
||||||
let chip = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
let chip = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
||||||
chip.add_css_class("status-chip");
|
chip.add_css_class("status-chip");
|
||||||
chip.set_hexpand(false);
|
chip.set_hexpand(false);
|
||||||
chip.set_size_request(96, -1);
|
chip.set_size_request(STATUS_CHIP_WIDTH, -1);
|
||||||
|
|
||||||
let label_widget = gtk::Label::new(Some(label));
|
let label_widget = gtk::Label::new(Some(label));
|
||||||
label_widget.add_css_class("status-chip-label");
|
label_widget.add_css_class("status-chip-label");
|
||||||
@ -64,7 +67,7 @@ fn build_status_chip(label: &str, value: &str) -> (gtk::Box, gtk::Label) {
|
|||||||
value_widget.set_xalign(0.5);
|
value_widget.set_xalign(0.5);
|
||||||
value_widget.set_ellipsize(pango::EllipsizeMode::End);
|
value_widget.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
value_widget.set_single_line_mode(true);
|
value_widget.set_single_line_mode(true);
|
||||||
value_widget.set_width_chars(9);
|
value_widget.set_width_chars(STATUS_CHIP_VALUE_CHARS);
|
||||||
chip.append(&label_widget);
|
chip.append(&label_widget);
|
||||||
chip.append(&value_widget);
|
chip.append(&value_widget);
|
||||||
(chip, value_widget)
|
(chip, value_widget)
|
||||||
@ -75,7 +78,7 @@ fn build_status_chip_with_light(label: &str, value: &str) -> (gtk::Box, gtk::Box
|
|||||||
let chip = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
let chip = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
||||||
chip.add_css_class("status-chip");
|
chip.add_css_class("status-chip");
|
||||||
chip.set_hexpand(false);
|
chip.set_hexpand(false);
|
||||||
chip.set_size_request(96, -1);
|
chip.set_size_request(STATUS_CHIP_WIDTH, -1);
|
||||||
|
|
||||||
let meta = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
let meta = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||||
meta.add_css_class("status-chip-meta");
|
meta.add_css_class("status-chip-meta");
|
||||||
@ -95,7 +98,7 @@ fn build_status_chip_with_light(label: &str, value: &str) -> (gtk::Box, gtk::Box
|
|||||||
value_widget.set_xalign(0.5);
|
value_widget.set_xalign(0.5);
|
||||||
value_widget.set_ellipsize(pango::EllipsizeMode::End);
|
value_widget.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
value_widget.set_single_line_mode(true);
|
value_widget.set_single_line_mode(true);
|
||||||
value_widget.set_width_chars(9);
|
value_widget.set_width_chars(STATUS_CHIP_VALUE_CHARS);
|
||||||
chip.append(&meta);
|
chip.append(&meta);
|
||||||
chip.append(&value_widget);
|
chip.append(&value_widget);
|
||||||
(chip, light, value_widget)
|
(chip, light, value_widget)
|
||||||
|
|||||||
@ -6,6 +6,10 @@ pub struct SummaryWidgets {
|
|||||||
pub routing_value: gtk::Label,
|
pub routing_value: gtk::Label,
|
||||||
pub gpio_light: gtk::Box,
|
pub gpio_light: gtk::Box,
|
||||||
pub gpio_value: gtk::Label,
|
pub gpio_value: gtk::Label,
|
||||||
|
pub left_eye_light: gtk::Box,
|
||||||
|
pub left_eye_value: gtk::Label,
|
||||||
|
pub right_eye_light: gtk::Box,
|
||||||
|
pub right_eye_value: gtk::Label,
|
||||||
pub usb_light: gtk::Box,
|
pub usb_light: gtk::Box,
|
||||||
pub usb_value: gtk::Label,
|
pub usb_value: gtk::Label,
|
||||||
pub uac_light: gtk::Box,
|
pub uac_light: gtk::Box,
|
||||||
@ -217,7 +221,7 @@ const LAUNCHER_DEFAULT_WIDTH: i32 = 1540;
|
|||||||
const LAUNCHER_DEFAULT_HEIGHT: i32 = 880;
|
const LAUNCHER_DEFAULT_HEIGHT: i32 = 880;
|
||||||
const OPERATIONS_RAIL_WIDTH: i32 = 276;
|
const OPERATIONS_RAIL_WIDTH: i32 = 276;
|
||||||
const RELAY_SUBGROUP_LABEL_WIDTH: i32 = 11;
|
const RELAY_SUBGROUP_LABEL_WIDTH: i32 = 11;
|
||||||
const DEVICE_STAGING_PANEL_WIDTH: i32 = 560;
|
const DEVICE_STAGING_PANEL_WIDTH: i32 = 512;
|
||||||
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 270;
|
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 270;
|
||||||
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 480;
|
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 480;
|
||||||
const EYE_PREVIEW_MIN_HEIGHT: i32 = 299;
|
const EYE_PREVIEW_MIN_HEIGHT: i32 = 299;
|
||||||
|
|||||||
@ -1,14 +1,8 @@
|
|||||||
pub fn gpio_power_label(power: &CapturePowerStatus) -> String {
|
pub fn gpio_power_label(power: &CapturePowerStatus) -> String {
|
||||||
if !power.available {
|
if power.available && power.enabled {
|
||||||
return "Unavailable".to_string();
|
"On".to_string()
|
||||||
}
|
} else {
|
||||||
if !power.enabled {
|
"Off".to_string()
|
||||||
return "Power Off".to_string();
|
|
||||||
}
|
|
||||||
match power.detected_devices {
|
|
||||||
0 => "No Eyes".to_string(),
|
|
||||||
1 => "1 Eye".to_string(),
|
|
||||||
count => format!("{count} Eyes"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,14 +280,7 @@ fn media_stream_health(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn gpio_light_state(power: &CapturePowerStatus) -> StatusLightState {
|
fn gpio_light_state(power: &CapturePowerStatus) -> StatusLightState {
|
||||||
if !power.available || !power.enabled {
|
StatusLightState::from_active(power.available && power.enabled)
|
||||||
return StatusLightState::Idle;
|
|
||||||
}
|
|
||||||
match power.detected_devices {
|
|
||||||
0 => StatusLightState::Warning,
|
|
||||||
1 => StatusLightState::Caution,
|
|
||||||
_ => StatusLightState::Live,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gpio_detection_detail(detected_devices: u32) -> String {
|
fn gpio_detection_detail(detected_devices: u32) -> String {
|
||||||
@ -304,6 +291,79 @@ fn gpio_detection_detail(detected_devices: u32) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum EyeChipSide {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eye_stream_health(
|
||||||
|
sample: Option<&crate::launcher::diagnostics::PerformanceSample>,
|
||||||
|
side: EyeChipSide,
|
||||||
|
relay_live: bool,
|
||||||
|
) -> (StatusLightState, String, String) {
|
||||||
|
if !relay_live {
|
||||||
|
return (
|
||||||
|
StatusLightState::Idle,
|
||||||
|
"Off".to_string(),
|
||||||
|
"Relay is not connected.".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let Some(sample) = sample else {
|
||||||
|
return (
|
||||||
|
StatusLightState::Warning,
|
||||||
|
"0 fps".to_string(),
|
||||||
|
"No eye telemetry sample is available yet.".to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let (receive_fps, present_fps, server_fps, queue_depth, queue_peak, packet_gap, present_gap) =
|
||||||
|
match side {
|
||||||
|
EyeChipSide::Left => (
|
||||||
|
sample.left_receive_fps,
|
||||||
|
sample.left_present_fps,
|
||||||
|
sample.left_server_fps,
|
||||||
|
sample.left_queue_depth,
|
||||||
|
sample.left_queue_peak,
|
||||||
|
sample.left_packet_gap_peak_ms,
|
||||||
|
sample.left_present_gap_peak_ms,
|
||||||
|
),
|
||||||
|
EyeChipSide::Right => (
|
||||||
|
sample.right_receive_fps,
|
||||||
|
sample.right_present_fps,
|
||||||
|
sample.right_server_fps,
|
||||||
|
sample.right_queue_depth,
|
||||||
|
sample.right_queue_peak,
|
||||||
|
sample.right_packet_gap_peak_ms,
|
||||||
|
sample.right_present_gap_peak_ms,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let label = format!("{:.0} fps", present_fps.max(0.0));
|
||||||
|
let target_fps = server_fps.max(receive_fps).max(1.0);
|
||||||
|
let ratio = (present_fps / target_fps).clamp(0.0, 1.0);
|
||||||
|
let has_glitch = sample.video_loss_pct > 0.1
|
||||||
|
|| sample.dropped_frames > 0
|
||||||
|
|| queue_depth > 0
|
||||||
|
|| queue_peak > 2
|
||||||
|
|| packet_gap > 90.0
|
||||||
|
|| present_gap > 90.0;
|
||||||
|
let state = if present_fps < 1.0 || receive_fps < 1.0 {
|
||||||
|
StatusLightState::Warning
|
||||||
|
} else if ratio >= 0.98 && !has_glitch {
|
||||||
|
StatusLightState::Connected
|
||||||
|
} else if ratio >= 0.90 && sample.video_loss_pct < 1.0 {
|
||||||
|
StatusLightState::Live
|
||||||
|
} else if ratio >= 0.65 {
|
||||||
|
StatusLightState::Caution
|
||||||
|
} else {
|
||||||
|
StatusLightState::Warning
|
||||||
|
};
|
||||||
|
let tooltip = format!(
|
||||||
|
"present={present_fps:.1} fps receive={receive_fps:.1} fps server={server_fps:.1} fps loss={:.1}% dropped={} queue={}/{} gaps={packet_gap:.0}/{present_gap:.0} ms",
|
||||||
|
sample.video_loss_pct, sample.dropped_frames, queue_depth, queue_peak
|
||||||
|
);
|
||||||
|
(state, label, tooltip)
|
||||||
|
}
|
||||||
|
|
||||||
/// Highlights the currently active capture mode so it reads like a segmented control.
|
/// Highlights the currently active capture mode so it reads like a segmented control.
|
||||||
fn sync_power_mode_button_styles(widgets: &LauncherWidgets, mode: &str) {
|
fn sync_power_mode_button_styles(widgets: &LauncherWidgets, mode: &str) {
|
||||||
for button in [
|
for button in [
|
||||||
|
|||||||
@ -68,7 +68,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
let gpio_label = if state.server_available {
|
let gpio_label = if state.server_available {
|
||||||
gpio_power_label(&state.capture_power)
|
gpio_power_label(&state.capture_power)
|
||||||
} else {
|
} else {
|
||||||
"Offline".to_string()
|
"Off".to_string()
|
||||||
};
|
};
|
||||||
set_status_light(
|
set_status_light(
|
||||||
&widgets.summary.gpio_light,
|
&widgets.summary.gpio_light,
|
||||||
@ -79,12 +79,32 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
widgets.summary.gpio_value.set_text(&gpio_label);
|
widgets.summary.gpio_value.set_text(&gpio_label);
|
||||||
widgets.summary.gpio_value.set_tooltip_text(Some(&gpio_label));
|
widgets
|
||||||
|
.summary
|
||||||
|
.gpio_value
|
||||||
|
.set_tooltip_text(Some(&capture_power_detail(&state.capture_power)));
|
||||||
widgets
|
widgets
|
||||||
.summary
|
.summary
|
||||||
.shortcut_value
|
.shortcut_value
|
||||||
.set_text(&toggle_key_label(&state.swap_key));
|
.set_text(&toggle_key_label(&state.swap_key));
|
||||||
|
|
||||||
|
let (left_eye_state, left_eye_value, left_eye_tooltip) =
|
||||||
|
eye_stream_health(latest_sample.as_ref(), EyeChipSide::Left, relay_live);
|
||||||
|
set_status_light(&widgets.summary.left_eye_light, left_eye_state);
|
||||||
|
widgets.summary.left_eye_value.set_text(&left_eye_value);
|
||||||
|
widgets
|
||||||
|
.summary
|
||||||
|
.left_eye_value
|
||||||
|
.set_tooltip_text(Some(&left_eye_tooltip));
|
||||||
|
let (right_eye_state, right_eye_value, right_eye_tooltip) =
|
||||||
|
eye_stream_health(latest_sample.as_ref(), EyeChipSide::Right, relay_live);
|
||||||
|
set_status_light(&widgets.summary.right_eye_light, right_eye_state);
|
||||||
|
widgets.summary.right_eye_value.set_text(&right_eye_value);
|
||||||
|
widgets
|
||||||
|
.summary
|
||||||
|
.right_eye_value
|
||||||
|
.set_tooltip_text(Some(&right_eye_tooltip));
|
||||||
|
|
||||||
let (usb_state, usb_value) = recovery_usb_health(state, relay_live);
|
let (usb_state, usb_value) = recovery_usb_health(state, relay_live);
|
||||||
set_status_light(&widgets.summary.usb_light, usb_state);
|
set_status_light(&widgets.summary.usb_light, usb_state);
|
||||||
widgets.summary.usb_value.set_text(&usb_value);
|
widgets.summary.usb_value.set_text(&usb_value);
|
||||||
@ -285,9 +305,9 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
widgets
|
widgets
|
||||||
.upstream_audio_transport_combo
|
.upstream_audio_transport_combo
|
||||||
.set_tooltip_text(Some(if relay_live {
|
.set_tooltip_text(Some(if relay_live {
|
||||||
"Changing upstream audio transport restarts the microphone path; Opus compresses, PCM is the fallback."
|
"Changing upstream audio transport restarts the microphone path; PCM is stable, Opus is compressed and experimental."
|
||||||
} else {
|
} else {
|
||||||
"Choose Opus for compressed upstream audio or PCM as the known-good fallback."
|
"Choose PCM for the known-good route or Opus for compressed upstream audio experiments."
|
||||||
}));
|
}));
|
||||||
widgets.input_toggle_button.set_label(match state.routing {
|
widgets.input_toggle_button.set_label(match state.routing {
|
||||||
InputRouting::Remote => "Route Local",
|
InputRouting::Remote => "Route Local",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.22.23"
|
version = "0.22.24"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.22.23"
|
version = "0.22.24"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -158,7 +158,7 @@ fn eye_and_device_combos_use_compact_labels_so_the_rail_stays_visible() {
|
|||||||
assert!(UI_LAYOUT_SRC.contains("combo.set_popup_fixed_width(false);"));
|
assert!(UI_LAYOUT_SRC.contains("combo.set_popup_fixed_width(false);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("fn combo_active_full_label("));
|
assert!(UI_LAYOUT_SRC.contains("fn combo_active_full_label("));
|
||||||
assert!(UI_LAYOUT_SRC.contains("Selected: {full}. Open the dropdown for compact choices"));
|
assert!(UI_LAYOUT_SRC.contains("Selected: {full}. Open the dropdown for compact choices"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 24;"));
|
assert!(UI_LAYOUT_SRC.contains("const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 18;"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("append_input_choice(combo, value);"));
|
assert!(UI_LAYOUT_SRC.contains("append_input_choice(combo, value);"));
|
||||||
assert!(
|
assert!(
|
||||||
UI_LAYOUT_SRC.contains("shorten_label_with_limit(&label, DEVICE_COMBO_BUTTON_LABEL_LIMIT)")
|
UI_LAYOUT_SRC.contains("shorten_label_with_limit(&label, DEVICE_COMBO_BUTTON_LABEL_LIMIT)")
|
||||||
@ -189,7 +189,7 @@ fn eye_and_device_combos_use_compact_labels_so_the_rail_stays_visible() {
|
|||||||
fn device_staging_and_testing_stay_independent_so_preview_does_not_fill_dead_height() {
|
fn device_staging_and_testing_stay_independent_so_preview_does_not_fill_dead_height() {
|
||||||
assert!(UI_LAYOUT_SRC.contains("staging_row.set_homogeneous(false);"));
|
assert!(UI_LAYOUT_SRC.contains("staging_row.set_homogeneous(false);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("staging_row.set_vexpand(false);"));
|
assert!(UI_LAYOUT_SRC.contains("staging_row.set_vexpand(false);"));
|
||||||
assert_eq!(const_i32("DEVICE_STAGING_PANEL_WIDTH"), 560);
|
assert_eq!(const_i32("DEVICE_STAGING_PANEL_WIDTH"), 512);
|
||||||
assert!(UI_LAYOUT_SRC.contains("devices_panel.set_hexpand(false);"));
|
assert!(UI_LAYOUT_SRC.contains("devices_panel.set_hexpand(false);"));
|
||||||
assert!(
|
assert!(
|
||||||
UI_LAYOUT_SRC.contains("devices_panel.set_size_request(DEVICE_STAGING_PANEL_WIDTH, -1);")
|
UI_LAYOUT_SRC.contains("devices_panel.set_size_request(DEVICE_STAGING_PANEL_WIDTH, -1);")
|
||||||
@ -324,13 +324,15 @@ fn status_chip_text_is_centered_inside_each_pill() {
|
|||||||
UI_LAYOUT_SRC
|
UI_LAYOUT_SRC
|
||||||
.matches("set_halign(gtk::Align::Center);")
|
.matches("set_halign(gtk::Align::Center);")
|
||||||
.count()
|
.count()
|
||||||
>= 5,
|
>= 7,
|
||||||
"chip labels and values should be horizontally centered"
|
"chip labels and values should be horizontally centered"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
UI_LAYOUT_SRC.matches("set_xalign(0.5);").count() >= 4,
|
UI_LAYOUT_SRC.matches("set_xalign(0.5);").count() >= 6,
|
||||||
"chip text lines should center their own text, not only their widgets"
|
"chip text lines should center their own text, not only their widgets"
|
||||||
);
|
);
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("build_status_chip_with_light(\"Left\", \"Off\")"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("build_status_chip_with_light(\"Right\", \"Off\")"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -288,11 +288,9 @@ fn launcher_audio_transport_selection_reaches_relay_env_and_chips() {
|
|||||||
assert!(
|
assert!(
|
||||||
UI_RUNTIME_SRC.contains("Changing upstream audio transport restarts the microphone path")
|
UI_RUNTIME_SRC.contains("Changing upstream audio transport restarts the microphone path")
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(UI_RUNTIME_SRC.contains(
|
||||||
UI_RUNTIME_SRC.contains(
|
"Choose PCM for the known-good route or Opus for compressed upstream audio experiments."
|
||||||
"Choose Opus for compressed upstream audio or PCM as the known-good fallback."
|
));
|
||||||
)
|
|
||||||
);
|
|
||||||
assert!(UI_SRC.contains("audio_combo.connect_changed"));
|
assert!(UI_SRC.contains("audio_combo.connect_changed"));
|
||||||
assert!(UI_SRC.contains("toggle.connect_toggled"));
|
assert!(UI_SRC.contains("toggle.connect_toggled"));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user