established full keyboard functionality, working on mouse

This commit is contained in:
Brad Stein 2025-06-16 17:54:47 -05:00
parent 5cc1e92de0
commit d5fe1898a4
8 changed files with 237 additions and 142 deletions

View File

@ -1,16 +1,17 @@
[package] [package]
name = "navka_client" name = "navka_client"
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
tokio = { version = "1.45", features = ["full", "fs"] } tokio = { version = "1.45", features = ["full", "fs", "rt-multi-thread", "macros", "sync", "time"] }
tonic = { version = "0.13", features = ["transport"] } tonic = { version = "0.13", features = ["transport"] }
tokio-stream = { version = "0.1", features = ["sync"] } tokio-stream = { version = "0.1", features = ["sync"] }
anyhow = "1.0" anyhow = "1.0"
navka_common = { path = "../common" } navka_common = { path = "../common" }
tracing = { version = "0.1", features = ["std"] } tracing = { version = "0.1", features = ["std"] }
tracing-subscriber = "0.3" tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
tracing-appender = "0.2"
futures = "0.3" futures = "0.3"
evdev = "0.13" evdev = "0.13"

View File

@ -1,11 +1,9 @@
// client/src/app.rs // client/src/app.rs
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
use anyhow::Result; use anyhow::Result;
use futures::{FutureExt, StreamExt};
use std::time::Duration; use std::time::Duration;
use tokio::{sync::mpsc, sync::broadcast, task::JoinHandle}; use tokio::{sync::broadcast, task::JoinHandle};
use tokio_stream::StreamExt as _; use tokio_stream::{wrappers::BroadcastStream, StreamExt};
use tokio_stream::wrappers::{ReceiverStream, BroadcastStream};
use tonic::Request; use tonic::Request;
use tracing::{info, warn, error, debug}; use tracing::{info, warn, error, debug};
@ -21,6 +19,7 @@ pub struct NavkaClientApp {
impl NavkaClientApp { impl NavkaClientApp {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
info!("Creating navka-client app!");
let dev_mode = std::env::var("NAVKA_DEV_MODE").is_ok(); let dev_mode = std::env::var("NAVKA_DEV_MODE").is_ok();
let addr = std::env::args() let addr = std::env::args()
.nth(1) .nth(1)
@ -40,6 +39,7 @@ impl NavkaClientApp {
} }
pub async fn run(&mut self) -> Result<()> { pub async fn run(&mut self) -> Result<()> {
info!("Running navka-client app!");
// spawn aggregator // spawn aggregator
let mut aggregator = std::mem::take(&mut self.aggregator).expect("aggregator must exist"); let mut aggregator = std::mem::take(&mut self.aggregator).expect("aggregator must exist");
let aggregator_task: JoinHandle<Result<()>> = tokio::spawn(async move { let aggregator_task: JoinHandle<Result<()>> = tokio::spawn(async move {
@ -105,8 +105,7 @@ impl NavkaClientApp {
}; };
// fresh reader over the *same* broadcast channel // fresh reader over the *same* broadcast channel
let mut rx = self.tx.subscribe(); let outbound = BroadcastStream::new(self.tx.subscribe()).filter_map(|r| r.ok());
let outbound = BroadcastStream::new(rx.clone()).filter_map(|r| async { r.ok() });
info!("🛫 spawning stream()"); info!("🛫 spawning stream()");
let response = match client.stream(Request::new(outbound)).await { let response = match client.stream(Request::new(outbound)).await {

View File

@ -84,7 +84,7 @@ impl InputAggregator {
info!("Grabbed mouse {:?}", dev.name().unwrap_or("UNKNOWN")); info!("Grabbed mouse {:?}", dev.name().unwrap_or("UNKNOWN"));
// let mouse_agg = MouseAggregator::new(dev);np // let mouse_agg = MouseAggregator::new(dev);np
let mouse_agg = MouseAggregator::new(dev, self.tx.clone()); let mouse_agg = MouseAggregator::new(dev, self.dev_mode, self.tx.clone());
self.mice.push(mouse_agg); self.mice.push(mouse_agg);
found_any = true; found_any = true;
continue; continue;

View File

@ -5,8 +5,7 @@ use evdev::{Device, InputEvent, KeyCode, EventType};
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
use tracing::{warn, error, info, debug}; use tracing::{warn, error, info, debug};
use navka_common::navka::HidReport; use navka_common::navka::{HidReport, hid_report};
use navka_common::navka::hid_report::Kind;
use crate::input::keymap::{keycode_to_usage, is_modifier}; use crate::input::keymap::{keycode_to_usage, is_modifier};
@ -29,31 +28,44 @@ impl KeyboardAggregator {
} }
} }
#[inline]
fn dev_log(&self, record: impl FnOnce()) {
if self.dev_mode {
record();
}
}
/// Called frequently (e.g. every ~10ms) to fetch + handle events /// Called frequently (e.g. every ~10ms) to fetch + handle events
pub fn process_events(&mut self) { pub fn process_events(&mut self) {
let events: Vec<InputEvent> = { // Fetch once. Any borrow of `self.dev` ends right here.
match self.dev.fetch_events() { let events: Vec<InputEvent> = match self.dev.fetch_events() {
Ok(it) => it.collect(), Ok(iter) => iter.collect(),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return,
Err(e) => { // Would-block → nothing new
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => return,
// Any other error → log without touching `self.dev`
Err(e) => {
if self.dev_mode {
error!("Keyboard device read error: {e}"); error!("Keyboard device read error: {e}");
return;
} }
return;
} }
}; };
// Safe: no mutable borrow is active now
if self.dev_mode && !events.is_empty() { if self.dev_mode && !events.is_empty() {
info!( self.dev_log(|| info!(
"Got {} events from dev: {}", "Got {} events from dev: {}",
events.len(), events.len(),
self.dev.name().unwrap_or("???") self.dev.name().unwrap_or("???")
); ));
} }
for ev in events { for ev in events {
self.handle_event(ev); self.handle_event(ev);
} }
} }
fn handle_event(&mut self, ev: InputEvent) { fn handle_event(&mut self, ev: InputEvent) {
if ev.event_type() == EventType::KEY { if ev.event_type() == EventType::KEY {
@ -61,10 +73,10 @@ impl KeyboardAggregator {
let val = ev.value(); // 1 = press, 0 = release, 2 = repeat let val = ev.value(); // 1 = press, 0 = release, 2 = repeat
if self.dev_mode { if self.dev_mode {
info!( self.dev_log(|| info!(
"Keyboard event: code={:?}, value={}, name={:?}", "Keyboard event: code={:?}, value={}, name={:?}",
code, val, self.dev.name() code, val, self.dev.name()
); ));
} }
match val { match val {
@ -77,12 +89,10 @@ impl KeyboardAggregator {
let report = self.build_report(); let report = self.build_report();
// TODO: send this somewhere (e.g. an mpsc::Sender) // TODO: send this somewhere (e.g. an mpsc::Sender)
// For now, just log: // For now, just log:
debug!(?report, "Keyboard HID report"); self.dev_log(|| debug!(?report, "Keyboard HID report"));
self.send_report(report); self.send_report(report);
// optional: magic chord
if self.is_magic_chord() { if self.is_magic_chord() {
warn!("Magic chord pressed => exit aggregator??"); self.dev_log(|| warn!("Magic chord pressed => AVADA KEDAVA!!!"));
// Or do something else
std::process::exit(0); std::process::exit(0);
} }
} }
@ -116,10 +126,10 @@ impl KeyboardAggregator {
match self.tx.send(msg.clone()) { match self.tx.send(msg.clone()) {
Ok(n) => { Ok(n) => {
info!("📤 sent HID report → {n} subscriber(s)"); self.dev_log(|| info!("📤 sent HID report → {n} subscriber(s)"));
} }
Err(e) => { Err(e) => {
tracing::warn!("❌ send failed: {e}"); self.dev_log(|| warn!("❌ send failed: {e}"));
let _ = self.tx.send(msg); let _ = self.tx.send(msg);
} }
} }

View File

@ -5,7 +5,7 @@ use evdev::KeyCode;
/// Return Some(usage) if we have a known mapping from evdev::KeyCode -> HID usage code /// Return Some(usage) if we have a known mapping from evdev::KeyCode -> HID usage code
pub fn keycode_to_usage(key: KeyCode) -> Option<u8> { pub fn keycode_to_usage(key: KeyCode) -> Option<u8> {
match key { match key {
// Letters // --- Letters ------------------------------------------------------
KeyCode::KEY_A => Some(0x04), KeyCode::KEY_A => Some(0x04),
KeyCode::KEY_B => Some(0x05), KeyCode::KEY_B => Some(0x05),
KeyCode::KEY_C => Some(0x06), KeyCode::KEY_C => Some(0x06),
@ -33,7 +33,7 @@ pub fn keycode_to_usage(key: KeyCode) -> Option<u8> {
KeyCode::KEY_Y => Some(0x1C), KeyCode::KEY_Y => Some(0x1C),
KeyCode::KEY_Z => Some(0x1D), KeyCode::KEY_Z => Some(0x1D),
// Number row // --- Number row ---------------------------------------------------
KeyCode::KEY_1 => Some(0x1E), KeyCode::KEY_1 => Some(0x1E),
KeyCode::KEY_2 => Some(0x1F), KeyCode::KEY_2 => Some(0x1F),
KeyCode::KEY_3 => Some(0x20), KeyCode::KEY_3 => Some(0x20),
@ -45,7 +45,7 @@ pub fn keycode_to_usage(key: KeyCode) -> Option<u8> {
KeyCode::KEY_9 => Some(0x26), KeyCode::KEY_9 => Some(0x26),
KeyCode::KEY_0 => Some(0x27), KeyCode::KEY_0 => Some(0x27),
// Common punctuation // --- Common punctuation -------------------------------------------
KeyCode::KEY_ENTER => Some(0x28), KeyCode::KEY_ENTER => Some(0x28),
KeyCode::KEY_ESC => Some(0x29), KeyCode::KEY_ESC => Some(0x29),
KeyCode::KEY_BACKSPACE => Some(0x2A), KeyCode::KEY_BACKSPACE => Some(0x2A),
@ -63,7 +63,42 @@ pub fn keycode_to_usage(key: KeyCode) -> Option<u8> {
KeyCode::KEY_DOT => Some(0x37), KeyCode::KEY_DOT => Some(0x37),
KeyCode::KEY_SLASH => Some(0x38), KeyCode::KEY_SLASH => Some(0x38),
// Function keys // --- Navigation / editing cluster ---------------------------------
KeyCode::KEY_SYSRQ => Some(0x46), // PrintScreen
KeyCode::KEY_SCROLLLOCK => Some(0x47),
KeyCode::KEY_PAUSE => Some(0x48),
KeyCode::KEY_INSERT => Some(0x49),
KeyCode::KEY_HOME => Some(0x4A),
KeyCode::KEY_PAGEUP => Some(0x4B),
KeyCode::KEY_DELETE => Some(0x4C),
KeyCode::KEY_END => Some(0x4D),
KeyCode::KEY_PAGEDOWN => Some(0x4E),
KeyCode::KEY_RIGHT => Some(0x4F),
KeyCode::KEY_LEFT => Some(0x50),
KeyCode::KEY_DOWN => Some(0x51),
KeyCode::KEY_UP => Some(0x52),
// --- Keypad / Numlock block --------------------------------------
KeyCode::KEY_NUMLOCK => Some(0x53),
KeyCode::KEY_KPSLASH => Some(0x54),
KeyCode::KEY_KPASTERISK => Some(0x55),
KeyCode::KEY_KPMINUS => Some(0x56),
KeyCode::KEY_KPPLUS => Some(0x57),
KeyCode::KEY_KPENTER => Some(0x58),
KeyCode::KEY_KP1 => Some(0x59),
KeyCode::KEY_KP2 => Some(0x5A),
KeyCode::KEY_KP3 => Some(0x5B),
KeyCode::KEY_KP4 => Some(0x5C),
KeyCode::KEY_KP5 => Some(0x5D),
KeyCode::KEY_KP6 => Some(0x5E),
KeyCode::KEY_KP7 => Some(0x5F),
KeyCode::KEY_KP8 => Some(0x60),
KeyCode::KEY_KP9 => Some(0x61),
KeyCode::KEY_KP0 => Some(0x62),
KeyCode::KEY_KPDOT => Some(0x63),
KeyCode::KEY_KPEQUAL => Some(0x67),
// --- Function keys ------------------------------------------------
KeyCode::KEY_CAPSLOCK => Some(0x39), KeyCode::KEY_CAPSLOCK => Some(0x39),
KeyCode::KEY_F1 => Some(0x3A), KeyCode::KEY_F1 => Some(0x3A),
KeyCode::KEY_F2 => Some(0x3B), KeyCode::KEY_F2 => Some(0x3B),
@ -78,6 +113,10 @@ pub fn keycode_to_usage(key: KeyCode) -> Option<u8> {
KeyCode::KEY_F11 => Some(0x44), KeyCode::KEY_F11 => Some(0x44),
KeyCode::KEY_F12 => Some(0x45), KeyCode::KEY_F12 => Some(0x45),
// --- Misc ---------------------------------------------------------
KeyCode::KEY_102ND => Some(0x64), // “<>” on ISO boards
KeyCode::KEY_MENU => Some(0x65), // Application / Compose
// We'll handle modifiers (ctrl, shift, alt, meta) in `is_modifier()` // We'll handle modifiers (ctrl, shift, alt, meta) in `is_modifier()`
_ => None, _ => None,
} }

View File

@ -1,16 +1,18 @@
// client/src/input/mouse.rs // client/src/input/mouse.rs
use anyhow::Result;
use evdev::{Device, InputEvent, EventType, KeyCode, RelativeAxisCode}; use evdev::{Device, InputEvent, EventType, KeyCode, RelativeAxisCode};
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::{self, Sender};
use tracing::{error, debug}; use tracing::{debug, error, warn};
use navka_common::navka::HidReport; use navka_common::navka::{HidReport, hid_report};
use navka_common::navka::hid_report::Kind;
/// Aggregator for a single mouse device /// Aggregator for a single mouse device
pub struct MouseAggregator { pub struct MouseAggregator {
dev: Device, dev: Device,
tx: Sender<HidReport>, tx: Sender<HidReport>,
dev_mode: bool,
buttons: u8, buttons: u8,
dx: i8, dx: i8,
dy: i8, dy: i8,
@ -18,10 +20,12 @@ pub struct MouseAggregator {
} }
impl MouseAggregator { impl MouseAggregator {
pub fn new(dev: Device, tx: Sender<HidReport>) -> Self { pub fn new(dev: Device, dev_mode: bool, tx: Sender<HidReport>) -> Self {
Self { Self {
dev, dev,
tx, tx,
dev_mode,
buttons: 0, buttons: 0,
dx: 0, dx: 0,
dy: 0, dy: 0,
@ -29,77 +33,93 @@ impl MouseAggregator {
} }
} }
/// helper to set or clear a mouse button bit #[inline]
fn set_btn(&mut self, idx: usize, pressed: bool) { fn dev_log(&self, record: impl FnOnce()) {
if pressed { if self.dev_mode {
self.buttons |= 1 << idx; record();
}
}
#[inline]
fn set_btn(&mut self, bit: u8, val: i32) {
if val != 0 {
self.buttons |= 1 << bit;
} else { } else {
self.buttons &= !(1 << idx); self.buttons &= !(1 << bit);
} }
} }
pub fn process_events(&mut self) { pub fn process_events(&mut self) {
let events_vec: Vec<InputEvent> = match self.dev.fetch_events() { /* 1 ─ read a nonblocking batch */
let events: Vec<InputEvent> = match self.dev.fetch_events() {
Ok(it) => it.collect(), Ok(it) => it.collect(),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => return,
return; Err(e) => { if self.dev_mode { error!("🖱️ read error: {e}"); } return; }
}
Err(e) => {
error!("Mouse device read error: {e}");
return;
}
}; };
for ev in events_vec { if self.dev_mode && !events.is_empty() {
self.handle_event(ev); self.dev_log(|| debug!("🖱️ {} events from {}", events.len(),
self.dev.name().unwrap_or("UNKNOWN")));
}
/* 2 ─ aggregate */
for ev in events {
match ev.event_type() {
/* ---------- buttons ---------- */
EventType::KEY => match ev.code() {
c if c == KeyCode::BTN_LEFT.0 => self.set_btn(0, ev.value()),
c if c == KeyCode::BTN_RIGHT.0 => self.set_btn(1, ev.value()),
c if c == KeyCode::BTN_MIDDLE.0 => self.set_btn(2, ev.value()),
_ => {}
},
/* ----------- axes ------------ */
EventType::RELATIVE => match ev.code() {
c if c == RelativeAxisCode::REL_X.0 => {
self.dx = self.dx.saturating_add(ev.value().clamp(-127, 127) as i8);
}
c if c == RelativeAxisCode::REL_Y.0 => {
self.dy = self.dy.saturating_add(ev.value().clamp(-127, 127) as i8);
}
c if c == RelativeAxisCode::REL_WHEEL.0 => {
self.wheel = self.wheel.saturating_add(ev.value().clamp(-1, 1) as i8);
}
_ => {}
},
/* ---- batch delimiter -------- */
EventType::SYNCHRONIZATION => {
// Any sync event is fine we only care about boundaries
self.flush_report();
}
_ => {}
}
} }
} }
fn handle_event(&mut self, ev: InputEvent) { /// Build & send HID packet, then clear deltas
match ev.event_type() { fn flush_report(&mut self) {
EventType::RELATIVE => { /* Nothing changed ⇒ nothing to send */
match RelativeAxisCode::new(ev.code()) { if self.dx == 0 && self.dy == 0 && self.wheel == 0 { return; }
RelativeAxisCode::REL_X => self.dx = ev.value() as i8,
RelativeAxisCode::REL_Y => self.dy = ev.value() as i8,
RelativeAxisCode::REL_WHEEL => self.wheel = ev.value() as i8,
_ => {}
}
}
EventType::KEY => {
let pressed = ev.value() != 0;
match ev.code() {
c if c == KeyCode::BTN_LEFT.0 => self.set_btn(0, pressed),
c if c == KeyCode::BTN_RIGHT.0 => self.set_btn(1, pressed),
c if c == KeyCode::BTN_MIDDLE.0 => self.set_btn(2, pressed),
_ => {}
}
}
_ => {}
}
// whenever we changed something, emit: let report = [
let report = [self.buttons, self.dx as u8, self.dy as u8, self.wheel as u8]; self.buttons,
self.send_report(report); self.dx.clamp(-127, 127) as u8,
self.dy.clamp(-127, 127) as u8,
self.wheel as u8,
];
// reset deltas so we send *relative* movement /* broadcast — this is nonblocking just like `try_send` on mpsc */
self.dx = 0; if let Err(broadcast::error::SendError(_)) = self.tx.send(HidReport {
self.dy = 0;
self.wheel = 0;
}
fn send_report(&self, report: [u8; 4]) {
let msg = HidReport {
kind: Some(hid_report::Kind::MouseReport(report.to_vec())), kind: Some(hid_report::Kind::MouseReport(report.to_vec())),
}; }) {
self.dev_log(|| warn!("❌ no HID receiver (mouse)"));
match self.tx.send(msg.clone()) { } else {
Ok(n) => { self.dev_log(|| debug!("📤 HID mouse {:?}", report));
tracing::trace!("queued → {} receiver(s)", n);
}
Err(e) => {
tracing::warn!("send dropped report ({e}); falling back to send()");
let _ = self.tx.send(msg);
}
} }
/* reset deltas for next frame */
self.dx = 0; self.dy = 0; self.wheel = 0;
} }
} }

View File

@ -4,56 +4,65 @@
use anyhow::Result; use anyhow::Result;
use navka_client::NavkaClientApp; use navka_client::NavkaClientApp;
use std::env; use std::{env, fs::OpenOptions, path::Path};
use std::fs::OpenOptions; use tracing_appender::non_blocking;
use tracing_subscriber::fmt; use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; use tracing_subscriber::{filter::EnvFilter, fmt, prelude::*};
use tracing_subscriber::{fmt, EnvFilter};
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
tracing_subscriber::fmt() /*------------- common filter & stderr layer ------------------------*/
.with_env_filter( let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
// honour RUST_LOG but fall back to very chatty defaults EnvFilter::new(
EnvFilter::try_from_default_env().unwrap_or_else(|_| { "navka_client=trace,\
EnvFilter::new( navka_server=trace,\
"navka_client=trace,\ tonic=debug,\
navka_server=trace,\ h2=debug,\
tonic=debug,\ tower=debug",
h2=debug,\ )
tower=debug", });
)
}), let stderr_layer = fmt::layer()
) .with_target(true)
.with_target(true) .with_thread_ids(true)
.with_thread_ids(true) .with_file(true);
.with_file(true)
.init();
let dev_mode = env::var("NAVKA_DEV_MODE").is_ok(); let dev_mode = env::var("NAVKA_DEV_MODE").is_ok();
let mut _guard: Option<WorkerGuard> = None; // keep guard alive
/*------------- subscriber setup -----------------------------------*/
if dev_mode { if dev_mode {
let log_path = Path::new("/tmp").join("navka-client.log");
// file → nonblocking writer (+ guard)
let file = OpenOptions::new() let file = OpenOptions::new()
.create(true) .create(true)
.append(true) .append(true)
.open("/tmp/navka-client.log")?; .open(&log_path)?;
let (file_writer, guard) = non_blocking(file);
_guard = Some(guard);
let subscriber = tracing_subscriber::registry() let file_layer = fmt::layer()
.with(fmt::layer()) .with_writer(file_writer)
.with( .with_ansi(false)
fmt::layer() .with_target(true)
.with_writer(file) .with_level(true);
.with_ansi(false)
.with_target(true)
.with_level(true),
);
tracing::subscriber::set_global_default(subscriber)?; tracing_subscriber::registry()
tracing::info!("navka-client starting in dev mode: logs -> /tmp/navka-client.log"); .with(env_filter)
.with(stderr_layer)
.with(file_layer)
.init();
tracing::info!("navka-client running in DEV mode → {}", log_path.display());
} else { } else {
tracing_subscriber::fmt::init(); tracing_subscriber::registry()
.with(env_filter)
.with(stderr_layer)
.init();
} }
/*------------- run the actual application -------------------------*/
let mut app = NavkaClientApp::new()?; let mut app = NavkaClientApp::new()?;
app.run().await app.run().await
} }

View File

@ -39,25 +39,42 @@ impl Relay for Handler {
while let Some(msg) = in_stream.next().await.transpose()? { while let Some(msg) = in_stream.next().await.transpose()? {
info!("📥 packet received"); info!("📥 packet received");
match msg.kind { match msg.kind {
/* ───────────── KEYBOARD ───────────── */
Some(hid_report::Kind::KeyboardReport(ref v)) if v.len() == 8 => { Some(hid_report::Kind::KeyboardReport(ref v)) if v.len() == 8 => {
kb.lock().await.write_all(v).await?; let mut f = kb.lock().await;
trace!(" └─ wrote 8B to /dev/hidg0"); match f.write_all(v).await {
Ok(()) => info!("⌨️ HID report forwarded → /dev/hidg0"),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
trace!("⌨️ /dev/hidg0 busy ({e}); dropped packet");
}
Err(e) => {
error!("⌨️ write error to /dev/hidg0: {e}");
continue; // drop this packet, keep stream alive
}
}
} }
/* ─────────────── MOUSE ─────────────── */
Some(hid_report::Kind::MouseReport(ref v)) if v.len() == 4 => { Some(hid_report::Kind::MouseReport(ref v)) if v.len() == 4 => {
ms.lock().await.write_all(v).await?; let mut f = ms.lock().await;
trace!(" └─ wrote 4B to /dev/hidg1"); match f.write_all(v).await {
Ok(()) => info!("🖱️ HID report forwarded → /dev/hidg1"),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
trace!("🖱️ /dev/hidg1 busy ({e}); dropped packet");
}
Err(e) => {
error!("🖱️ write error to /dev/hidg1: {e}");
continue;
}
}
} }
/* ────────────── BAD PACKET ──────────── */
_ => { _ => {
error!(?msg.kind, "⚠️ malformed packet"); error!(?msg.kind, "⚠️ malformed packet");
let _bad_len = match &msg.kind {
Some(hid_report::Kind::KeyboardReport(v)) => v.len(),
Some(hid_report::Kind::MouseReport(v)) => v.len(),
_ => 0,
};
continue; continue;
} }
} }
info!("HID report forwarded");
let _ = tx.send(Ok(msg)).await; let _ = tx.send(Ok(msg)).await;
} }
info!("🔚 client stream closed"); info!("🔚 client stream closed");