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]
name = "navka_client"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
[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"] }
tokio-stream = { version = "0.1", features = ["sync"] }
anyhow = "1.0"
navka_common = { path = "../common" }
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"
evdev = "0.13"

View File

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

View File

@ -5,8 +5,7 @@ use evdev::{Device, InputEvent, KeyCode, EventType};
use tokio::sync::broadcast::Sender;
use tracing::{warn, error, info, debug};
use navka_common::navka::HidReport;
use navka_common::navka::hid_report::Kind;
use navka_common::navka::{HidReport, hid_report};
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
pub fn process_events(&mut self) {
let events: Vec<InputEvent> = {
match self.dev.fetch_events() {
Ok(it) => it.collect(),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return,
Err(e) => {
// Fetch once. Any borrow of `self.dev` ends right here.
let events: Vec<InputEvent> = match self.dev.fetch_events() {
Ok(iter) => iter.collect(),
// 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}");
return;
}
return;
}
};
// Safe: no mutable borrow is active now
if self.dev_mode && !events.is_empty() {
info!(
self.dev_log(|| info!(
"Got {} events from dev: {}",
events.len(),
self.dev.name().unwrap_or("???")
);
));
}
for ev in events {
self.handle_event(ev);
}
}
}
fn handle_event(&mut self, ev: InputEvent) {
if ev.event_type() == EventType::KEY {
@ -61,10 +73,10 @@ impl KeyboardAggregator {
let val = ev.value(); // 1 = press, 0 = release, 2 = repeat
if self.dev_mode {
info!(
self.dev_log(|| info!(
"Keyboard event: code={:?}, value={}, name={:?}",
code, val, self.dev.name()
);
));
}
match val {
@ -77,12 +89,10 @@ impl KeyboardAggregator {
let report = self.build_report();
// TODO: send this somewhere (e.g. an mpsc::Sender)
// For now, just log:
debug!(?report, "Keyboard HID report");
self.dev_log(|| debug!(?report, "Keyboard HID report"));
self.send_report(report);
// optional: magic chord
if self.is_magic_chord() {
warn!("Magic chord pressed => exit aggregator??");
// Or do something else
self.dev_log(|| warn!("Magic chord pressed => AVADA KEDAVA!!!"));
std::process::exit(0);
}
}
@ -116,10 +126,10 @@ impl KeyboardAggregator {
match self.tx.send(msg.clone()) {
Ok(n) => {
info!("📤 sent HID report → {n} subscriber(s)");
self.dev_log(|| info!("📤 sent HID report → {n} subscriber(s)"));
}
Err(e) => {
tracing::warn!("❌ send failed: {e}");
self.dev_log(|| warn!("❌ send failed: {e}"));
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
pub fn keycode_to_usage(key: KeyCode) -> Option<u8> {
match key {
// Letters
// --- Letters ------------------------------------------------------
KeyCode::KEY_A => Some(0x04),
KeyCode::KEY_B => Some(0x05),
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_Z => Some(0x1D),
// Number row
// --- Number row ---------------------------------------------------
KeyCode::KEY_1 => Some(0x1E),
KeyCode::KEY_2 => Some(0x1F),
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_0 => Some(0x27),
// Common punctuation
// --- Common punctuation -------------------------------------------
KeyCode::KEY_ENTER => Some(0x28),
KeyCode::KEY_ESC => Some(0x29),
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_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_F1 => Some(0x3A),
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_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()`
_ => None,
}

View File

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

View File

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

View File

@ -39,25 +39,42 @@ impl Relay for Handler {
while let Some(msg) = in_stream.next().await.transpose()? {
info!("📥 packet received");
match msg.kind {
/* ───────────── KEYBOARD ───────────── */
Some(hid_report::Kind::KeyboardReport(ref v)) if v.len() == 8 => {
kb.lock().await.write_all(v).await?;
trace!(" └─ wrote 8B to /dev/hidg0");
let mut f = kb.lock().await;
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 => {
ms.lock().await.write_all(v).await?;
trace!(" └─ wrote 4B to /dev/hidg1");
let mut f = ms.lock().await;
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");
let _bad_len = match &msg.kind {
Some(hid_report::Kind::KeyboardReport(v)) => v.len(),
Some(hid_report::Kind::MouseReport(v)) => v.len(),
_ => 0,
};
continue;
}
}
info!("HID report forwarded");
let _ = tx.send(Ok(msg)).await;
}
info!("🔚 client stream closed");