This commit is contained in:
Brad Stein 2025-06-08 22:24:14 -05:00
parent 411a2ab3fe
commit d4b8208762
11 changed files with 307 additions and 127 deletions

View File

@ -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<Result<()>> = 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
}

View File

@ -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(())
}
}

145
client/src/input/inputs.rs Normal file
View File

@ -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<KeyboardAggregator>,
mice: Vec<MouseAggregator>,
// Possibly store camera or mic aggregator as well:
camera: Option<CameraCapture>,
mic: Option<MicrophoneCapture>,
}
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,
}

View File

@ -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<HidReport>,
// The list of evdev devices we are reading
devices: Vec<Device>,
// Pressed keys for building HID reports
dev: Device,
pressed_keys: HashSet<KeyCode>,
}
impl KeyboardAggregator {
pub fn new(tx: Sender<HidReport>) -> 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::<Vec<_>>(),
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)
}
}

View File

@ -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(())
}
}

View File

@ -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

51
client/src/input/mouse.rs Normal file
View File

@ -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?
}
}

View File

@ -1,3 +1,5 @@
// client/src/lib.rs
#![forbid(unsafe_code)]
pub mod app;

View File

@ -1,4 +1,5 @@
//! main.rs — entry point for navka-client
// client/src/main.rs
#![forbid(unsafe_code)]
use anyhow::Result;

View File

@ -1,3 +1,5 @@
// client/tests/integration.rs
#[cfg(test)]
mod tests {
use crate::input::keymap::{keycode_to_usage, is_modifier};

View File

@ -1 +1,3 @@
// client/src/tests/mod.rs
pub mod integration_test;