From d4b8208762c082e806dcb55dc8952e1609a4ccf8 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 8 Jun 2025 22:24:14 -0500 Subject: [PATCH] updates --- client/src/app.rs | 16 +-- client/src/input/camera.rs | 21 ++++ client/src/input/inputs.rs | 145 ++++++++++++++++++++++++ client/src/input/keyboard.rs | 163 ++++++++------------------- client/src/input/microphone.rs | 19 ++++ client/src/input/mod.rs | 10 +- client/src/input/mouse.rs | 51 +++++++++ client/src/lib.rs | 2 + client/src/main.rs | 3 +- client/src/tests/integration_test.rs | 2 + client/src/tests/mod.rs | 2 + 11 files changed, 307 insertions(+), 127 deletions(-) create mode 100644 client/src/input/camera.rs create mode 100644 client/src/input/inputs.rs create mode 100644 client/src/input/microphone.rs create mode 100644 client/src/input/mouse.rs diff --git a/client/src/app.rs b/client/src/app.rs index 4a0b5c7..158cf05 100644 --- a/client/src/app.rs +++ b/client/src/app.rs @@ -1,12 +1,15 @@ +// client/src/app.rs + use anyhow::{Context, Result}; use tokio::{sync::mpsc, time::Duration, task::JoinHandle}; use tokio_stream::wrappers::ReceiverStream; use tonic::Request; use tracing::{info, warn, error}; use navka_common::navka::{relay_client::RelayClient, HidReport}; -use crate::input::keyboard::KeyboardAggregator; +use crate::input::inputs::InputAggregator; pub struct NavkaClientApp { + aggregator: InputAggregator, server_addr: String, dev_mode: bool, } @@ -18,21 +21,20 @@ impl NavkaClientApp { .nth(1) .or_else(|| std::env::var("NAVKA_SERVER_ADDR").ok()) .unwrap_or_else(|| "http://127.0.0.1:50051".to_owned()); + let mut aggregator = InputAggregator::new(); + aggregator.init()?; // discover & grab Ok(Self { + aggregator, server_addr: addr, dev_mode, }) } pub async fn run(&mut self) -> Result<()> { - // aggregator - let mut aggregator = KeyboardAggregator::new(self.make_channel()?); - aggregator.init_devices()?; // discover & grab - // spawn aggregator let aggregator_task: JoinHandle> = tokio::spawn(async move { - aggregator.run().await + self.aggregator.run().await }); // dev-mode kill future @@ -40,7 +42,7 @@ impl NavkaClientApp { if self.dev_mode { info!("DEV-mode: will kill itself in 30 s"); tokio::time::sleep(Duration::from_secs(30)).await; - Err::<(), _>(anyhow::anyhow!("dev-mode timer expired\ngoodbye cruel world...")) + Err::<(), _>(anyhow::anyhow!("dev-mode timer expired - goodbye cruel world...")) } else { futures::future::pending().await } diff --git a/client/src/input/camera.rs b/client/src/input/camera.rs new file mode 100644 index 0000000..29814db --- /dev/null +++ b/client/src/input/camera.rs @@ -0,0 +1,21 @@ +// client/src/input/camera.rs + +use anyhow::Result; + +/// A stub camera aggregator or capture +pub struct CameraCapture { + // no real fields yet +} + +impl CameraCapture { + pub fn new_stub() -> Self { + // in real code: open /dev/video0, set formats, etc + CameraCapture {} + } + + /// Called regularly to capture frames or do nothing + pub fn process_frames(&mut self) -> Result<()> { + // no-op + Ok(()) + } +} diff --git a/client/src/input/inputs.rs b/client/src/input/inputs.rs new file mode 100644 index 0000000..42265a5 --- /dev/null +++ b/client/src/input/inputs.rs @@ -0,0 +1,145 @@ +// client/src/input/inputs.rs + +use anyhow::{bail, Context, Result}; +use evdev::{Device, EventType, KeyCode, RelativeAxisType}; +use tokio::time::{interval, Duration}; +use tracing::info; + +use crate::input::keyboard::KeyboardAggregator; +use crate::input::mouse::MouseAggregator; +use crate::input::camera::CameraCapture; +use crate::input::microphone::MicrophoneCapture; + +/// A top-level aggregator that enumerates /dev/input/event*, +/// classifies them as keyboard vs. mouse vs. other, +/// spawns specialized aggregator objects, and can also +/// create stubs for camera/microphone logic if needed. +pub struct InputAggregator { + keyboards: Vec, + mice: Vec, + // Possibly store camera or mic aggregator as well: + camera: Option, + mic: Option, +} + +impl InputAggregator { + pub fn new() -> Self { + Self { + keyboards: Vec::new(), + mice: Vec::new(), + camera: None, + mic: None, + } + } + + /// Called once at startup: enumerates input devices, + /// classifies them, and constructs a aggregator struct per type. + pub fn init(&mut self) -> Result<()> { + let paths = std::fs::read_dir("/dev/input") + .context("Failed to read /dev/input")?; + + let mut found_any = false; + + for entry in paths { + let entry = entry?; + let path = entry.path(); + + // skip anything that isn't "event*" + if !path.file_name() + .map_or(false, |f| f.to_string_lossy().starts_with("event")) + { + continue; + } + + // attempt open + let mut dev = match Device::open(&path) { + Ok(d) => d, + Err(e) => { + tracing::debug!("Skipping {:?}, open error {e}", path); + continue; + } + }; + + match classify_device(&dev) { + DeviceKind::Keyboard => { + dev.grab().with_context(|| format!("grabbing keyboard {:?}", path))?; + info!("Grabbed keyboard {:?}", dev.name().unwrap_or("UNKNOWN")); + let kbd_agg = KeyboardAggregator::new(dev); + self.keyboards.push(kbd_agg); + found_any = true; + } + DeviceKind::Mouse => { + dev.grab().with_context(|| format!("grabbing mouse {:?}", path))?; + info!("Grabbed mouse {:?}", dev.name().unwrap_or("UNKNOWN")); + let mouse_agg = MouseAggregator::new(dev); + self.mice.push(mouse_agg); + found_any = true; + } + DeviceKind::Other => { + tracing::debug!("Skipping non-kbd/mouse device: {:?}", dev.name().unwrap_or("UNKNOWN")); + } + } + } + + if !found_any { + bail!("No suitable keyboard/mouse devices found or none grabbed."); + } + + // Stubs for camera / mic: + self.camera = Some(CameraCapture::new_stub()); + self.mic = Some(MicrophoneCapture::new_stub()); + + Ok(()) + } + + /// We spawn the sub-aggregators in a loop or using separate tasks. + /// (For a real system: you'd spawn a separate task for each aggregator.) + pub async fn run(&mut self) -> Result<()> { + // Example approach: poll each aggregator in a simple loop + let mut tick = interval(Duration::from_millis(10)); + loop { + for kbd in &mut self.keyboards { + kbd.process_events(); + } + for mouse in &mut self.mice { + mouse.process_events(); + } + // camera / mic stubs could go here + tick.tick().await; + } + } +} + +/// The classification function +fn classify_device(dev: &Device) -> DeviceKind { + let evbits = dev.supported_events(); + + // If it supports typed scancodes => Keyboard + if evbits.contains(EventType::KEY) { + if let Some(keys) = dev.supported_keys() { + if keys.contains(KeyCode::KEY_A) || keys.contains(KeyCode::KEY_ENTER) { + return DeviceKind::Keyboard; + } + } + } + // If it supports REL_X / REL_Y => Mouse + if evbits.contains(EventType::REL) { + if let Some(rel) = dev.supported_relative_axes() { + if rel.contains(RelativeAxisType::REL_X) && + rel.contains(RelativeAxisType::REL_Y) + { + return DeviceKind::Mouse; + } + } + } + + DeviceKind::Other +} + +/// Internal enum for device classification +#[derive(Debug, Clone, Copy)] +enum DeviceKind { + Keyboard, + Mouse, + Other, +} diff --git a/client/src/input/keyboard.rs b/client/src/input/keyboard.rs index 7ca6e88..aca2ce4 100644 --- a/client/src/input/keyboard.rs +++ b/client/src/input/keyboard.rs @@ -1,160 +1,89 @@ +// client/src/input/keyboard.rs + use std::collections::HashSet; -use anyhow::{bail, Context, Result}; use evdev::{Device, InputEvent, KeyCode, EventType}; -use tokio::sync::mpsc::Sender; -use tokio::time::{interval, Duration}; +use tracing::warn; use navka_common::navka::HidReport; use crate::input::keymap::{keycode_to_usage, is_modifier}; -/// Up to 6 normal keys. Byte[0] = modifiers, Byte[1] = reserved, Byte[2..7] = pressed keys. -/// Magic chord example: LeftCtrl + LeftAlt + LeftShift + Esc -const MAGIC_CHORD: &[KeyCode] = &[ - KeyCode::KEY_LEFTCTRL, - // KeyCode::KEY_LEFTSHIFT, - // KeyCode::KEY_LEFTALT, - KeyCode::KEY_ESC, -]; - +/// The aggregator logic for a single keyboard device. pub struct KeyboardAggregator { - // MPSC channel to server - tx: Sender, - // The list of evdev devices we are reading - devices: Vec, - // Pressed keys for building HID reports + dev: Device, pressed_keys: HashSet, } impl KeyboardAggregator { - pub fn new(tx: Sender) -> Self { + pub fn new(dev: Device) -> Self { Self { - tx, - devices: Vec::new(), + dev, pressed_keys: HashSet::new(), } } - /// Discover all /dev/input/event* devices, filter ones with EV_KEY, and grab them. - pub fn init_devices(&mut self) -> Result<()> { - let paths = std::fs::read_dir("/dev/input") - .context("Failed to read /dev/input")?; - - for entry in paths { - let entry = entry?; - let path = entry.path(); - - // skip anything that isn't "event*" - if !path.file_name().unwrap_or_default().to_string_lossy().starts_with("event") { - continue; - } - let mut dev = Device::open(&path).with_context(|| format!("opening {:?}", path))?; - - let maybe_keys = dev.supported_keys(); - if let Some(supported) = maybe_keys { - if supported.iter().next().is_none() { - // no real keys - continue; - } - } else { - // Not a keyboard at all - continue; - } - - // Attempt to grab - dev.grab().with_context(|| format!("grabbing {:?}", path))?; - - tracing::info!("Grabbed keyboard device: {:?}", path); - self.devices.push(dev); - } - if self.devices.is_empty() { - bail!("No keyboard devices found or none grabbed successfully."); - } - Ok(()) - } - - /// Main loop: read events from all devices in a round-robin style - /// building HID reports as needed. If MAGIC_CHORD is detected, exit. - pub async fn run(&mut self) -> Result<()> { - let mut tick = interval(Duration::from_millis(10)); - - loop { - // We'll poll each device in turn - for i in 0..self.devices.len() { - // Non-blocking fetch - let evs = match self.devices[i].fetch_events() { - Ok(iter) => iter.collect::>(), - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Vec::new(), - Err(e) => { - tracing::error!("Device read error: {e}"); - Vec::new() - } - }; - for ev in evs { - self.handle_event(ev)?; + /// Called frequently (e.g. every ~10ms) to fetch + handle events + pub fn process_events(&mut self) { + match self.dev.fetch_events() { + Ok(events) => { + for ev in events { + self.handle_event(ev); } } - if self.is_magic_chord() { - tracing::warn!("Magic chord pressed; stopping navka-client."); - // ungrab all before exit - for dev in &mut self.devices { - let _ = dev.ungrab(); - } - std::process::exit(0); + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // nothing + } + Err(e) => { + tracing::error!("Keyboard device read error: {e}"); } - tick.tick().await; } } - fn handle_event(&mut self, ev: InputEvent) -> Result<()> { - // We only care about KEY events + fn handle_event(&mut self, ev: InputEvent) { if ev.event_type() == EventType::KEY { let code = KeyCode::new(ev.code()); - let value = ev.value(); - match value { + match ev.value() { 1 => { self.pressed_keys.insert(code); } 0 => { self.pressed_keys.remove(&code); } 2 => { /* repeats, if needed */ } _ => {} } - // Build new HID report - let report_bytes = self.build_hid_report(); - let _ = self.tx.try_send(HidReport { - data: report_bytes.to_vec(), - }); + let report = self.build_report(); + // TODO: send this somewhere (e.g. an mpsc::Sender) + // For now, just log: + tracing::debug!(?report, "Keyboard HID report"); + // optional: magic chord + if self.is_magic_chord() { + warn!("Magic chord pressed => exit aggregator??"); + // Or do something else + } } - Ok(()) } - fn build_hid_report(&self) -> [u8; 8] { - // Byte 0: modifiers (bitmask) - // Byte 1: reserved - // Byte 2..7: up to 6 keys - // We'll do a naive approach: gather up to 6 non-modifier codes. - let mut report = [0u8; 8]; + fn build_report(&self) -> [u8; 8] { + let mut bytes = [0u8; 8]; + let mut normal_keys = Vec::new(); - let mut modifier_mask = 0u8; - - for &key in &self.pressed_keys { - if let Some(modbits) = is_modifier(key) { - // e.g. KEY_LEFTSHIFT => 0x02 - modifier_mask |= modbits; - } else if let Some(usage) = keycode_to_usage(key) { - // Normal key + let mut modifiers = 0u8; + + for &kc in &self.pressed_keys { + if let Some(m) = is_modifier(kc) { + modifiers |= m; + } else if let Some(usage) = keycode_to_usage(kc) { normal_keys.push(usage); } } - - report[0] = modifier_mask; - // Byte[1] is reserved 0 - for (i, code) in normal_keys.into_iter().take(6).enumerate() { - report[2 + i] = code; + + bytes[0] = modifiers; + for (i, keycode) in normal_keys.into_iter().take(6).enumerate() { + bytes[2 + i] = keycode; } - report + bytes } fn is_magic_chord(&self) -> bool { - // All the keys in MAGIC_CHORD must be pressed - MAGIC_CHORD.iter().all(|k| self.pressed_keys.contains(k)) + // example logic + self.pressed_keys.contains(&KeyCode::KEY_LEFTCTRL) && + self.pressed_keys.contains(&KeyCode::KEY_ESC) } } diff --git a/client/src/input/microphone.rs b/client/src/input/microphone.rs new file mode 100644 index 0000000..2c5a61f --- /dev/null +++ b/client/src/input/microphone.rs @@ -0,0 +1,19 @@ +// client/src/input/microphone.rs + +use anyhow::Result; + +pub struct MicrophoneCapture { + // no real fields yet +} + +impl MicrophoneCapture { + pub fn new_stub() -> Self { + // real code would open /dev/snd, or use cpal / rodio / etc + MicrophoneCapture {} + } + + pub fn capture_audio(&mut self) -> Result<()> { + // no-op + Ok(()) + } +} diff --git a/client/src/input/mod.rs b/client/src/input/mod.rs index fc0a761..1da92ad 100644 --- a/client/src/input/mod.rs +++ b/client/src/input/mod.rs @@ -1,2 +1,8 @@ -pub mod keyboard; -pub mod keymap; +// client/src/input/mod.rs + +pub mod inputs; // the aggregator that scans /dev/input and spawns sub-aggregators +pub mod keyboard; // existing keyboard aggregator logic (minus scanning) +pub mod mouse; // a stub aggregator for mice +pub mod camera; // stub for camera +pub mod microphone; // stub for mic +pub mod keymap; // keyboard keymap logic diff --git a/client/src/input/mouse.rs b/client/src/input/mouse.rs new file mode 100644 index 0000000..279ac8d --- /dev/null +++ b/client/src/input/mouse.rs @@ -0,0 +1,51 @@ +// client/src/input/mouse.rs + +use evdev::{Device, InputEvent, EventType, RelativeAxisType}; + +/// Aggregator for a single mouse device +pub struct MouseAggregator { + dev: Device, + dx: i32, + dy: i32, + // etc +} + +impl MouseAggregator { + pub fn new(dev: Device) -> Self { + Self { + dev, + dx: 0, + dy: 0, + } + } + + pub fn process_events(&mut self) { + match self.dev.fetch_events() { + Ok(events) => { + for ev in events { + self.handle_event(ev); + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {} + Err(e) => { + tracing::error!("Mouse device read error: {e}"); + } + } + } + + fn handle_event(&mut self, ev: InputEvent) { + if ev.event_type() == EventType::REL { + match ev.code() { + x if x == RelativeAxisType::REL_X.0 => { + self.dx += ev.value(); + } + y if y == RelativeAxisType::REL_Y.0 => { + self.dy += ev.value(); + } + _ => {} + } + tracing::debug!("mouse dx={} dy={}", self.dx, self.dy); + } + // etc. Also handle buttons with KEY_B? + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 4402856..75d23fa 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,3 +1,5 @@ +// client/src/lib.rs + #![forbid(unsafe_code)] pub mod app; diff --git a/client/src/main.rs b/client/src/main.rs index 1211c66..ec97b3e 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,4 +1,5 @@ -//! main.rs — entry point for navka-client +// client/src/main.rs + #![forbid(unsafe_code)] use anyhow::Result; diff --git a/client/src/tests/integration_test.rs b/client/src/tests/integration_test.rs index d6a2d3e..f351460 100644 --- a/client/src/tests/integration_test.rs +++ b/client/src/tests/integration_test.rs @@ -1,3 +1,5 @@ +// client/tests/integration.rs + #[cfg(test)] mod tests { use crate::input::keymap::{keycode_to_usage, is_modifier}; diff --git a/client/src/tests/mod.rs b/client/src/tests/mod.rs index 6b912ce..bfe931e 100644 --- a/client/src/tests/mod.rs +++ b/client/src/tests/mod.rs @@ -1 +1,3 @@ +// client/src/tests/mod.rs + pub mod integration_test; \ No newline at end of file