This commit is contained in:
Brad Stein 2025-06-24 23:48:06 -05:00
parent 45e1a82295
commit 57b6da0895
10 changed files with 145 additions and 59 deletions

View File

@ -4,15 +4,20 @@
use anyhow::Result; use anyhow::Result;
use std::time::Duration; use std::time::Duration;
use tokio::{sync::broadcast, task::JoinHandle}; use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use tokio_stream::{wrappers::BroadcastStream, StreamExt};
use tonic::Request; use tonic::Request;
use tracing::{debug, error, info, warn}; 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_common::lesavka::{relay_client::RelayClient, KeyboardReport, MouseReport, MonitorRequest, VideoPacket};
use lesavka_client::input::inputs::InputAggregator; use crate::input::inputs::InputAggregator;
use lesavka_client::output::video::MonitorWindow; use crate::output::video::MonitorWindow;
pub struct LesavkaClientApp { pub struct LesavkaClientApp {
aggregator: Option<InputAggregator>, aggregator: Option<InputAggregator>,
@ -58,22 +63,30 @@ impl LesavkaClientApp {
if self.dev_mode { if self.dev_mode {
tokio::time::sleep(Duration::from_secs(30)).await; tokio::time::sleep(Duration::from_secs(30)).await;
warn!("devmode timeout"); warn!("devmode timeout");
self.aggregator.keyboards.dev.ungrab(); // self.aggregator.keyboards.dev.ungrab();
self.aggregator.mice.dev.ungrab(); // self.aggregator.mice.dev.ungrab();
std::process::exit(0); std::process::exit(0);
} else { futures::future::pending::<()>().await } } else { futures::future::pending::<()>().await }
}; };
/* video windows use a dedicated eventloop thread */ /* video windows use a dedicated eventloop thread */
let (video_tx, mut video_rx) = tokio::sync::mpsc::unbounded_channel::<VideoPacket>(); let (video_tx, mut video_rx) = tokio::sync::mpsc::unbounded_channel::<VideoPacket>();
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 || { 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 win0 = MonitorWindow::new(0, &el).expect("win0");
let win1 = MonitorWindow::new(1, &el).expect("win1"); let win1 = MonitorWindow::new(1, &el).expect("win1");
el.run(move |_, _, _| { el.run(move |_: Event<()>, _el| {
while let Ok(pkt) = video_rx.try_recv() { while let Ok(pkt) = video_rx.try_recv() {
match pkt.id { match pkt.id {
0 => win0.push_packet(pkt), 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); let vid_loop = Self::video_loop(self.server_addr.clone(), video_tx);
@ -163,7 +175,7 @@ impl LesavkaClientApp {
Ok(mut stream) => { Ok(mut stream) => {
while let Some(pkt) = stream.get_mut().message().await.transpose() { while let Some(pkt) = stream.get_mut().message().await.transpose() {
match pkt { match pkt {
Ok(p) => let _ = tx.send(p), Ok(p) => { let _ = tx.send(p); },
Err(e) => { error!("video {monitor_id}: {e}"); break } Err(e) => { error!("video {monitor_id}: {e}"); break }
} }
} }

View File

@ -3,7 +3,7 @@
use std::collections::HashSet; use std::collections::HashSet;
use evdev::{Device, EventType, InputEvent, KeyCode}; use evdev::{Device, EventType, InputEvent, KeyCode};
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, warn};
use lesavka_common::lesavka::KeyboardReport; use lesavka_common::lesavka::KeyboardReport;

View File

@ -3,11 +3,10 @@
use gstreamer as gst; use gstreamer as gst;
use gstreamer_app as gst_app; use gstreamer_app as gst_app;
use gst::prelude::*; use gst::prelude::*;
use gst_app::prelude::*;
use lesavka_common::lesavka::VideoPacket; use lesavka_common::lesavka::VideoPacket;
use winit::{ use winit::window::{Window, WindowAttributes};
event_loop::EventLoop, use winit::event_loop::EventLoop;
window::{Window, WindowBuilder},
};
pub struct MonitorWindow { pub struct MonitorWindow {
id: u32, id: u32,
@ -19,31 +18,35 @@ impl MonitorWindow {
pub fn new(id: u32, el: &EventLoop<()>) -> anyhow::Result<Self> { pub fn new(id: u32, el: &EventLoop<()>) -> anyhow::Result<Self> {
gst::init()?; gst::init()?;
let window = WindowBuilder::new() let window = el.create_window(
.with_title(format!("Lesavkamonitor{id}")) WindowAttributes::default()
.with_decorations(false) .with_title(format!("Lesavkamonitor{id}"))
.build(el)?; .with_decorations(false)
)?;
// appsrc -> decode -> convert -> waylandsink // appsrc -> decode -> convert -> waylandsink
let pipeline = gst::parse_launch( let desc = "appsrc name=src is-live=true format=time do-timestamp=true ! \
"appsrc name=src is-live=true format=time do-timestamp=true ! \ queue ! h264parse ! avdec_h264 ! videoconvert ! \
queue ! h264parse ! avdec_h264 ! videoconvert ! \ waylandsink name=sink sync=false";
waylandsink name=sink sync=false",
)?
.downcast::<gst::Pipeline>()?;
let src = pipeline.by_name("src").unwrap().downcast::<gst_app::AppSrc>()?; let pipeline = gst::parse::launch(desc)?
src.set_latency(gst::ClockTime::NONE); .downcast::<gst::Pipeline>()
src.set_property_format(gst::Format::Time); .unwrap();
let src = pipeline
.by_name("src").unwrap()
.downcast::<gst_app::AppSrc>().unwrap();
src.set_latency(gst::ClockTime::NONE, gst::ClockTime::NONE);
pipeline.set_state(gst::State::Playing)?; pipeline.set_state(gst::State::Playing)?;
Ok(Self { id, _window: window, src }) Ok(Self { id, _window: window, src })
} }
pub fn push_packet(&self, pkt: VideoPacket) { pub fn push_packet(&self, pkt: VideoPacket) {
if let Ok(mut buf) = gst::Buffer::from_mut_slice(pkt.data) { let mut buf = gst::Buffer::from_slice(pkt.data);
buf.set_pts(gst::ClockTime::from_microseconds(pkt.pts)); buf.get_mut().unwrap()
let _ = self.src.push_buffer(buf); .set_pts(Some(gst::ClockTime::from_useconds(pkt.pts)));
} let _ = self.src.push_buffer(buf);
} }
} }

View File

@ -4,7 +4,7 @@ path = "build.rs"
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -18,16 +18,27 @@ options uvcvideo quirks=0x200 timeout=10000
EOF EOF
echo "==> 2b. Predictable /dev names for each capture card" echo "==> 2b. Predictable /dev names for each capture card"
sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null <<'RULES' # sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null <<'RULES'
# RIGHT eye (video index 0) # # RIGHT eye (video index 0)
SUBSYSTEM=="video4linux", ATTRS{idVendor}=="07ca", ATTRS{idProduct}=="3311", \ # SUBSYSTEM=="video4linux", ATTRS{idVendor}=="07ca", ATTRS{idProduct}=="3311", \
ATTRS{serial}=="5313550401897", ATTR{index}=="0", \ # ATTRS{serial}=="5313550401897", ATTR{index}=="0", \
SYMLINK+="lesavka_r_eye" # 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 hubport 13
SUBSYSTEM=="video4linux", ATTRS{idVendor}=="07ca", ATTRS{idProduct}=="3311", \ 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" SYMLINK+="lesavka_l_eye"
# RIGHT eye GC311 on hubport 14
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 RULES
sudo udevadm control --reload sudo udevadm control --reload

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -1,4 +1,5 @@
// server/src/lib.rs // server/src/lib.rs
pub mod video; pub mod video;
pub mod usb_reset; pub mod usb_reset;
pub mod usb_gadget;

View File

@ -11,6 +11,7 @@ use tracing::{info, trace};
use tracing_subscriber::{fmt, EnvFilter}; use tracing_subscriber::{fmt, EnvFilter};
use udev::{MonitorBuilder}; use udev::{MonitorBuilder};
use usb_gadget::UsbGadgetManager;
use lesavka_server::{video, usb_reset}; use lesavka_server::{video, usb_reset};
use lesavka_common::lesavka::{ use lesavka_common::lesavka::{
@ -76,6 +77,19 @@ fn list_gc311_devices() -> anyhow::Result<Vec<String>> {
struct Handler { struct Handler {
kb: Arc<Mutex<tokio::fs::File>>, kb: Arc<Mutex<tokio::fs::File>>,
ms: Arc<Mutex<tokio::fs::File>>, ms: Arc<Mutex<tokio::fs::File>>,
gadget: UsbGadgetManager,
}
impl Handler {
fn make(gadget: UsbGadget) -> anyhow::Result<Self> {
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] #[tonic::async_trait]
@ -88,6 +102,7 @@ impl Relay for Handler {
&self, &self,
req: Request<tonic::Streaming<KeyboardReport>>, req: Request<tonic::Streaming<KeyboardReport>>,
) -> Result<Response<Self::StreamKeyboardStream>, Status> { ) -> Result<Response<Self::StreamKeyboardStream>, Status> {
self.gadget.cycle().map_err(|e| Status::internal(e.to_string()))?;
let (tx, rx) = tokio::sync::mpsc::channel(32); let (tx, rx) = tokio::sync::mpsc::channel(32);
let kb = self.kb.clone(); let kb = self.kb.clone();
@ -141,8 +156,12 @@ impl Relay for Handler {
) -> Result<Response<Self::CaptureVideoStream>, Status> { ) -> Result<Response<Self::CaptureVideoStream>, Status> {
let r = req.into_inner(); let r = req.into_inner();
let devs = list_gc311_devices() let devs = loop {
.map_err(|e| Status::internal(format!("enum v4l2: {e}")))?; 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 let dev = devs
.get(r.id as usize) .get(r.id as usize)
@ -172,21 +191,23 @@ async fn main() -> anyhow::Result<()> {
/* autocycle task */ /* autocycle task */
// tokio::spawn(async { monitor_gc311_disconnect().await.ok(); }); // tokio::spawn(async { monitor_gc311_disconnect().await.ok(); });
let kb = OpenOptions::new() let gadget = UsbGadgetManager::new("lesavka");
.write(true) gadget.cycle().ok();
.open("/dev/hidg0")
.await?;
let ms = OpenOptions::new() let handler = Handler::make(gadget.clone())?;
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open("/dev/hidg1")
.await?;
let handler = Handler { tokio::spawn({
kb: Arc::new(Mutex::new(kb)), let gadget = gadget.clone();
ms: Arc::new(Mutex::new(ms)), async move {
}; loop {
tokio::time::sleep(Duration::from_secs(4)).await;
if LAST_HID_WRITE.elapsed().as_secs() > 3 {
warn!("no HID traffic in 3s cycling UDC");
let _ = gadget.cycle();
}
}
}
});
println!("🌐 lesavka-server listening on 0.0.0.0:50051"); println!("🌐 lesavka-server listening on 0.0.0.0:50051");
Server::builder() Server::builder()

38
server/src/usb_gadget.rs Normal file
View File

@ -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/<name>/UDC
Self { udc_file: Box::leak(
format!("/sys/kernel/config/usb_gadget/{gadget_name}/UDC").into_boxed_str())
}
}
/// Force the host to reenumerate our HID gadget.
pub fn cycle(&self) -> Result<()> {
// 1. detach
info!("UDCcycle: 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. reattach 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!("UDCcycle: reattaching to {name}");
OpenOptions::new().write(true).open(self.udc_file)?
.write_all(name.as_bytes())?;
Ok(())
}
}

View File

@ -17,7 +17,7 @@ pub async fn spawn_camera(
// v4l2src → H.264 already, we only parse & relay // v4l2src → H.264 already, we only parse & relay
let desc = format!( 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,\ video/x-h264,stream-format=byte-stream,profile=baseline,level=4,\
bitrate={max_bitrate_kbit}000 ! appsink name=sink emit-signals=true sync=false" bitrate={max_bitrate_kbit}000 ! appsink name=sink emit-signals=true sync=false"
); );