lesavka/server/src/main/handler_startup.rs

146 lines
5.3 KiB
Rust
Raw Normal View History

impl Handler {
async fn new(gadget: UsbGadget) -> anyhow::Result<Self> {
#[cfg(not(coverage))]
if runtime_support::allow_gadget_cycle() {
info!("🛠️ Initial USB recovery…");
match UsbGadget::current_controller_state() {
Ok((ctrl, state)) if !UsbGadget::host_enumerated_state(&state) => {
warn!("⚠️ UDC {ctrl} is {state}; forcing gadget recovery before opening HID");
if let Err(error) = gadget.recover_enumeration() {
warn!("⚠️ initial USB recovery did not enumerate the host: {error:#}");
}
}
Ok(_) => {
let _ = gadget.cycle(); // ignore failure - may boot without host
}
Err(error) => {
warn!("⚠️ UDC state unavailable during startup: {error:#}");
let _ = gadget.cycle(); // preserve the old best-effort startup path
}
}
}
#[cfg(not(coverage))]
{
if !runtime_support::allow_gadget_cycle() {
if runtime_support::external_uvc_helper_owns_gadget() {
info!(
"🔒 gadget cycle disabled at startup because external UVC helper owns the gadget"
);
} else {
info!(
"🔒 gadget cycle disabled at startup (set LESAVKA_ALLOW_GADGET_CYCLE=1 to enable)"
);
}
}
info!("🛠️ opening HID endpoints …");
}
let kb_path = hid_endpoint(0);
let ms_path = hid_endpoint(1);
let kb = runtime_support::open_hid_if_ready(&kb_path).await?;
let ms = runtime_support::open_hid_if_ready(&ms_path).await?;
#[cfg(not(coverage))]
if kb.is_some() && ms.is_some() {
info!("✅ HID endpoints ready");
} else {
warn!("⌛ HID endpoints are not ready; relay will keep running and open them lazily");
}
2026-04-30 08:16:57 -03:00
let upstream_media_rt = Arc::new(UpstreamMediaRuntime::new());
let calibration = Arc::new(CalibrationStore::load(upstream_media_rt.clone()));
2026-05-02 17:48:45 -03:00
#[cfg(not(coverage))]
lesavka_server::blind_healer::spawn_blind_healer(
upstream_media_rt.clone(),
calibration.clone(),
);
2026-04-30 08:16:57 -03:00
Ok(Self {
kb: Arc::new(Mutex::new(kb)),
ms: Arc::new(Mutex::new(ms)),
gadget,
did_cycle: Arc::new(AtomicBool::new(false)),
camera_rt: Arc::new(CameraRuntime::new()),
2026-04-30 08:16:57 -03:00
upstream_media_rt,
calibration,
capture_power: CapturePowerManager::new(),
eye_hubs: Arc::new(Mutex::new(HashMap::new())),
})
}
async fn reopen_hid(&self) -> anyhow::Result<()> {
let kb_new = runtime_support::open_hid_if_ready(&hid_endpoint(0)).await?;
let ms_new = runtime_support::open_hid_if_ready(&hid_endpoint(1)).await?;
*self.kb.lock().await = kb_new;
*self.ms.lock().await = ms_new;
Ok(())
}
fn detected_capture_devices_from_symlinks() -> u32 {
["/dev/lesavka_l_eye", "/dev/lesavka_r_eye"]
.into_iter()
.filter(|path| {
std::fs::metadata(path)
.ok()
.is_some_and(|metadata| metadata.file_type().is_char_device())
})
.count() as u32
}
#[cfg(not(coverage))]
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
}
#[cfg(coverage)]
fn detected_capture_devices_from_udev() -> u32 {
std::env::var("LESAVKA_TEST_UDEV_CAPTURE_DEVICES")
.ok()
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(0)
.min(2)
}
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::<HashSet<_>>()
.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
}
}