From fc38925f0d623bc684ea568a3eb8ca7c59b5d594 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Mon, 20 Apr 2026 01:41:57 -0300 Subject: [PATCH] lesavka: refresh live eye detection --- client/Cargo.toml | 2 +- client/src/bin/lesavka-relayctl.rs | 1 + client/src/launcher/ui.rs | 18 ++++++++- common/Cargo.toml | 2 +- common/src/cli.rs | 2 +- server/Cargo.toml | 2 +- server/src/main.rs | 65 +++++++++++++++++++++++++----- 7 files changed, 76 insertions(+), 16 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index 307b1fc..6c1b0da 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.11.20" +version = "0.11.21" edition = "2024" [dependencies] diff --git a/client/src/bin/lesavka-relayctl.rs b/client/src/bin/lesavka-relayctl.rs index ef747d6..dc8edeb 100644 --- a/client/src/bin/lesavka-relayctl.rs +++ b/client/src/bin/lesavka-relayctl.rs @@ -81,6 +81,7 @@ fn print_state(state: lesavka_common::lesavka::CapturePowerState) { println!("available={}", state.available); println!("enabled={}", state.enabled); println!("mode={}", state.mode); + println!("detected_devices={}", state.detected_devices); println!("active_leases={}", state.active_leases); println!("unit={}", state.unit); println!("detail={}", state.detail); diff --git a/client/src/launcher/ui.rs b/client/src/launcher/ui.rs index 26b5b43..5203b51 100644 --- a/client/src/launcher/ui.rs +++ b/client/src/launcher/ui.rs @@ -738,6 +738,8 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { let caps_request_in_flight = Rc::new(Cell::new(false)); let diagnostics_network = Rc::new(RefCell::new(NetworkTelemetry::default())); let diagnostics_process = Rc::new(RefCell::new(ProcessCpuSampler::new())); + let next_power_probe = + Rc::new(Cell::new(Instant::now() + Duration::from_millis(500))); let next_diagnostics_probe = Rc::new(Cell::new(Instant::now() + Duration::from_millis(250))); let next_diagnostics_sample = @@ -1891,6 +1893,21 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { } let now = Instant::now(); + let child_running = child_proc.borrow().is_some(); + + if now >= next_power_probe.get() + && !power_request_in_flight.get() + && (child_running + || state.borrow().capture_power.enabled + || state.borrow().remote_active) + { + power_request_in_flight.set(true); + let server_addr = + selected_server_addr(&server_entry, server_addr_fallback.as_ref()); + request_capture_power_refresh(power_tx.clone(), server_addr, Duration::ZERO); + next_power_probe.set(now + Duration::from_secs(2)); + } + if now >= next_diagnostics_probe.get() && !caps_request_in_flight.get() { caps_request_in_flight.set(true); let server_addr = @@ -1915,7 +1932,6 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { next_diagnostics_sample.set(now + Duration::from_secs(1)); } - let child_running = child_proc.borrow().is_some(); refresh_launcher_ui(&widgets, &state.borrow(), child_running); refresh_test_buttons(&widgets, &mut tests.borrow_mut()); glib::ControlFlow::Continue diff --git a/common/Cargo.toml b/common/Cargo.toml index 41d5acd..4e99edb 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.11.20" +version = "0.11.21" edition = "2024" build = "build.rs" diff --git a/common/src/cli.rs b/common/src/cli.rs index f01b289..e650e15 100644 --- a/common/src/cli.rs +++ b/common/src/cli.rs @@ -17,6 +17,6 @@ mod tests { #[test] fn banner_includes_version() { - assert_eq!(banner("0.11.20"), "lesavka-common CLI (v0.11.20)"); + assert_eq!(banner("0.11.21"), "lesavka-common CLI (v0.11.21)"); } } diff --git a/server/Cargo.toml b/server/Cargo.toml index 98908c7..49f1578 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.11.20" +version = "0.11.21" edition = "2024" autobins = false diff --git a/server/src/main.rs b/server/src/main.rs index 8b73a58..fe90fe6 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3,6 +3,7 @@ #[allow(clippy::useless_attribute)] #[forbid(unsafe_code)] use futures_util::{Stream, StreamExt}; +use std::collections::HashSet; use std::collections::HashMap; use std::path::Path; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; @@ -112,15 +113,59 @@ impl Handler { Ok(()) } - fn detected_capture_devices() -> u32 { + fn detected_capture_devices_from_symlinks() -> u32 { ["/dev/lesavka_l_eye", "/dev/lesavka_r_eye"] .into_iter() .filter(|path| Path::new(path).exists()) .count() as u32 } - fn with_detected_capture_devices(mut state: CapturePowerState) -> CapturePowerState { - state.detected_devices = Self::detected_capture_devices(); + fn detected_capture_devices_from_udev() -> u32 { + let Ok(mut enumerator) = udev::Enumerator::new() else { + return 0; + }; + let _ = enumerator.match_subsystem("video4linux"); + let Ok(devices) = enumerator.scan_devices() else { + return 0; + }; + devices + .filter(|device| { + device + .attribute_value("index") + .and_then(|value| value.to_str()) + == Some("0") + && device + .property_value("ID_VENDOR_ID") + .and_then(|value| value.to_str()) + == Some("07ca") + && device + .property_value("ID_MODEL_ID") + .and_then(|value| value.to_str()) + == Some("3311") + }) + .count() + .min(2) as u32 + } + + async fn active_eye_source_count(&self) -> u32 { + self.eye_hubs + .lock() + .await + .iter() + .filter_map(|(key, hub)| { + hub.running + .load(Ordering::Relaxed) + .then_some(key.source_id) + }) + .collect::>() + .len() + .min(2) as u32 + } + + async fn with_detected_capture_devices(&self, mut state: CapturePowerState) -> CapturePowerState { + state.detected_devices = Self::detected_capture_devices_from_udev() + .max(Self::detected_capture_devices_from_symlinks()) + .max(self.active_eye_source_count().await); state } @@ -295,12 +340,12 @@ impl Handler { } async fn get_capture_power_reply(&self) -> Result, Status> { - self.capture_power + let state = self + .capture_power .snapshot() .await - .map(Self::with_detected_capture_devices) - .map(Response::new) - .map_err(|e| Status::internal(format!("{e:#}"))) + .map_err(|e| Status::internal(format!("{e:#}")))?; + Ok(Response::new(self.with_detected_capture_devices(state).await)) } async fn set_capture_power_reply( @@ -316,10 +361,8 @@ impl Handler { CapturePowerCommand::ForceOff => self.capture_power.set_manual(false).await, CapturePowerCommand::Unspecified => self.capture_power.set_manual(req.enabled).await, }; - result - .map(Self::with_detected_capture_devices) - .map(Response::new) - .map_err(|e| Status::internal(format!("{e:#}"))) + let state = result.map_err(|e| Status::internal(format!("{e:#}")))?; + Ok(Response::new(self.with_detected_capture_devices(state).await)) } }