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 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<InputAggregator>,
@ -58,22 +63,30 @@ impl LesavkaClientApp {
if self.dev_mode {
tokio::time::sleep(Duration::from_secs(30)).await;
warn!("devmode 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 eventloop thread */
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 || {
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 }
}
}

View File

@ -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;

View File

@ -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<Self> {
gst::init()?;
let window = WindowBuilder::new()
.with_title(format!("Lesavkamonitor{id}"))
.with_decorations(false)
.build(el)?;
let window = el.create_window(
WindowAttributes::default()
.with_title(format!("Lesavkamonitor{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::<gst::Pipeline>()?;
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::<gst_app::AppSrc>()?;
src.set_latency(gst::ClockTime::NONE);
src.set_property_format(gst::Format::Time);
let pipeline = gst::parse::launch(desc)?
.downcast::<gst::Pipeline>()
.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)?;
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);
}
}

View File

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

View File

@ -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 hubport 13
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 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
sudo udevadm control --reload

View File

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

View File

@ -1,4 +1,5 @@
// server/src/lib.rs
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 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<Vec<String>> {
struct Handler {
kb: 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]
@ -88,6 +102,7 @@ impl Relay for Handler {
&self,
req: Request<tonic::Streaming<KeyboardReport>>,
) -> 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 kb = self.kb.clone();
@ -141,8 +156,12 @@ impl Relay for Handler {
) -> Result<Response<Self::CaptureVideoStream>, 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<()> {
/* autocycle 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 3s cycling UDC");
let _ = gadget.cycle();
}
}
}
});
println!("🌐 lesavka-server listening on 0.0.0.0:50051");
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
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"
);