rewrote client, crashes currently
This commit is contained in:
parent
2058014ac6
commit
53e9f69356
@ -1,16 +1,22 @@
|
||||
[package]
|
||||
name = "navka-client"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
name = "navka_client"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.45", features = ["full", "fs"] }
|
||||
tonic = "0.11"
|
||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||
anyhow = "1.0"
|
||||
navka-common = { path = "../common" }
|
||||
tracing = { version = "0.1", features = ["std"] }
|
||||
tracing-subscriber = "0.3"
|
||||
tokio = { version = "1.45", features = ["full", "fs"] }
|
||||
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"
|
||||
futures = "0.3"
|
||||
evdev = "0.13"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.12"
|
||||
prost-build = "0.13"
|
||||
|
||||
[lib]
|
||||
name = "navka_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
56
client/src/app.rs
Normal file
56
client/src/app.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use anyhow::{Context, Result};
|
||||
use navka_common::navka::{relay_client::RelayClient, HidReport};
|
||||
use tokio::sync::mpsc;
|
||||
// use tokio_stream::{wrappers::ReceiverStream, StreamExt};
|
||||
// use tonic::{Status, Request};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::Request;
|
||||
use crate::input::keyboard::KeyboardAggregator;
|
||||
|
||||
pub struct NavkaClientApp {
|
||||
server_addr: String,
|
||||
}
|
||||
|
||||
impl NavkaClientApp {
|
||||
pub fn new() -> Result<Self> {
|
||||
let addr = std::env::args()
|
||||
.nth(1)
|
||||
.or_else(|| std::env::var("NAVKA_SERVER_ADDR").ok())
|
||||
.unwrap_or_else(|| "http://127.0.0.1:50051".to_owned());
|
||||
|
||||
Ok(Self { server_addr: addr })
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
// 1) Connect to navka-server
|
||||
let mut client = RelayClient::connect(self.server_addr.clone())
|
||||
.await
|
||||
.with_context(|| format!("failed to connect to {}", self.server_addr))?;
|
||||
|
||||
// 2) Create a bidirectional streaming stub
|
||||
let (tx, rx) = mpsc::channel::<HidReport>(32);
|
||||
// let outbound = ReceiverStream::new(rx).map(|report| Ok(report) as Result<HidReport, Status>);
|
||||
// let response = client.stream(Request::new(outbound)).await?;
|
||||
let outbound = ReceiverStream::new(rx);
|
||||
let response = client.stream(Request::new(outbound)).await?;
|
||||
let mut inbound = response.into_inner();
|
||||
|
||||
// 3) Start reading from all keyboards in a background task
|
||||
let mut aggregator = KeyboardAggregator::new(tx.clone());
|
||||
aggregator.init_devices()?; // discover & grab
|
||||
let input_task = tokio::spawn(async move {
|
||||
if let Err(e) = aggregator.run().await {
|
||||
tracing::error!("KeyboardAggregator failed: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
// 4) Inbound loop: we do something with reports from the server, e.g. logging:
|
||||
while let Some(report) = inbound.message().await? {
|
||||
tracing::info!(?report.data, "echo from server");
|
||||
}
|
||||
|
||||
// 5) If inbound stream ends, stop the input task
|
||||
input_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
160
client/src/input/keyboard.rs
Normal file
160
client/src/input/keyboard.rs
Normal file
@ -0,0 +1,160 @@
|
||||
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 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,
|
||||
];
|
||||
|
||||
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
|
||||
pressed_keys: HashSet<KeyCode>,
|
||||
}
|
||||
|
||||
impl KeyboardAggregator {
|
||||
pub fn new(tx: Sender<HidReport>) -> Self {
|
||||
Self {
|
||||
tx,
|
||||
devices: Vec::new(),
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
tick.tick().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, ev: InputEvent) -> Result<()> {
|
||||
// We only care about KEY events
|
||||
if ev.event_type() == EventType::KEY {
|
||||
let code = KeyCode::new(ev.code());
|
||||
let value = ev.value();
|
||||
match 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(),
|
||||
});
|
||||
}
|
||||
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];
|
||||
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
|
||||
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;
|
||||
}
|
||||
report
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
99
client/src/input/keymap.rs
Normal file
99
client/src/input/keymap.rs
Normal file
@ -0,0 +1,99 @@
|
||||
// client/src/input/keymap.rs
|
||||
|
||||
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
|
||||
KeyCode::KEY_A => Some(0x04),
|
||||
KeyCode::KEY_B => Some(0x05),
|
||||
KeyCode::KEY_C => Some(0x06),
|
||||
KeyCode::KEY_D => Some(0x07),
|
||||
KeyCode::KEY_E => Some(0x08),
|
||||
KeyCode::KEY_F => Some(0x09),
|
||||
KeyCode::KEY_G => Some(0x0A),
|
||||
KeyCode::KEY_H => Some(0x0B),
|
||||
KeyCode::KEY_I => Some(0x0C),
|
||||
KeyCode::KEY_J => Some(0x0D),
|
||||
KeyCode::KEY_K => Some(0x0E),
|
||||
KeyCode::KEY_L => Some(0x0F),
|
||||
KeyCode::KEY_M => Some(0x10),
|
||||
KeyCode::KEY_N => Some(0x11),
|
||||
KeyCode::KEY_O => Some(0x12),
|
||||
KeyCode::KEY_P => Some(0x13),
|
||||
KeyCode::KEY_Q => Some(0x14),
|
||||
KeyCode::KEY_R => Some(0x15),
|
||||
KeyCode::KEY_S => Some(0x16),
|
||||
KeyCode::KEY_T => Some(0x17),
|
||||
KeyCode::KEY_U => Some(0x18),
|
||||
KeyCode::KEY_V => Some(0x19),
|
||||
KeyCode::KEY_W => Some(0x1A),
|
||||
KeyCode::KEY_X => Some(0x1B),
|
||||
KeyCode::KEY_Y => Some(0x1C),
|
||||
KeyCode::KEY_Z => Some(0x1D),
|
||||
|
||||
// Number row
|
||||
KeyCode::KEY_1 => Some(0x1E),
|
||||
KeyCode::KEY_2 => Some(0x1F),
|
||||
KeyCode::KEY_3 => Some(0x20),
|
||||
KeyCode::KEY_4 => Some(0x21),
|
||||
KeyCode::KEY_5 => Some(0x22),
|
||||
KeyCode::KEY_6 => Some(0x23),
|
||||
KeyCode::KEY_7 => Some(0x24),
|
||||
KeyCode::KEY_8 => Some(0x25),
|
||||
KeyCode::KEY_9 => Some(0x26),
|
||||
KeyCode::KEY_0 => Some(0x27),
|
||||
|
||||
// Common punctuation
|
||||
KeyCode::KEY_ENTER => Some(0x28),
|
||||
KeyCode::KEY_ESC => Some(0x29),
|
||||
KeyCode::KEY_BACKSPACE => Some(0x2A),
|
||||
KeyCode::KEY_TAB => Some(0x2B),
|
||||
KeyCode::KEY_SPACE => Some(0x2C),
|
||||
KeyCode::KEY_MINUS => Some(0x2D),
|
||||
KeyCode::KEY_EQUAL => Some(0x2E),
|
||||
KeyCode::KEY_LEFTBRACE => Some(0x2F),
|
||||
KeyCode::KEY_RIGHTBRACE => Some(0x30),
|
||||
KeyCode::KEY_BACKSLASH => Some(0x31),
|
||||
KeyCode::KEY_SEMICOLON => Some(0x33),
|
||||
KeyCode::KEY_APOSTROPHE => Some(0x34),
|
||||
KeyCode::KEY_GRAVE => Some(0x35),
|
||||
KeyCode::KEY_COMMA => Some(0x36),
|
||||
KeyCode::KEY_DOT => Some(0x37),
|
||||
KeyCode::KEY_SLASH => Some(0x38),
|
||||
|
||||
// Function keys
|
||||
KeyCode::KEY_CAPSLOCK => Some(0x39),
|
||||
KeyCode::KEY_F1 => Some(0x3A),
|
||||
KeyCode::KEY_F2 => Some(0x3B),
|
||||
KeyCode::KEY_F3 => Some(0x3C),
|
||||
KeyCode::KEY_F4 => Some(0x3D),
|
||||
KeyCode::KEY_F5 => Some(0x3E),
|
||||
KeyCode::KEY_F6 => Some(0x3F),
|
||||
KeyCode::KEY_F7 => Some(0x40),
|
||||
KeyCode::KEY_F8 => Some(0x41),
|
||||
KeyCode::KEY_F9 => Some(0x42),
|
||||
KeyCode::KEY_F10 => Some(0x43),
|
||||
KeyCode::KEY_F11 => Some(0x44),
|
||||
KeyCode::KEY_F12 => Some(0x45),
|
||||
|
||||
// We'll handle modifiers (ctrl, shift, alt, meta) in `is_modifier()`
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// If a key is a modifier, return the bit(s) to set in HID byte[0].
|
||||
pub fn is_modifier(key: KeyCode) -> Option<u8> {
|
||||
match key {
|
||||
KeyCode::KEY_LEFTCTRL => Some(0x01),
|
||||
KeyCode::KEY_LEFTSHIFT => Some(0x02),
|
||||
KeyCode::KEY_LEFTALT => Some(0x04),
|
||||
KeyCode::KEY_LEFTMETA => Some(0x08),
|
||||
KeyCode::KEY_RIGHTCTRL => Some(0x10),
|
||||
KeyCode::KEY_RIGHTSHIFT => Some(0x20),
|
||||
KeyCode::KEY_RIGHTALT => Some(0x40),
|
||||
KeyCode::KEY_RIGHTMETA => Some(0x80),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
2
client/src/input/mod.rs
Normal file
2
client/src/input/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod keyboard;
|
||||
pub mod keymap;
|
||||
6
client/src/lib.rs
Normal file
6
client/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod app;
|
||||
pub mod input;
|
||||
|
||||
pub use app::NavkaClientApp;
|
||||
@ -1,45 +1,13 @@
|
||||
//! navka-client – forward keyboard/mouse HID reports to navka-server
|
||||
//! main.rs — entry point for navka-client
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use anyhow::Result;
|
||||
use navka_common::navka::{relay_client::RelayClient, HidReport};
|
||||
use tokio::{sync::mpsc, time::sleep};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::{transport::Channel, Request};
|
||||
use navka_client::NavkaClientApp;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// -- server address comes from CLI arg, env or falls back to localhost
|
||||
let addr = std::env::args()
|
||||
.nth(1)
|
||||
.or_else(|| std::env::var("NAVKA_SERVER_ADDR").ok())
|
||||
.unwrap_or_else(|| "http://127.0.0.1:50051".to_owned());
|
||||
|
||||
let channel: Channel = Channel::from_shared(addr)?
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
// mpsc -> ReceiverStream -> bidirectional gRPC
|
||||
let (tx, rx) = mpsc::channel::<HidReport>(32);
|
||||
let outbound = ReceiverStream::new(rx);
|
||||
let mut inbound = RelayClient::new(channel)
|
||||
.stream(Request::new(outbound))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
// demo: press & release ‘a’
|
||||
tokio::spawn(async move {
|
||||
let press_a = HidReport { data: vec![0, 0, 0x04, 0, 0, 0, 0, 0] };
|
||||
let release = HidReport { data: vec![0; 8] };
|
||||
tx.send(press_a).await.ok();
|
||||
sleep(std::time::Duration::from_millis(100)).await;
|
||||
tx.send(release).await.ok();
|
||||
});
|
||||
|
||||
while let Some(report) = inbound.message().await? {
|
||||
tracing::info!(bytes=?report.data, "echo from server");
|
||||
}
|
||||
Ok(())
|
||||
let mut app = NavkaClientApp::new()?;
|
||||
app.run().await
|
||||
}
|
||||
|
||||
13
client/src/tests/integration_test.rs
Normal file
13
client/src/tests/integration_test.rs
Normal file
@ -0,0 +1,13 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::input::keymap::{keycode_to_usage, is_modifier};
|
||||
use evdev::Key;
|
||||
|
||||
#[test]
|
||||
fn test_keycode_mapping() {
|
||||
assert_eq!(keycode_to_usage(Key::KEY_A), Some(0x04));
|
||||
assert_eq!(keycode_to_usage(Key::KEY_Z), Some(0x1D));
|
||||
assert_eq!(keycode_to_usage(Key::KEY_LEFTCTRL), Some(0)); // this is "handled" by is_modifier
|
||||
assert!(is_modifier(Key::KEY_LEFTCTRL).is_some());
|
||||
}
|
||||
}
|
||||
1
client/src/tests/mod.rs
Normal file
1
client/src/tests/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod integration_test;
|
||||
@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "navka-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
name = "navka_common"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
tonic = "0.11"
|
||||
prost = "0.12"
|
||||
tonic = "0.13"
|
||||
prost = "0.13"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.11"
|
||||
tonic-build = "0.13"
|
||||
|
||||
[lib]
|
||||
name = "navka_common"
|
||||
path = "src/lib.rs"
|
||||
name = "navka_common"
|
||||
path = "src/lib.rs"
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
[package]
|
||||
name = "navka-server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
name = "navka_server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.45", features = ["full", "fs"] }
|
||||
tokio-stream = "0.1"
|
||||
tonic = { version = "0.11", features = ["transport"] }
|
||||
tonic = { version = "0.13", features = ["transport"] }
|
||||
anyhow = "1.0"
|
||||
navka-common = { path = "../common" }
|
||||
navka_common = { path = "../common" }
|
||||
tracing = { version = "0.1", features = ["std"] }
|
||||
tracing-subscriber = "0.3"
|
||||
libc = "0.2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user