lesavka/server/src/main.rs

182 lines
6.1 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! navka-server — receive HidReport and write to /dev/hidg0
// sever/src/main.rs
#![forbid(unsafe_code)]
use anyhow::Context;
use futures_util::Stream;
use std::{pin::Pin, sync::Arc, time::Duration};
use tokio::{fs::{File, OpenOptions}, io::AsyncWriteExt, sync::Mutex};
use tokio_stream::{wrappers::ReceiverStream};
use tonic::{transport::Server, Request, Response, Status};
use tracing::{error, info, trace, warn};
use tracing_subscriber::{fmt, EnvFilter};
use udev::{Enumerator, MonitorBuilder};
use navka_server::{video, usb_reset};
use navka_common::navka::{
relay_server::{Relay, RelayServer},
KeyboardReport, MouseReport,
MonitorRequest, VideoPacket,
};
/*─────────────────── GC311 discovery ───────────────────*/
fn list_gc311_devices() -> anyhow::Result<Vec<String>> {
let mut v = Vec::new();
for entry in std::fs::read_dir("/sys/class/video4linux")? {
let path = entry?.path();
let name = std::fs::read_to_string(path.join("name"))?;
if name.to_lowercase().contains("gc311") {
v.push(
path.file_name()
.unwrap()
.to_string_lossy()
.replace("video", "/dev/video"),
);
}
}
v.sort();
Ok(v)
}
/// background task: whenever GC311 disappears, cycle USB port
async fn monitor_gc311_disconnect() -> anyhow::Result<()> {
let mut mon = MonitorBuilder::new()?
.match_subsystem("usb")?
.match_property("PRODUCT", "7ca/3311/*")? // vendor: 0x07ca, device 0x3311
.listen()?;
while let Some(ev) = mon.next() {
if ev.event_type() == udev::EventType::Remove {
if let (Some(bus), Some(dev)) = (ev.attribute_value("busnum"), ev.attribute_value("devnum")) {
usb_reset::cycle_port(bus.to_str().unwrap(), dev.to_str().unwrap());
}
}
}
Ok(())
}
/*─────────────────── tonic service ─────────────────────*/
struct Handler {
kb: Arc<Mutex<tokio::fs::File>>,
ms: Arc<Mutex<tokio::fs::File>>,
}
#[tonic::async_trait]
impl Relay for Handler {
type StreamKeyboardStream = ReceiverStream<Result<KeyboardReport, Status>>;
type StreamMouseStream = ReceiverStream<Result<MouseReport, Status>>;
type CaptureVideoStream = Pin<Box<dyn Stream<Item = Result<VideoPacket, Status>> + Send + Sync + 'static>>;
async fn stream_keyboard(
&self,
req: Request<tonic::Streaming<KeyboardReport>>,
) -> Result<Response<Self::StreamKeyboardStream>, Status> {
let (tx, rx) = tokio::sync::mpsc::channel(32);
let kb = self.kb.clone();
tokio::spawn(async move {
let mut s = req.into_inner();
while let Some(pkt) = s.next().await.transpose()? {
kb.lock().await.write_all(&pkt.data).await?;
tx.send(Ok(pkt)).await;//.ok(); // best-effort echo
}
Ok::<(), Status>(())
});
Ok(Response::new(ReceiverStream::new(rx)))
}
async fn stream_mouse(
&self,
req: Request<tonic::Streaming<MouseReport>>,
) -> Result<Response<Self::StreamMouseStream>, Status> {
let (tx, rx) = tokio::sync::mpsc::channel(4096); // higher burst
let ms = self.ms.clone();
tokio::spawn(async move {
let mut s = req.into_inner();
let mut boot_mode = true;
while let Some(pkt) = s.next().await.transpose()? {
loop {
match ms.lock().await.write_all(&pkt.data).await {
Ok(()) => {
trace!("🖱️ wrote {}", pkt.data.iter()
.map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "));
break;
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
tokio::time::sleep(Duration::from_micros(500)).await;
}
Err(e) => return Err(Status::internal(format!("hidg1: {e}"))),
}
}
let _ = tx.send(Ok(pkt)).await;
}
Ok::<(), Status>(())
});
Ok(Response::new(ReceiverStream::new(rx)))
}
async fn capture_video(
&self,
req: Request<MonitorRequest>,
) -> 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 dev = devs
.get(r.id as usize)
.ok_or_else(|| Status::invalid_argument(format!("monitor id {} absent", r.id)))?
.to_owned();
info!("🎥 streaming {dev} at ≤{}kb/s", r.max_bitrate);
let s = video::spawn_camera(&dev, r.id, r.max_bitrate)
.await
.map_err(|e| Status::internal(format!("{e:#?}")))?;
Ok(Response::new(Box::pin(s) as _))
}
}
/*─────────────────── main ──────────────────────────────*/
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() -> anyhow::Result<()> {
/* logging */
fmt().with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("navka_server=info")),
)
.init();
/* autocycle task */
tokio::spawn(async { monitor_gc311_disconnect().await.ok(); });
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?;
let handler = Handler {
kb: Arc::new(Mutex::new(kb)),
ms: Arc::new(Mutex::new(ms)),
};
println!("🌐 navka-server listening on 0.0.0.0:50051");
Server::builder()
.add_service(RelayServer::new(handler))
.serve(([0, 0, 0, 0], 50051).into())
.await?;
Ok(())
}