This commit is contained in:
Brad Stein 2025-06-26 14:05:23 -05:00
parent dd9df940db
commit 1110c59f1e
7 changed files with 147 additions and 97 deletions

View File

@ -6,7 +6,7 @@ use anyhow::Result;
use std::time::Duration; use std::time::Duration;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use tokio_stream::{wrappers::BroadcastStream, StreamExt};
use tonic::Request; use tonic::{Request, transport::Channel};
use tracing::{debug, error, info, warn, trace}; use tracing::{debug, error, info, warn, trace};
use winit::{ use winit::{
event_loop::EventLoopBuilder, event_loop::EventLoopBuilder,
@ -29,10 +29,10 @@ pub struct LesavkaClientApp {
impl LesavkaClientApp { impl LesavkaClientApp {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let dev_mode = std::env::var("NAVKA_DEV_MODE").is_ok(); let dev_mode = std::env::var("LESAVKA_DEV_MODE").is_ok();
let server_addr = std::env::args() let server_addr = std::env::args()
.nth(1) .nth(1)
.or_else(|| std::env::var("NAVKA_SERVER_ADDR").ok()) .or_else(|| std::env::var("LESAVKA_SERVER_ADDR").ok())
.unwrap_or_else(|| "http://127.0.0.1:50051".into()); .unwrap_or_else(|| "http://127.0.0.1:50051".into());
let (kbd_tx, _) = broadcast::channel::<KeyboardReport>(1024); let (kbd_tx, _) = broadcast::channel::<KeyboardReport>(1024);
@ -47,6 +47,19 @@ impl LesavkaClientApp {
} }
pub async fn run(&mut self) -> Result<()> { pub async fn run(&mut self) -> Result<()> {
// ---- build two channels ------------------------------------------------
let hid_ep: Channel = Channel::from_shared(self.server_addr.clone())
.unwrap()
.tcp_nodelay(true)
.concurrency_limit(1)
.http2_keep_alive_interval(Duration::from_secs(15))
.connect_lazy();
let vid_ep: Channel = Channel::from_shared(self.server_addr.clone())
.unwrap()
.tcp_nodelay(true)
.connect_lazy();
/* detach the aggregator before spawn so `self` is not moved */ /* detach the aggregator before spawn so `self` is not moved */
let aggregator = self.aggregator.take().expect("InputAggregator present"); let aggregator = self.aggregator.take().expect("InputAggregator present");
let agg_task = tokio::spawn(async move { let agg_task = tokio::spawn(async move {
@ -55,8 +68,8 @@ impl LesavkaClientApp {
}); });
/* two networking tasks */ /* two networking tasks */
let kbd_loop = self.stream_loop_keyboard(); let kbd_loop = self.stream_loop_keyboard(hid_ep.clone());
let mou_loop = self.stream_loop_mouse(); let mou_loop = self.stream_loop_mouse(hid_ep.clone());
/* optional suicide timer */ /* optional suicide timer */
let suicide = async { let suicide = async {
@ -93,7 +106,7 @@ impl LesavkaClientApp {
}); });
}); });
let vid_loop = Self::video_loop(self.server_addr.clone(), video_tx); let vid_loop = Self::video_loop(vid_ep.clone(), video_tx);
tokio::select! { tokio::select! {
_ = kbd_loop => unreachable!(), _ = kbd_loop => unreachable!(),
@ -109,82 +122,79 @@ impl LesavkaClientApp {
} }
/*──────────────── keyboard stream ───────────────*/ /*──────────────── keyboard stream ───────────────*/
async fn stream_loop_keyboard(&self) { async fn stream_loop_keyboard(&self, ep: Channel) {
loop { loop {
info!("⌨️ connect {}", self.server_addr); info!("⌨️ connect {}", self.server_addr);
let mut cli = match RelayClient::connect(self.server_addr.clone()).await { // let mut cli = match RelayClient::connect(self.server_addr.clone()).await {
Ok(c) => c, // Ok(c) => c,
Err(e) => { error!("connect: {e}"); Self::delay().await; continue } // Err(e) => { error!("connect: {e}"); Self::delay().await; continue }
}; // };
let mut cli = RelayClient::new(ep.clone());
let outbound = BroadcastStream::new(self.kbd_tx.subscribe()).filter_map(|r| r.ok()); let outbound = BroadcastStream::new(self.kbd_tx.subscribe()).filter_map(|r| r.ok());
let resp = match cli.stream_keyboard(Request::new(outbound)).await { let resp = match cli.stream_keyboard(Request::new(outbound)).await {
Ok(r) => r, Err(e) => { error!("stream_keyboard: {e}"); Self::delay().await; continue } Ok(r) => r, Err(e) => { error!("stream_keyboard: {e}"); Self::delay().await; continue }
}; };
let mut inbound = resp.into_inner(); // let mut inbound = resp.into_inner();
while let Some(m) = inbound.message().await.transpose() { // while let Some(m) = inbound.message().await.transpose() {
match m { // match m {
Ok(r) => trace!("kbd echo {}B", r.data.len()), // Ok(r) => trace!("kbd echo {}B", r.data.len()),
Err(e) => { error!("kbd inbound: {e}"); break } // Err(e) => { error!("kbd inbound: {e}"); break }
} // }
} // }
drop(resp);
warn!("⌨️ disconnected"); warn!("⌨️ disconnected");
Self::delay().await; Self::delay().await;
} }
} }
/*──────────────── mouse stream ──────────────────*/ /*──────────────── mouse stream ──────────────────*/
async fn stream_loop_mouse(&self) { async fn stream_loop_mouse(&self, ep: Channel) {
loop { loop {
info!("🖱️ connect {}", self.server_addr); info!("🖱️ connect {}", self.server_addr);
let mut cli = match RelayClient::connect(self.server_addr.clone()).await { // let mut cli = match RelayClient::connect(self.server_addr.clone()).await {
Ok(c) => c, // Ok(c) => c,
Err(e) => { error!("connect: {e}"); Self::delay().await; continue } // Err(e) => { error!("connect: {e}"); Self::delay().await; continue }
}; // };
let mut cli = RelayClient::new(ep.clone());
let outbound = BroadcastStream::new(self.mou_tx.subscribe()).filter_map(|r| r.ok()); let outbound = BroadcastStream::new(self.mou_tx.subscribe()).filter_map(|r| r.ok());
let resp = match cli.stream_mouse(Request::new(outbound)).await { let resp = match cli.stream_mouse(Request::new(outbound)).await {
Ok(r) => r, Err(e) => { error!("stream_mouse: {e}"); Self::delay().await; continue } Ok(r) => r, Err(e) => { error!("stream_mouse: {e}"); Self::delay().await; continue }
}; };
let mut inbound = resp.into_inner(); // let mut inbound = resp.into_inner();
while let Some(m) = inbound.message().await.transpose() { // while let Some(m) = inbound.message().await.transpose() {
match m { // match m {
Ok(r) => trace!("mouse echo {}B", r.data.len()), // Ok(r) => trace!("mouse echo {}B", r.data.len()),
Err(e) => { error!("mouse inbound: {e}"); break } // Err(e) => { error!("mouse inbound: {e}"); break }
} // }
} // }
drop(resp);
warn!("🖱️ disconnected"); warn!("🖱️ disconnected");
Self::delay().await; Self::delay().await;
} }
} }
/*──────────────── monitor stream ────────────────*/ /*──────────────── monitor stream ────────────────*/
async fn video_loop(addr: String, tx: tokio::sync::mpsc::UnboundedSender<VideoPacket>) { async fn video_loop(ep: Channel, tx: tokio::sync::mpsc::UnboundedSender<VideoPacket>) {
loop { loop {
match RelayClient::connect(addr.clone()).await { let mut cli = RelayClient::new(ep.clone());
Ok(mut cli) => { for monitor_id in 0..=1 {
for monitor_id in 0..=1 { let req = MonitorRequest { id: monitor_id, max_bitrate: 6_000 };
let req = MonitorRequest { id: monitor_id, max_bitrate: 6_000 }; if let Ok(mut stream) = cli.capture_video(Request::new(req)).await {
match cli.capture_video(Request::new(req)).await { while let Some(pkt) = stream.get_mut().message().await.transpose() {
Ok(mut stream) => { match pkt {
while let Some(pkt) = stream.get_mut().message().await.transpose() { Ok(p) => { let _ = tx.send(p); }
match pkt { Err(e) => { error!("video {monitor_id}: {e}"); break }
Ok(p) => { let _ = tx.send(p); },
Err(e) => { error!("video {monitor_id}: {e}"); break }
}
}
} }
Err(e) => error!("video {monitor_id}: {e}"),
} }
} }
} }
Err(e) => error!("video connect: {e}"),
}
tokio::time::sleep(Duration::from_secs(2)).await; tokio::time::sleep(Duration::from_secs(2)).await;
} }
} }
#[inline(always)] #[inline(always)]
async fn delay() { tokio::time::sleep(Duration::from_secs(1)).await; } async fn delay() { tokio::time::sleep(Duration::from_secs(1)).await; }

View File

@ -3,7 +3,7 @@
use std::{collections::HashSet, sync::atomic::{AtomicU32, Ordering}}; use std::{collections::HashSet, sync::atomic::{AtomicU32, Ordering}};
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, warn}; use tracing::{debug, error, warn, trace};
use lesavka_common::lesavka::KeyboardReport; use lesavka_common::lesavka::KeyboardReport;

View File

@ -2,14 +2,18 @@
use evdev::{Device, EventType, InputEvent, KeyCode, RelativeAxisCode}; use evdev::{Device, EventType, InputEvent, KeyCode, RelativeAxisCode};
use tokio::sync::broadcast::{self, Sender}; use tokio::sync::broadcast::{self, Sender};
use tracing::{debug, error, warn}; use std::time::{Duration, Instant};
use tracing::{debug, error, warn, trace};
use lesavka_common::lesavka::MouseReport; use lesavka_common::lesavka::MouseReport;
const SEND_INTERVAL: Duration = Duration::from_micros(2000);
pub struct MouseAggregator { pub struct MouseAggregator {
dev: Device, dev: Device,
tx: Sender<MouseReport>, tx: Sender<MouseReport>,
dev_mode: bool, dev_mode: bool,
next_send: Instant,
buttons: u8, buttons: u8,
last_buttons: u8, last_buttons: u8,
@ -20,7 +24,7 @@ pub struct MouseAggregator {
impl MouseAggregator { impl MouseAggregator {
pub fn new(dev: Device, dev_mode: bool, tx: Sender<MouseReport>) -> Self { pub fn new(dev: Device, dev_mode: bool, tx: Sender<MouseReport>) -> Self {
Self { dev, tx, dev_mode, buttons:0, last_buttons:0, dx:0, dy:0, wheel:0 } Self { dev, tx, dev_mode, next_send: Instant::now(), buttons:0, last_buttons:0, dx:0, dy:0, wheel:0 }
} }
#[inline] fn slog(&self, f: impl FnOnce()) { if self.dev_mode { f() } } #[inline] fn slog(&self, f: impl FnOnce()) { if self.dev_mode { f() } }
@ -60,9 +64,8 @@ impl MouseAggregator {
} }
fn flush(&mut self) { fn flush(&mut self) {
if self.dx==0 && self.dy==0 && self.wheel==0 && self.buttons==self.last_buttons { if Instant::now() < self.next_send { return; }
return; self.next_send += SEND_INTERVAL;
}
let pkt = [ let pkt = [
self.buttons, self.buttons,

View File

@ -21,7 +21,7 @@ fn ensure_runtime_dir() {
} }
} }
#[tokio::main] #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> { async fn main() -> Result<()> {
ensure_runtime_dir(); ensure_runtime_dir();
@ -41,7 +41,7 @@ async fn main() -> Result<()> {
.with_thread_ids(true) .with_thread_ids(true)
.with_file(true); .with_file(true);
let dev_mode = env::var("NAVKA_DEV_MODE").is_ok(); let dev_mode = env::var("LESAVKA_DEV_MODE").is_ok();
let mut _guard: Option<WorkerGuard> = None; // keep guard alive let mut _guard: Option<WorkerGuard> = None; // keep guard alive
/*------------- subscriber setup -----------------------------------*/ /*------------- subscriber setup -----------------------------------*/

View File

@ -25,14 +25,14 @@ impl MonitorWindow {
)?; )?;
// appsrc -> decode -> convert -> autovideosink // appsrc -> decode -> convert -> autovideosink
let desc = if std::env::var_os("XDG_RUNTIME_DIR").is_some() { let desc = if std::env::var_os("LESAVKA_HW_DEC").is_some() {
// graphical "appsrc name=src is-live=true format=time do-timestamp=true ! queue ! \
h264parse ! vaapih264dec low-latency=true ! videoconvert ! \
autovideosink sync=false"
} else {
// fallback
"appsrc name=src is-live=true format=time do-timestamp=true ! \ "appsrc name=src is-live=true format=time do-timestamp=true ! \
queue ! h264parse ! decodebin ! videoconvert ! autovideosink sync=false" queue ! h264parse ! decodebin ! videoconvert ! autovideosink sync=false"
} else {
// headless / debugging over ssh
"appsrc name=src is-live=true format=time do-timestamp=true ! \
fakesink sync=false"
}; };
let pipeline = gst::parse::launch(desc)? let pipeline = gst::parse::launch(desc)?

View File

@ -38,8 +38,8 @@ User=root
Group=root Group=root
Environment=RUST_LOG=debug Environment=RUST_LOG=debug
Environment=NAVKA_DEV_MODE=1 Environment=LESAVKA_DEV_MODE=1
Environment=NAVKA_SERVER_ADDR=http://64.25.10.31:50051 Environment=LESAVKA_SERVER_ADDR=http://64.25.10.31:50051
ExecStart=/usr/local/bin/lesavka-client ExecStart=/usr/local/bin/lesavka-client
Restart=no Restart=no

View File

@ -9,11 +9,10 @@ use tokio::{fs::{OpenOptions}, io::AsyncWriteExt, sync::Mutex};
use tokio_stream::{wrappers::ReceiverStream}; use tokio_stream::{wrappers::ReceiverStream};
use tonic::{transport::Server, Request, Response, Status}; use tonic::{transport::Server, Request, Response, Status};
use anyhow::Context as _; use anyhow::Context as _;
use tracing::{info, trace, warn, error}; use tracing::{info, trace, error};
use tracing_subscriber::{filter::EnvFilter, fmt, prelude::*}; use tracing_subscriber::{filter::EnvFilter, fmt, prelude::*};
use tracing_appender::non_blocking; use tracing_appender::non_blocking;
use tracing_appender::non_blocking::WorkerGuard; use tracing_appender::non_blocking::WorkerGuard;
use udev::{MonitorBuilder};
use lesavka_server::{usb_gadget::UsbGadget, video}; use lesavka_server::{usb_gadget::UsbGadget, video};
@ -144,31 +143,48 @@ impl Relay for Handler {
.map_err(|e| Status::internal(e.to_string()))?; .map_err(|e| Status::internal(e.to_string()))?;
tokio::time::sleep(Duration::from_millis(500)).await; tokio::time::sleep(Duration::from_millis(500)).await;
} }
let (tx, rx) = tokio::sync::mpsc::channel(32); let (tx, _rx) =
tokio::sync::mpsc::channel::<Result<KeyboardReport, Status>>(32);
let kb = self.kb.clone(); let kb = self.kb.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut s = req.into_inner(); let mut s = req.into_inner();
while let Some(pkt) = s.next().await.transpose()? { while let Some(pkt) = s.next().await.transpose()? {
// kb.lock().await.write_all(&pkt.data).await?; // kb.lock().await.write_all(&pkt.data).await?;
loop { const SPINS: usize = 20;
match kb.lock().await.write_all(&pkt.data).await { for _ in 0..SPINS {
Ok(()) => { match kb.lock().await.write(&pkt.data).await {
trace!("⌨️ wrote {}", pkt.data.iter() // Ok(n) if n == pkt.data.len() => {
.map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" ")); // trace!("⌨️ wrote {}", pkt.data.iter()
// .map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "));
// break;
// },
// Ok(_) | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
// std::hint::spin_loop();
// continue; // try again
// }
// Err(e)
// if matches!(e.raw_os_error(),
// Some(libc::EBUSY) | // still opening
// Some(libc::ENODEV) | // gadget notyet configured
// Some(libc::EPIPE) | // host vanished
// Some(libc::EINVAL) | // host hasnt accepted EP config yet
// Some(libc::EAGAIN)) // nonblocking
// => {
// tokio::time::sleep(Duration::from_millis(10)).await;
// continue;
// }
// Err(e) => return Err(Status::internal(e.to_string())),
Ok(n) if n == pkt.data.len() => { // success
trace!("⌨️ wrote {}", pkt.data.iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "));
break; break;
}, }
Err(e) Ok(_) => continue,
if matches!(e.raw_os_error(), Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
Some(libc::EBUSY) | // still opening std::hint::spin_loop();
Some(libc::ENODEV) | // gadget notyet configured
Some(libc::EPIPE) | // host vanished
Some(libc::EINVAL) | // host hasnt accepted EP config yet
Some(libc::EAGAIN)) // nonblocking
=> {
tokio::time::sleep(Duration::from_millis(10)).await;
continue; continue;
} }
Err(ref e) if e.kind() == std::io::ErrorKind::BrokenPipe => break,
Err(e) => return Err(Status::internal(e.to_string())), Err(e) => return Err(Status::internal(e.to_string())),
} }
} }
@ -177,14 +193,17 @@ impl Relay for Handler {
Ok::<(), Status>(()) Ok::<(), Status>(())
}); });
Ok(Response::new(ReceiverStream::new(rx))) let (_noop_tx, empty_rx) =
tokio::sync::mpsc::channel::<Result<KeyboardReport, Status>>(1);
Ok(Response::new(ReceiverStream::new(empty_rx)))
} }
async fn stream_mouse( async fn stream_mouse(
&self, &self,
req: Request<tonic::Streaming<MouseReport>>, req: Request<tonic::Streaming<MouseReport>>,
) -> Result<Response<Self::StreamMouseStream>, Status> { ) -> Result<Response<Self::StreamMouseStream>, Status> {
let (tx, rx) = tokio::sync::mpsc::channel(4096); // higher burst let (tx, _rx) =
tokio::sync::mpsc::channel::<Result<MouseReport, Status>>(4096);
let ms = self.ms.clone(); let ms = self.ms.clone();
tokio::spawn(async move { tokio::spawn(async move {
@ -192,23 +211,38 @@ impl Relay for Handler {
let mut boot_mode = true; let mut boot_mode = true;
while let Some(pkt) = s.next().await.transpose()? { while let Some(pkt) = s.next().await.transpose()? {
loop { loop {
match ms.lock().await.write_all(&pkt.data).await { match ms.lock().await.write(&pkt.data).await {
Ok(()) => { // Ok(n) if n == pkt.data.len() => {
trace!("🖱️ wrote {}", pkt.data.iter() // trace!("🖱️ wrote {}", pkt.data.iter()
.map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" ")); // .map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "));
// break;
// }
// Ok(_) | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
// std::hint::spin_loop();
// continue; // try again
// }
// Err(e)
// if matches!(e.raw_os_error(),
// Some(libc::EBUSY) | // still opening
// Some(libc::ENODEV) | // gadget notyet configured
// Some(libc::EPIPE) | // host vanished
// Some(libc::EINVAL) | // host hasnt accepted EP config yet
// Some(libc::EAGAIN)) // nonblocking
// => {
// tokio::time::sleep(Duration::from_millis(10)).await;
// continue;
// }
// Err(e) => return Err(Status::internal(e.to_string())),
Ok(n) if n == pkt.data.len() => { // success
trace!("⌨️ wrote {}", pkt.data.iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "));
break; break;
} }
Err(e) Ok(_) => continue,
if matches!(e.raw_os_error(), Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
Some(libc::EBUSY) | // still opening std::hint::spin_loop();
Some(libc::ENODEV) | // gadget notyet configured
Some(libc::EPIPE) | // host vanished
Some(libc::EINVAL) | // host hasnt accepted EP config yet
Some(libc::EAGAIN)) // nonblocking
=> {
tokio::time::sleep(Duration::from_millis(10)).await;
continue; continue;
} }
Err(ref e) if e.kind() == std::io::ErrorKind::BrokenPipe => break,
Err(e) => return Err(Status::internal(e.to_string())), Err(e) => return Err(Status::internal(e.to_string())),
} }
} }
@ -217,7 +251,9 @@ impl Relay for Handler {
Ok::<(), Status>(()) Ok::<(), Status>(())
}); });
Ok(Response::new(ReceiverStream::new(rx))) let (_noop_tx, empty_rx) =
tokio::sync::mpsc::channel::<Result<MouseReport, Status>>(1);
Ok(Response::new(ReceiverStream::new(empty_rx)))
} }
async fn capture_video( async fn capture_video(
@ -294,6 +330,7 @@ async fn main() -> anyhow::Result<()> {
info!("🌐 lesavkaserver listening on 0.0.0.0:50051"); info!("🌐 lesavkaserver listening on 0.0.0.0:50051");
if let Err(e) = Server::builder() if let Err(e) = Server::builder()
.tcp_nodelay(true)
.add_service(RelayServer::new(handler)) .add_service(RelayServer::new(handler))
.serve(([0, 0, 0, 0], 50051).into()) .serve(([0, 0, 0, 0], 50051).into())
.await .await