sakara
This commit is contained in:
parent
45e1a82295
commit
57b6da0895
@ -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!("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::<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 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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!("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::<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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ path = "build.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// server/src/lib.rs
|
||||
|
||||
pub mod video;
|
||||
pub mod usb_reset;
|
||||
pub mod usb_reset;
|
||||
pub mod usb_gadget;
|
||||
|
||||
@ -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<()> {
|
||||
/* 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()
|
||||
|
||||
38
server/src/usb_gadget.rs
Normal file
38
server/src/usb_gadget.rs
Normal 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 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(())
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user