diff --git a/client/src/app.rs b/client/src/app.rs index 34ac675..afdc8d3 100644 --- a/client/src/app.rs +++ b/client/src/app.rs @@ -4,15 +4,20 @@ use anyhow::Result; use std::time::Duration; -use tokio::{sync::broadcast, task::JoinHandle}; +use tokio::sync::broadcast; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use tonic::Request; use tracing::{debug, error, info, warn}; +use winit::{ + event_loop::EventLoopBuilder, + platform::unix::EventLoopBuilderExtUnix, + event::Event, +}; use lesavka_common::lesavka::{relay_client::RelayClient, KeyboardReport, MouseReport, MonitorRequest, VideoPacket}; -use lesavka_client::input::inputs::InputAggregator; -use lesavka_client::output::video::MonitorWindow; +use crate::input::inputs::InputAggregator; +use crate::output::video::MonitorWindow; pub struct LesavkaClientApp { aggregator: Option, @@ -58,22 +63,30 @@ impl LesavkaClientApp { if self.dev_mode { tokio::time::sleep(Duration::from_secs(30)).await; warn!("dev‑mode timeout"); - self.aggregator.keyboards.dev.ungrab(); - self.aggregator.mice.dev.ungrab(); + // self.aggregator.keyboards.dev.ungrab(); + // self.aggregator.mice.dev.ungrab(); std::process::exit(0); } else { futures::future::pending::<()>().await } }; /* video windows use a dedicated event‑loop thread */ let (video_tx, mut video_rx) = tokio::sync::mpsc::unbounded_channel::(); - let (event_tx, event_rx) = std::sync::mpsc::channel(); + // let (event_tx, event_rx) = std::sync::mpsc::channel(); + let (_event_tx, _event_rx) = std::sync::mpsc::channel::<()>(); + let el = EventLoopBuilder::with_user_event() + .with_any_thread(true) + .build() + .unwrap(); std::thread::spawn(move || { - let el = EventLoop::with_user_event(); + let el = EventLoopBuilder::default() + .with_any_thread(true) + .build() + .unwrap(); let win0 = MonitorWindow::new(0, &el).expect("win0"); let win1 = MonitorWindow::new(1, &el).expect("win1"); - - el.run(move |_, _, _| { + + el.run(move |_: Event<()>, _el| { while let Ok(pkt) = video_rx.try_recv() { match pkt.id { 0 => win0.push_packet(pkt), @@ -82,7 +95,6 @@ impl LesavkaClientApp { } } }); - // never returns }); let vid_loop = Self::video_loop(self.server_addr.clone(), video_tx); @@ -163,7 +175,7 @@ impl LesavkaClientApp { Ok(mut stream) => { while let Some(pkt) = stream.get_mut().message().await.transpose() { match pkt { - Ok(p) => let _ = tx.send(p), + Ok(p) => { let _ = tx.send(p); }, Err(e) => { error!("video {monitor_id}: {e}"); break } } } diff --git a/client/src/input/keyboard.rs b/client/src/input/keyboard.rs index 264fba7..8d72ebb 100644 --- a/client/src/input/keyboard.rs +++ b/client/src/input/keyboard.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use evdev::{Device, EventType, InputEvent, KeyCode}; use tokio::sync::broadcast::Sender; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, warn}; use lesavka_common::lesavka::KeyboardReport; diff --git a/client/src/output/video.rs b/client/src/output/video.rs index 6f306c7..9daf19b 100644 --- a/client/src/output/video.rs +++ b/client/src/output/video.rs @@ -3,11 +3,10 @@ use gstreamer as gst; use gstreamer_app as gst_app; use gst::prelude::*; +use gst_app::prelude::*; use lesavka_common::lesavka::VideoPacket; -use winit::{ - event_loop::EventLoop, - window::{Window, WindowBuilder}, -}; +use winit::window::{Window, WindowAttributes}; +use winit::event_loop::EventLoop; pub struct MonitorWindow { id: u32, @@ -19,31 +18,35 @@ impl MonitorWindow { pub fn new(id: u32, el: &EventLoop<()>) -> anyhow::Result { gst::init()?; - let window = WindowBuilder::new() - .with_title(format!("Lesavka‑monitor‑{id}")) - .with_decorations(false) - .build(el)?; + let window = el.create_window( + WindowAttributes::default() + .with_title(format!("Lesavka‑monitor‑{id}")) + .with_decorations(false) + )?; // appsrc -> decode -> convert -> waylandsink - let pipeline = gst::parse_launch( - "appsrc name=src is-live=true format=time do-timestamp=true ! \ - queue ! h264parse ! avdec_h264 ! videoconvert ! \ - waylandsink name=sink sync=false", - )? - .downcast::()?; + let desc = "appsrc name=src is-live=true format=time do-timestamp=true ! \ + queue ! h264parse ! avdec_h264 ! videoconvert ! \ + waylandsink name=sink sync=false"; - let src = pipeline.by_name("src").unwrap().downcast::()?; - src.set_latency(gst::ClockTime::NONE); - src.set_property_format(gst::Format::Time); + let pipeline = gst::parse::launch(desc)? + .downcast::() + .unwrap(); + + let src = pipeline + .by_name("src").unwrap() + .downcast::().unwrap(); + src.set_latency(gst::ClockTime::NONE, gst::ClockTime::NONE); pipeline.set_state(gst::State::Playing)?; + Ok(Self { id, _window: window, src }) } pub fn push_packet(&self, pkt: VideoPacket) { - if let Ok(mut buf) = gst::Buffer::from_mut_slice(pkt.data) { - buf.set_pts(gst::ClockTime::from_microseconds(pkt.pts)); - let _ = self.src.push_buffer(buf); - } + let mut buf = gst::Buffer::from_slice(pkt.data); + buf.get_mut().unwrap() + .set_pts(Some(gst::ClockTime::from_useconds(pkt.pts))); + let _ = self.src.push_buffer(buf); } } diff --git a/common/Cargo.toml b/common/Cargo.toml index d714e6c..3defb77 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -4,7 +4,7 @@ path = "build.rs" [package] name = "lesavka_common" -version = "0.2.0" +version = "0.3.0" edition = "2024" [dependencies] diff --git a/scripts/install-server.sh b/scripts/install-server.sh index 8b04388..ec15bbe 100755 --- a/scripts/install-server.sh +++ b/scripts/install-server.sh @@ -18,16 +18,27 @@ options uvcvideo quirks=0x200 timeout=10000 EOF echo "==> 2b. Predictable /dev names for each capture card" -sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null <<'RULES' -# RIGHT eye (video index 0) -SUBSYSTEM=="video4linux", ATTRS{idVendor}=="07ca", ATTRS{idProduct}=="3311", \ - ATTRS{serial}=="5313550401897", ATTR{index}=="0", \ - SYMLINK+="lesavka_r_eye" +# sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null <<'RULES' +# # RIGHT eye (video index 0) +# SUBSYSTEM=="video4linux", ATTRS{idVendor}=="07ca", ATTRS{idProduct}=="3311", \ +# ATTRS{serial}=="5313550401897", ATTR{index}=="0", \ +# SYMLINK+="lesavka_r_eye" -# LEFT eye (video index 0) +# # LEFT eye (video index 0) +# SUBSYSTEM=="video4linux", ATTRS{idVendor}=="07ca", ATTRS{idProduct}=="3311", \ +# ATTRS{serial}=="1200655409098", ATTR{index}=="0", \ +# SYMLINK+="lesavka_l_eye" +# RULES +sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null <<'RULES' +# LEFT eye – GC311 on hub‑port 1‑3 SUBSYSTEM=="video4linux", ATTRS{idVendor}=="07ca", ATTRS{idProduct}=="3311", \ - ATTRS{serial}=="1200655409098", ATTR{index}=="0", \ + ATTRS{index}=="0", ENV{ID_PATH_TAG}=="usb-platform-1a400000.xhci-usb-0_1_3_1_0", \ SYMLINK+="lesavka_l_eye" + +# RIGHT eye – GC311 on hub‑port 1‑4 +SUBSYSTEM=="video4linux", ATTRS{idVendor}=="07ca", ATTRS{idProduct}=="3311", \ + ATTRS{index}=="0", ENV{ID_PATH_TAG}=="usb-platform-1a400000.xhci-usb-0_1_4_1_0", \ + SYMLINK+="lesavka_r_eye" RULES sudo udevadm control --reload diff --git a/server/Cargo.toml b/server/Cargo.toml index 97728c1..f2296cb 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_server" -version = "0.2.0" +version = "0.3.0" edition = "2024" [dependencies] diff --git a/server/src/lib.rs b/server/src/lib.rs index d11ab4f..33cfb8b 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,4 +1,5 @@ // server/src/lib.rs pub mod video; -pub mod usb_reset; \ No newline at end of file +pub mod usb_reset; +pub mod usb_gadget; diff --git a/server/src/main.rs b/server/src/main.rs index c82a304..e4daa2b 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -11,6 +11,7 @@ use tracing::{info, trace}; use tracing_subscriber::{fmt, EnvFilter}; use udev::{MonitorBuilder}; +use usb_gadget::UsbGadgetManager; use lesavka_server::{video, usb_reset}; use lesavka_common::lesavka::{ @@ -76,6 +77,19 @@ fn list_gc311_devices() -> anyhow::Result> { struct Handler { kb: Arc>, ms: Arc>, + gadget: UsbGadgetManager, +} + +impl Handler { + fn make(gadget: UsbGadget) -> anyhow::Result { + let kb = OpenOptions::new().write(true).open("/dev/hidg0").await?; + let ms = OpenOptions::new().write(true) + .custom_flags(libc::O_NONBLOCK) + .open("/dev/hidg1").await?; + Ok(Self { kb: Arc::new(Mutex::new(kb)), + ms: Arc::new(Mutex::new(ms)), + gadget }) + } } #[tonic::async_trait] @@ -88,6 +102,7 @@ impl Relay for Handler { &self, req: Request>, ) -> Result, Status> { + self.gadget.cycle().map_err(|e| Status::internal(e.to_string()))?; let (tx, rx) = tokio::sync::mpsc::channel(32); let kb = self.kb.clone(); @@ -141,8 +156,12 @@ impl Relay for Handler { ) -> Result, Status> { let r = req.into_inner(); - let devs = list_gc311_devices() - .map_err(|e| Status::internal(format!("enum v4l2: {e}")))?; + let devs = loop { + let list = list_gc311_devices() + .map_err(|e| Status::internal(format!("enum v4l2: {e}")))?; + if !list.is_empty() { break list; } + tokio::time::sleep(Duration::from_secs(1)).await; + }; let dev = devs .get(r.id as usize) @@ -172,21 +191,23 @@ async fn main() -> anyhow::Result<()> { /* auto‑cycle task */ // tokio::spawn(async { monitor_gc311_disconnect().await.ok(); }); - let kb = OpenOptions::new() - .write(true) - .open("/dev/hidg0") - .await?; + let gadget = UsbGadgetManager::new("lesavka"); + gadget.cycle().ok(); - let ms = OpenOptions::new() - .write(true) - .custom_flags(libc::O_NONBLOCK) - .open("/dev/hidg1") - .await?; + let handler = Handler::make(gadget.clone())?; - let handler = Handler { - kb: Arc::new(Mutex::new(kb)), - ms: Arc::new(Mutex::new(ms)), - }; + tokio::spawn({ + let gadget = gadget.clone(); + async move { + loop { + tokio::time::sleep(Duration::from_secs(4)).await; + if LAST_HID_WRITE.elapsed().as_secs() > 3 { + warn!("no HID traffic in 3 s – cycling UDC"); + let _ = gadget.cycle(); + } + } + } + }); println!("🌐 lesavka-server listening on 0.0.0.0:50051"); Server::builder() diff --git a/server/src/usb_gadget.rs b/server/src/usb_gadget.rs new file mode 100644 index 0000000..d7b28e4 --- /dev/null +++ b/server/src/usb_gadget.rs @@ -0,0 +1,38 @@ +use std::{fs::OpenOptions, io::Write, thread, time::Duration}; +use anyhow::{Result, Context}; +use tracing::{info, warn}; + +#[derive(Clone)] +pub struct UsbGadget { + udc_file: &'static str, +} + +impl UsbGadgetManager { + pub fn new(gadget_name: &'static str) -> Self { + // /sys/kernel/config/usb_gadget//UDC + Self { udc_file: Box::leak( + format!("/sys/kernel/config/usb_gadget/{gadget_name}/UDC").into_boxed_str()) + } + } + + /// Force the host to re‑enumerate our HID gadget. + pub fn cycle(&self) -> Result<()> { + // 1. detach + info!("UDC‑cycle: detaching gadget"); + OpenOptions::new().write(true).open(self.udc_file)? + .write_all(b"")?; + + // 2. wait ≥ 100 ms so host sees a disconnect + std::thread::sleep(Duration::from_millis(200)); + + // 3. re‑attach to **first** UDC (dwc2) + let udc = std::fs::read_dir("/sys/class/udc")? + .next().context("no UDC present")? + .file_name(); + let name = udc.to_string_lossy(); + info!("UDC‑cycle: re‑attaching to {name}"); + OpenOptions::new().write(true).open(self.udc_file)? + .write_all(name.as_bytes())?; + Ok(()) + } +} diff --git a/server/src/video.rs b/server/src/video.rs index 0a15e63..37a5c47 100644 --- a/server/src/video.rs +++ b/server/src/video.rs @@ -17,7 +17,7 @@ pub async fn spawn_camera( // v4l2src → H.264 already, we only parse & relay let desc = format!( - "v4l2src device={dev} io-mode=dmabuf ! queue ! h264parse config-interval=1 ! \ + "v4l2src device={dev} io-mode=dmabuf ! queue ! h264parse config-interval=-1 ! \ video/x-h264,stream-format=byte-stream,profile=baseline,level=4,\ bitrate={max_bitrate_kbit}000 ! appsink name=sink emit-signals=true sync=false" );