lesavka/server/src/main.rs

675 lines
23 KiB
Rust
Raw Normal View History

2026-01-08 23:58:19 -03:00
//! lesavka-server - gadget cycle guarded by env
2025-06-27 06:56:08 -05:00
// server/src/main.rs
2025-06-01 16:04:00 -05:00
#![forbid(unsafe_code)]
2025-06-01 13:31:22 -05:00
2025-06-25 18:23:38 -05:00
use anyhow::Context as _;
2025-06-27 06:56:08 -05:00
use futures_util::{Stream, StreamExt};
2026-01-06 12:11:48 -03:00
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
2026-01-06 11:48:36 -03:00
use std::{backtrace::Backtrace, panic, pin::Pin, sync::Arc};
use tokio::{fs::OpenOptions, io::AsyncWriteExt, process::Command, sync::Mutex};
2025-06-27 06:56:08 -05:00
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::Server;
2025-11-30 16:16:03 -03:00
use tonic::{Request, Response, Status};
use tonic_reflection::server::Builder as ReflBuilder;
use tracing::{debug, error, info, trace, warn};
2025-06-25 16:52:26 -05:00
use tracing_appender::non_blocking::WorkerGuard;
2025-11-30 16:16:03 -03:00
use tracing_subscriber::{filter::EnvFilter, fmt, prelude::*};
2025-06-21 05:21:57 -05:00
2025-06-23 07:18:26 -05:00
use lesavka_common::lesavka::{
AudioPacket, Empty, KeyboardReport, MonitorRequest, MouseReport, PasteReply, PasteRequest,
ResetUsbReply, VideoPacket,
2025-06-01 21:26:57 -05:00
relay_server::{Relay, RelayServer},
2025-06-01 16:04:00 -05:00
};
2025-06-02 20:24:00 -05:00
use lesavka_server::{audio, camera, gadget::UsbGadget, handshake::HandshakeSvc, paste, video};
2025-06-27 06:56:08 -05:00
/*──────────────── constants ────────────────*/
2025-07-04 10:17:01 -05:00
const VERSION: &str = env!("CARGO_PKG_VERSION");
const PKG_NAME: &str = env!("CARGO_PKG_NAME");
2025-06-27 06:56:08 -05:00
/*──────────────── logging ───────────────────*/
2025-06-25 16:52:26 -05:00
fn init_tracing() -> anyhow::Result<WorkerGuard> {
2025-06-25 16:23:50 -05:00
let file = std::fs::OpenOptions::new()
2025-11-30 16:16:03 -03:00
.create(true)
.truncate(true)
.write(true)
2025-06-25 16:23:50 -05:00
.open("/tmp/lesavka-server.log")?;
2025-06-27 06:56:08 -05:00
let (file_writer, guard) = tracing_appender::non_blocking(file);
2025-06-25 16:23:50 -05:00
2025-11-30 16:16:03 -03:00
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("lesavka_server=info,lesavka_server::video=warn"));
2025-06-28 18:51:13 -05:00
let filter_str = env_filter.to_string();
2025-06-25 16:23:50 -05:00
tracing_subscriber::registry()
2025-06-28 18:51:13 -05:00
.with(env_filter)
2025-11-30 16:16:03 -03:00
.with(fmt::layer().with_target(true).with_thread_ids(true))
2025-06-28 18:51:13 -05:00
.with(
fmt::layer()
.with_writer(file_writer)
.with_ansi(false)
.with_target(true)
.with_level(true),
)
2025-06-25 16:23:50 -05:00
.init();
2025-06-28 18:51:13 -05:00
tracing::info!("📜 effective RUST_LOG = \"{}\"", filter_str);
2025-06-25 16:52:26 -05:00
Ok(guard)
2025-06-25 16:23:50 -05:00
}
2025-06-27 06:56:08 -05:00
/*──────────────── helpers ───────────────────*/
2025-06-25 21:15:24 -05:00
async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
2025-11-30 16:16:03 -03:00
for attempt in 1..=200 {
// ≈10s
2025-06-25 19:04:11 -05:00
match OpenOptions::new()
2025-11-30 16:16:03 -03:00
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)
.await
2025-06-25 19:04:11 -05:00
{
Ok(f) => {
2025-06-25 21:15:24 -05:00
info!("✅ {path} opened on attempt #{attempt}");
2025-06-25 19:04:11 -05:00
return Ok(f);
}
2025-06-25 22:24:58 -05:00
Err(e) if e.raw_os_error() == Some(libc::EBUSY) => {
2025-06-27 06:56:08 -05:00
trace!("⏳ {path} busy… retry #{attempt}");
2025-06-25 21:15:24 -05:00
tokio::time::sleep(Duration::from_millis(50)).await;
2025-06-25 20:00:34 -05:00
}
2025-06-27 06:56:08 -05:00
Err(e) => return Err(e).with_context(|| format!("opening {path}")),
2025-06-25 19:04:11 -05:00
}
}
2025-06-27 06:56:08 -05:00
Err(anyhow::anyhow!("timeout waiting for {path}"))
2025-06-25 19:04:11 -05:00
}
2026-01-08 23:58:19 -03:00
fn allow_gadget_cycle() -> bool {
std::env::var("LESAVKA_ALLOW_GADGET_CYCLE").is_ok()
}
2026-01-06 04:38:41 -03:00
async fn recover_hid_if_needed(
err: &std::io::Error,
gadget: UsbGadget,
kb: Arc<Mutex<tokio::fs::File>>,
ms: Arc<Mutex<tokio::fs::File>>,
did_cycle: Arc<AtomicBool>,
) {
let code = err.raw_os_error();
2026-01-28 17:52:00 -03:00
let should_recover = matches!(
code,
Some(libc::ENOTCONN) | Some(libc::ESHUTDOWN) | Some(libc::EPIPE)
);
2026-01-06 04:38:41 -03:00
if !should_recover {
return;
}
if did_cycle
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return;
}
2026-01-08 23:58:19 -03:00
let allow_cycle = allow_gadget_cycle();
2026-01-06 04:38:41 -03:00
tokio::spawn(async move {
2026-01-08 23:58:19 -03:00
if allow_cycle {
warn!("🔁 HID transport down (errno={code:?}) - cycling gadget");
match tokio::task::spawn_blocking(move || gadget.cycle()).await {
Ok(Ok(())) => info!("✅ USB gadget cycle complete (auto-recover)"),
Ok(Err(e)) => error!("💥 USB gadget cycle failed: {e:#}"),
Err(e) => error!("💥 USB gadget cycle task panicked: {e:#}"),
}
} else {
warn!(
"🔒 HID transport down (errno={code:?}) - gadget cycle disabled; set LESAVKA_ALLOW_GADGET_CYCLE=1 to enable"
);
2026-01-06 04:38:41 -03:00
}
if let Err(e) = async {
let kb_new = open_with_retry("/dev/hidg0").await?;
let ms_new = open_with_retry("/dev/hidg1").await?;
*kb.lock().await = kb_new;
*ms.lock().await = ms_new;
Ok::<(), anyhow::Error>(())
}
.await
{
error!("💥 HID reopen failed: {e:#}");
}
tokio::time::sleep(Duration::from_secs(2)).await;
did_cycle.store(false, Ordering::SeqCst);
});
}
async fn open_voice_with_retry(uac_dev: &str) -> anyhow::Result<audio::Voice> {
let attempts = std::env::var("LESAVKA_MIC_INIT_ATTEMPTS")
.ok()
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(5)
.max(1);
let delay_ms = std::env::var("LESAVKA_MIC_INIT_DELAY_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(250);
let mut last_err: Option<anyhow::Error> = None;
for attempt in 1..=attempts {
match audio::Voice::new(uac_dev).await {
Ok(v) => {
if attempt > 1 {
info!(%uac_dev, attempt, "🎤 microphone sink recovered");
}
return Ok(v);
}
Err(e) => {
warn!(%uac_dev, attempt, "⚠️ microphone sink init failed: {e:#}");
last_err = Some(e);
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
}
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("microphone sink init failed")))
}
2025-12-01 03:34:01 -03:00
/// Pick the UVC gadget video node.
/// Priority: 1) `LESAVKA_UVC_DEV` override; 2) first `video_output` node.
/// Returns an error when nothing matches instead of guessing a capture card.
fn pick_uvc_device() -> anyhow::Result<String> {
if let Ok(path) = std::env::var("LESAVKA_UVC_DEV") {
return Ok(path);
}
2026-01-06 12:11:48 -03:00
let ctrl = UsbGadget::find_controller().ok();
if let Some(ctrl) = ctrl.as_deref() {
let by_path = format!("/dev/v4l/by-path/platform-{ctrl}-video-index0");
if Path::new(&by_path).exists() {
return Ok(by_path);
}
}
2025-12-01 03:34:01 -03:00
// walk /dev/video* via udev and look for an outputcapable node (gadget exposes one)
2026-01-06 12:11:48 -03:00
let mut fallback: Option<String> = None;
2025-12-01 03:34:01 -03:00
if let Ok(mut en) = udev::Enumerator::new() {
let _ = en.match_subsystem("video4linux");
if let Ok(devs) = en.scan_devices() {
for dev in devs {
let caps = dev
.property_value("ID_V4L_CAPABILITIES")
.and_then(|v| v.to_str())
.unwrap_or_default();
2026-01-06 12:11:48 -03:00
if !caps.contains(":video_output:") {
continue;
}
let Some(node) = dev.devnode() else { continue };
let node = node.to_string_lossy().into_owned();
let product = dev
.property_value("ID_V4L_PRODUCT")
.and_then(|v| v.to_str())
.unwrap_or_default();
let path = dev
.property_value("ID_PATH")
.and_then(|v| v.to_str())
.unwrap_or_default();
if let Some(ctrl) = ctrl.as_deref() {
if product == ctrl || path.contains(ctrl) {
return Ok(node);
2025-12-01 03:34:01 -03:00
}
}
2026-01-06 12:11:48 -03:00
if fallback.is_none() {
fallback = Some(node);
}
2025-12-01 03:34:01 -03:00
}
}
}
2026-01-06 12:11:48 -03:00
if let Some(node) = fallback {
return Ok(node);
}
2025-12-01 03:34:01 -03:00
Err(anyhow::anyhow!(
"no video_output v4l2 node found; set LESAVKA_UVC_DEV"
))
}
2026-01-06 11:48:36 -03:00
fn uvc_ctrl_bin() -> String {
std::env::var("LESAVKA_UVC_CTRL_BIN")
.unwrap_or_else(|_| "/usr/local/bin/lesavka-uvc".to_string())
}
fn spawn_uvc_control(bin: &str, uvc_dev: &str) -> anyhow::Result<tokio::process::Child> {
2026-01-06 04:38:41 -03:00
Command::new(bin)
.arg("--device")
.arg(uvc_dev)
.spawn()
.context("spawning lesavka-uvc")
}
2026-01-06 11:48:36 -03:00
async fn supervise_uvc_control(bin: String) {
let mut waiting_logged = false;
loop {
let uvc_dev = match pick_uvc_device() {
Ok(dev) => {
if waiting_logged {
info!(%dev, "📷 UVC device discovered");
waiting_logged = false;
}
dev
}
Err(e) => {
if !waiting_logged {
warn!("⚠️ UVC device not ready: {e:#}");
waiting_logged = true;
}
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
};
match spawn_uvc_control(&bin, &uvc_dev) {
Ok(mut child) => {
info!(%uvc_dev, "📷 UVC control helper started");
match child.wait().await {
Ok(status) => {
warn!(%uvc_dev, "⚠️ lesavka-uvc exited: {status}");
}
Err(e) => {
warn!(%uvc_dev, "⚠️ lesavka-uvc wait failed: {e:#}");
}
}
}
Err(e) => {
warn!(%uvc_dev, "⚠️ failed to start lesavka-uvc: {e:#}");
}
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
}
2025-06-27 06:56:08 -05:00
/*──────────────── Handler ───────────────────*/
2025-06-02 20:41:36 -05:00
struct Handler {
2025-06-17 20:54:31 -05:00
kb: Arc<Mutex<tokio::fs::File>>,
ms: Arc<Mutex<tokio::fs::File>>,
2025-06-25 07:46:50 -05:00
gadget: UsbGadget,
2026-01-06 04:38:41 -03:00
did_cycle: Arc<AtomicBool>,
camera_rt: Arc<CameraRuntime>,
}
struct CameraRelaySlot {
cfg: camera::CameraConfig,
relay: Arc<video::CameraRelay>,
}
struct CameraRuntime {
generation: AtomicU64,
slot: Mutex<Option<CameraRelaySlot>>,
}
impl CameraRuntime {
fn new() -> Self {
Self {
generation: AtomicU64::new(0),
slot: Mutex::new(None),
}
}
async fn activate(
&self,
cfg: &camera::CameraConfig,
) -> Result<(u64, Arc<video::CameraRelay>), Status> {
let session_id = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
let mut slot = self.slot.lock().await;
let mut reused = false;
let relay = if let Some(existing) = slot.as_ref() {
if camera_cfg_eq(&existing.cfg, cfg) {
reused = true;
existing.relay.clone()
} else {
self.make_relay(cfg)?
}
} else {
self.make_relay(cfg)?
};
if !reused {
*slot = Some(CameraRelaySlot {
cfg: cfg.clone(),
relay: relay.clone(),
});
info!(
session_id,
output = cfg.output.as_str(),
codec = cfg.codec.as_str(),
width = cfg.width,
height = cfg.height,
fps = cfg.fps,
"🎥 camera relay (re)created"
);
} else {
info!(session_id, "🎥 camera relay reused");
}
Ok((session_id, relay))
}
fn is_active(&self, session_id: u64) -> bool {
self.generation.load(Ordering::Relaxed) == session_id
}
fn make_relay(&self, cfg: &camera::CameraConfig) -> Result<Arc<video::CameraRelay>, Status> {
let relay = match cfg.output {
camera::CameraOutput::Uvc => {
if std::env::var("LESAVKA_DISABLE_UVC").is_ok() {
return Err(Status::failed_precondition(
"UVC output disabled (LESAVKA_DISABLE_UVC set)",
));
}
let uvc = pick_uvc_device().map_err(|e| Status::internal(format!("{e:#}")))?;
info!(%uvc, "🎥 stream_camera using UVC sink");
video::CameraRelay::new_uvc(0, &uvc, cfg)
.map_err(|e| Status::internal(format!("{e:#}")))?
}
camera::CameraOutput::Hdmi => video::CameraRelay::new_hdmi(0, cfg)
.map_err(|e| Status::internal(format!("{e:#}")))?,
};
Ok(Arc::new(relay))
}
}
fn camera_cfg_eq(a: &camera::CameraConfig, b: &camera::CameraConfig) -> bool {
if a.output != b.output
|| a.codec != b.codec
|| a.width != b.width
|| a.height != b.height
|| a.fps != b.fps
{
return false;
}
match (&a.hdmi, &b.hdmi) {
(Some(ha), Some(hb)) => ha.name == hb.name && ha.id == hb.id,
(None, None) => true,
_ => false,
}
2025-06-24 23:48:06 -05:00
}
impl Handler {
2025-06-27 06:56:08 -05:00
async fn new(gadget: UsbGadget) -> anyhow::Result<Self> {
2026-01-08 23:58:19 -03:00
if allow_gadget_cycle() {
2025-06-27 06:56:08 -05:00
info!("🛠️ Initial USB reset…");
2025-11-30 16:16:03 -03:00
let _ = gadget.cycle(); // ignore failure - may boot without host
2025-06-26 21:49:29 -05:00
} else {
2026-01-28 17:52:00 -03:00
info!(
"🔒 gadget cycle disabled at startup (set LESAVKA_ALLOW_GADGET_CYCLE=1 to enable)"
);
2025-06-26 21:49:29 -05:00
}
2025-06-25 16:23:50 -05:00
2025-06-27 06:56:08 -05:00
info!("🛠️ opening HID endpoints …");
2025-06-25 21:15:24 -05:00
let kb = open_with_retry("/dev/hidg0").await?;
let ms = open_with_retry("/dev/hidg1").await?;
2025-06-25 20:00:34 -05:00
info!("✅ HID endpoints ready");
2025-06-26 21:49:29 -05:00
Ok(Self {
kb: Arc::new(Mutex::new(kb)),
ms: Arc::new(Mutex::new(ms)),
gadget,
2026-01-06 04:38:41 -03:00
did_cycle: Arc::new(AtomicBool::new(false)),
camera_rt: Arc::new(CameraRuntime::new()),
2025-06-26 21:49:29 -05:00
})
}
2025-07-05 12:45:08 -05:00
async fn reopen_hid(&self) -> anyhow::Result<()> {
let kb_new = open_with_retry("/dev/hidg0").await?;
let ms_new = open_with_retry("/dev/hidg1").await?;
*self.kb.lock().await = kb_new;
*self.ms.lock().await = ms_new;
Ok(())
}
2025-06-01 16:04:00 -05:00
}
2025-06-01 13:31:22 -05:00
2025-06-27 06:56:08 -05:00
/*──────────────── gRPC service ─────────────*/
2025-06-02 20:41:36 -05:00
#[tonic::async_trait]
2025-06-16 22:14:52 -05:00
impl Relay for Handler {
2025-06-28 15:45:11 -05:00
/* existing streams ─ unchanged, except: no more auto-reset */
2025-11-30 16:16:03 -03:00
type StreamKeyboardStream = ReceiverStream<Result<KeyboardReport, Status>>;
type StreamMouseStream = ReceiverStream<Result<MouseReport, Status>>;
type CaptureVideoStream = Pin<Box<dyn Stream<Item = Result<VideoPacket, Status>> + Send>>;
type CaptureAudioStream = Pin<Box<dyn Stream<Item = Result<AudioPacket, Status>> + Send>>;
2025-06-30 19:35:38 -05:00
type StreamMicrophoneStream = ReceiverStream<Result<Empty, Status>>;
2025-07-03 08:19:59 -05:00
type StreamCameraStream = ReceiverStream<Result<Empty, Status>>;
2025-06-01 13:31:22 -05:00
2025-06-17 08:17:23 -05:00
async fn stream_keyboard(
2025-06-01 13:31:22 -05:00
&self,
2025-06-17 08:17:23 -05:00
req: Request<tonic::Streaming<KeyboardReport>>,
) -> Result<Response<Self::StreamKeyboardStream>, Status> {
2025-06-27 06:56:08 -05:00
let (tx, rx) = tokio::sync::mpsc::channel(32);
2025-06-12 01:57:08 -05:00
let kb = self.kb.clone();
2026-01-06 04:38:41 -03:00
let ms = self.ms.clone();
let gadget = self.gadget.clone();
let did_cycle = self.did_cycle.clone();
2025-06-02 20:41:36 -05:00
tokio::spawn(async move {
2025-06-17 08:17:23 -05:00
let mut s = req.into_inner();
2025-06-17 20:54:31 -05:00
while let Some(pkt) = s.next().await.transpose()? {
2025-06-27 06:56:08 -05:00
if let Err(e) = kb.lock().await.write_all(&pkt.data).await {
warn!("⌨️ write failed: {e} (dropped)");
2026-01-28 17:52:00 -03:00
recover_hid_if_needed(
&e,
gadget.clone(),
kb.clone(),
ms.clone(),
did_cycle.clone(),
)
.await;
2025-06-25 15:13:49 -05:00
}
2025-06-27 06:56:08 -05:00
tx.send(Ok(pkt)).await.ok();
2025-06-17 08:17:23 -05:00
}
Ok::<(), Status>(())
});
2025-06-27 06:56:08 -05:00
2025-06-26 15:12:23 -05:00
Ok(Response::new(ReceiverStream::new(rx)))
2025-06-17 08:17:23 -05:00
}
2025-06-16 19:19:14 -05:00
2025-06-17 08:17:23 -05:00
async fn stream_mouse(
&self,
req: Request<tonic::Streaming<MouseReport>>,
) -> Result<Response<Self::StreamMouseStream>, Status> {
2025-06-28 15:45:11 -05:00
let (tx, rx) = tokio::sync::mpsc::channel(1024);
2025-06-17 08:17:23 -05:00
let ms = self.ms.clone();
2026-01-06 04:38:41 -03:00
let kb = self.kb.clone();
let gadget = self.gadget.clone();
let did_cycle = self.did_cycle.clone();
2025-06-16 19:19:14 -05:00
2025-06-17 08:17:23 -05:00
tokio::spawn(async move {
let mut s = req.into_inner();
2025-06-17 20:54:31 -05:00
while let Some(pkt) = s.next().await.transpose()? {
2025-06-27 06:56:08 -05:00
if let Err(e) = ms.lock().await.write_all(&pkt.data).await {
warn!("🖱️ write failed: {e} (dropped)");
2026-01-28 17:52:00 -03:00
recover_hid_if_needed(
&e,
gadget.clone(),
kb.clone(),
ms.clone(),
did_cycle.clone(),
)
.await;
2025-06-26 20:38:55 -05:00
}
2025-06-27 06:56:08 -05:00
tx.send(Ok(pkt)).await.ok();
2025-06-01 13:31:22 -05:00
}
2025-06-17 08:17:23 -05:00
Ok::<(), Status>(())
2025-06-01 13:31:22 -05:00
});
2025-06-27 06:56:08 -05:00
2025-06-26 15:12:23 -05:00
Ok(Response::new(ReceiverStream::new(rx)))
2025-06-01 13:31:22 -05:00
}
2025-06-21 05:21:57 -05:00
2025-06-30 19:35:38 -05:00
async fn stream_microphone(
&self,
req: Request<tonic::Streaming<AudioPacket>>,
) -> Result<Response<Self::StreamMicrophoneStream>, Status> {
2025-07-01 17:30:34 -05:00
// 1 ─ build once, early
2025-12-01 03:34:01 -03:00
let uac_dev = std::env::var("LESAVKA_UAC_DEV").unwrap_or_else(|_| "hw:UAC2Gadget,0".into());
info!(%uac_dev, "🎤 stream_microphone using UAC sink");
let mut sink = open_voice_with_retry(&uac_dev)
2025-11-30 16:16:03 -03:00
.await
2025-06-30 19:35:38 -05:00
.map_err(|e| Status::internal(format!("{e:#}")))?;
2025-07-01 17:30:34 -05:00
// 2 ─ dummy outbound stream (same trick as before)
2025-06-30 19:35:38 -05:00
let (tx, rx) = tokio::sync::mpsc::channel(1);
2025-07-01 12:11:52 -05:00
2025-07-01 17:30:34 -05:00
// 3 ─ drive the sink in a background task
2025-06-30 19:35:38 -05:00
tokio::spawn(async move {
let mut inbound = req.into_inner();
2025-11-30 16:16:03 -03:00
static CNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2025-07-01 17:30:34 -05:00
2025-06-30 19:35:38 -05:00
while let Some(pkt) = inbound.next().await.transpose()? {
2025-07-01 10:23:51 -05:00
let n = CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if n < 10 || n % 300 == 0 {
2025-07-01 17:30:34 -05:00
tracing::trace!("🎤⬇ srv pkt#{n} {} bytes", pkt.data.len());
2025-06-30 19:35:38 -05:00
}
2025-07-01 17:30:34 -05:00
sink.push(&pkt);
2025-06-30 19:35:38 -05:00
}
2025-11-30 16:16:03 -03:00
sink.finish(); // flush on EOS
2025-06-30 19:35:38 -05:00
let _ = tx.send(Ok(Empty {})).await;
2025-07-01 17:30:34 -05:00
Ok::<(), Status>(())
2025-06-30 19:35:38 -05:00
});
2025-07-01 17:30:34 -05:00
2025-06-30 19:35:38 -05:00
Ok(Response::new(ReceiverStream::new(rx)))
}
2025-07-03 08:19:59 -05:00
async fn stream_camera(
&self,
2025-07-03 09:24:57 -05:00
req: Request<tonic::Streaming<VideoPacket>>,
2025-07-03 08:19:59 -05:00
) -> Result<Response<Self::StreamCameraStream>, Status> {
2026-01-28 17:52:00 -03:00
let cfg = camera::current_camera_config();
info!(
output = cfg.output.as_str(),
codec = cfg.codec.as_str(),
width = cfg.width,
height = cfg.height,
fps = cfg.fps,
hdmi = cfg.hdmi.as_ref().map(|h| h.name.as_str()).unwrap_or("none"),
"🎥 stream_camera output selected"
);
let (session_id, relay) = self.camera_rt.activate(&cfg).await?;
let camera_rt = self.camera_rt.clone();
2025-11-30 16:16:03 -03:00
2025-07-03 09:24:57 -05:00
// dummy outbound (same pattern as other streams)
2025-07-03 08:19:59 -05:00
let (tx, rx) = tokio::sync::mpsc::channel(1);
2025-11-30 16:16:03 -03:00
2025-07-03 09:24:57 -05:00
tokio::spawn(async move {
let mut s = req.into_inner();
while let Some(pkt) = s.next().await.transpose()? {
if !camera_rt.is_active(session_id) {
info!(session_id, "🎥 stream_camera session superseded");
break;
}
2025-11-30 16:16:03 -03:00
relay.feed(pkt); // ← all logging inside video.rs
2025-07-03 09:24:57 -05:00
}
tx.send(Ok(Empty {})).await.ok();
Ok::<(), Status>(())
});
2025-11-30 16:16:03 -03:00
2025-07-03 08:19:59 -05:00
Ok(Response::new(ReceiverStream::new(rx)))
}
2025-06-21 05:21:57 -05:00
async fn capture_video(
&self,
req: Request<MonitorRequest>,
) -> Result<Response<Self::CaptureVideoStream>, Status> {
let req = req.into_inner();
let id = req.id;
2025-06-27 06:56:08 -05:00
let dev = match id {
2025-06-25 12:31:48 -05:00
0 => "/dev/lesavka_l_eye",
1 => "/dev/lesavka_r_eye",
_ => return Err(Status::invalid_argument("monitor id must be 0 or 1")),
2025-06-27 06:56:08 -05:00
};
2025-06-29 04:17:44 -05:00
debug!("🎥 streaming {dev}");
let s = video::eye_ball(dev, id, req.max_bitrate)
2025-06-21 05:21:57 -05:00
.await
2025-06-27 06:56:08 -05:00
.map_err(|e| Status::internal(format!("{e:#}")))?;
Ok(Response::new(Box::pin(s)))
}
2025-06-21 05:21:57 -05:00
2025-06-29 03:46:34 -05:00
async fn capture_audio(
&self,
req: Request<MonitorRequest>,
) -> Result<Response<Self::CaptureAudioStream>, Status> {
2025-06-29 22:57:54 -05:00
// Only one speaker stream for now; both 0/1 → same ALSA dev.
2025-11-30 16:16:03 -03:00
let _id = req.into_inner().id;
2025-06-29 22:57:54 -05:00
// Allow override (`LESAVKA_ALSA_DEV=hw:2,0` for debugging).
2025-11-30 16:16:03 -03:00
let dev = std::env::var("LESAVKA_ALSA_DEV").unwrap_or_else(|_| "hw:UAC2Gadget,0".into());
2025-06-29 22:57:54 -05:00
2025-06-30 02:42:20 -05:00
let s = audio::ear(&dev, 0)
2025-06-29 22:57:54 -05:00
.await
.map_err(|e| Status::internal(format!("{e:#}")))?;
Ok(Response::new(Box::pin(s)))
2025-06-29 03:46:34 -05:00
}
async fn paste_text(&self, req: Request<PasteRequest>) -> Result<Response<PasteReply>, Status> {
let req = req.into_inner();
let text = paste::decrypt(&req).map_err(|e| Status::unauthenticated(format!("{e}")))?;
if let Err(e) = paste::type_text(self.kb.as_ref(), &text).await {
return Ok(Response::new(PasteReply {
ok: false,
error: format!("{e}"),
}));
}
Ok(Response::new(PasteReply {
ok: true,
error: String::new(),
}))
}
2025-07-01 12:11:52 -05:00
/*────────────── USB-reset RPC ────────────*/
2025-11-30 16:16:03 -03:00
async fn reset_usb(&self, _req: Request<Empty>) -> Result<Response<ResetUsbReply>, Status> {
2025-06-27 06:56:08 -05:00
info!("🔴 explicit ResetUsb() called");
match self.gadget.cycle() {
2025-07-05 12:45:08 -05:00
Ok(_) => {
if let Err(e) = self.reopen_hid().await {
error!("💥 reopen HID failed: {e:#}");
return Err(Status::internal(e.to_string()));
}
Ok(Response::new(ResetUsbReply { ok: true }))
}
2025-06-27 06:56:08 -05:00
Err(e) => {
error!("💥 cycle failed: {e:#}");
Err(Status::internal(e.to_string()))
}
}
2025-06-21 05:21:57 -05:00
}
2025-06-01 13:31:22 -05:00
}
2025-06-27 06:56:08 -05:00
/*──────────────── main ───────────────────────*/
#[tokio::main(worker_threads = 4)]
2025-06-02 20:41:36 -05:00
async fn main() -> anyhow::Result<()> {
2025-06-27 06:56:08 -05:00
let _guard = init_tracing()?;
2025-07-04 10:17:01 -05:00
info!("🚀 {} v{} starting up", PKG_NAME, VERSION);
2025-06-05 22:44:27 -05:00
2025-06-25 20:00:34 -05:00
panic::set_hook(Box::new(|p| {
let bt = Backtrace::force_capture();
error!("💥 panic: {p}\n{bt}");
}));
2025-11-30 16:16:03 -03:00
let gadget = UsbGadget::new("lesavka");
2026-01-06 11:48:36 -03:00
if std::env::var("LESAVKA_DISABLE_UVC").is_err() {
2026-01-08 00:59:14 -03:00
if std::env::var("LESAVKA_UVC_EXTERNAL").is_ok() {
info!("📷 UVC control helper external; not spawning");
} else {
let bin = uvc_ctrl_bin();
tokio::spawn(supervise_uvc_control(bin));
}
2026-01-06 04:38:41 -03:00
} else {
info!("📷 UVC disabled (LESAVKA_DISABLE_UVC set)");
2026-01-06 11:48:36 -03:00
}
2025-06-27 06:56:08 -05:00
let handler = Handler::new(gadget.clone()).await?;
2025-06-12 01:57:08 -05:00
2025-06-28 15:45:11 -05:00
info!("🌐 lesavka-server listening on 0.0.0.0:50051");
2025-06-27 06:56:08 -05:00
Server::builder()
.tcp_nodelay(true)
2025-11-30 16:16:03 -03:00
.max_frame_size(Some(2 * 1024 * 1024))
2025-06-27 06:56:08 -05:00
.add_service(RelayServer::new(handler))
2025-07-04 03:41:39 -05:00
.add_service(HandshakeSvc::server())
2025-06-27 19:31:46 -05:00
.add_service(ReflBuilder::configure().build_v1().unwrap())
2025-11-30 16:16:03 -03:00
.serve(([0, 0, 0, 0], 50051).into())
2025-06-27 06:56:08 -05:00
.await?;
2025-06-01 13:31:22 -05:00
Ok(())
}