updates
This commit is contained in:
parent
411a2ab3fe
commit
d4b8208762
@ -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
|
||||
}
|
||||
|
||||
21
client/src/input/camera.rs
Normal file
21
client/src/input/camera.rs
Normal 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
145
client/src/input/inputs.rs
Normal 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,
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
19
client/src/input/microphone.rs
Normal file
19
client/src/input/microphone.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@ -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
51
client/src/input/mouse.rs
Normal 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?
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
// client/src/lib.rs
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod app;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
//! main.rs — entry point for navka-client
|
||||
// client/src/main.rs
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// client/tests/integration.rs
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::input::keymap::{keycode_to_usage, is_modifier};
|
||||
|
||||
@ -1 +1,3 @@
|
||||
// client/src/tests/mod.rs
|
||||
|
||||
pub mod integration_test;
|
||||
Loading…
x
Reference in New Issue
Block a user