sakara
This commit is contained in:
parent
45e1a82295
commit
57b6da0895
@ -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!("dev‑mode timeout");
|
warn!("dev‑mode 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 event‑loop thread */
|
/* video windows use a dedicated event‑loop 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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
WindowAttributes::default()
|
||||||
.with_title(format!("Lesavka‑monitor‑{id}"))
|
.with_title(format!("Lesavka‑monitor‑{id}"))
|
||||||
.with_decorations(false)
|
.with_decorations(false)
|
||||||
.build(el)?;
|
)?;
|
||||||
|
|
||||||
// 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()
|
||||||
|
.set_pts(Some(gst::ClockTime::from_useconds(pkt.pts)));
|
||||||
let _ = self.src.push_buffer(buf);
|
let _ = self.src.push_buffer(buf);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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 hub‑port 1‑3
|
||||||
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 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
|
RULES
|
||||||
|
|
||||||
sudo udevadm control --reload
|
sudo udevadm control --reload
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -2,3 +2,4 @@
|
|||||||
|
|
||||||
pub mod video;
|
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 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 {
|
||||||
|
let list = list_gc311_devices()
|
||||||
.map_err(|e| Status::internal(format!("enum v4l2: {e}")))?;
|
.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<()> {
|
|||||||
/* auto‑cycle task */
|
/* auto‑cycle 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 3 s – 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
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
|
// 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"
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user