diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..e2f1a1e --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "navka-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +navka-common = { path = "../common" } +evdev = "0.12" +tonic = "0.11" diff --git a/client/src/main.rs b/client/src/main.rs new file mode 100644 index 0000000..3485a98 --- /dev/null +++ b/client/src/main.rs @@ -0,0 +1,32 @@ +use evdev::{Device, InputEventKind}; +use navka_common::navka::{relay_client::RelayClient, HidReport}; +use tonic::transport::Channel; + +const KEYBOARD: &str = "/dev/input/event4"; // adjust +const NAVKA_IP: &str = "192.168.42.253:4000"; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // open evdev + let mut dev = Device::open(KEYBOARD)?; + + // connect to navka + let channel = Channel::from_shared(format!("http://{NAVKA_IP}"))? + .connect().await?; + let (mut tx, _rx) = RelayClient::new(channel).stream().await?.into_inner(); + + loop { + for ev in dev.fetch_events()? { + if let InputEventKind::Key(code) = ev.kind() { + // simple map: only letters a-z for PoC + let usage = match code.0 { + 30..=55 => (code.0 - 26) as u8, // HID usage 0x04..0x1d + _ => continue, + }; + let mut buf = [0u8; 8]; + buf[2] = if ev.value() == 1 { usage } else { 0 }; + tx.send(HidReport { data: buf.to_vec() }).await?; + } + } + } +} diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000..9438985 --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "navka-common" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +[dependencies] +tonic = "0.11" +prost = "0.12" + +[build-dependencies] +tonic-build = "0.11" diff --git a/common/build.rs b/common/build.rs new file mode 100644 index 0000000..b713560 --- /dev/null +++ b/common/build.rs @@ -0,0 +1,8 @@ +fn main() { + tonic_build::configure() + .build_client(true) + .build_server(true) + .out_dir("src/") + .compile(&["proto/navka.proto"], &["proto"]) + .unwrap(); +} diff --git a/common/navka.proto b/common/navka.proto new file mode 100644 index 0000000..cf1f202 --- /dev/null +++ b/common/navka.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package navka; + +message HidReport { bytes data = 1; } + +service Relay { + rpc Stream (stream HidReport) returns (stream HidReport); +} diff --git a/install-navka-relay.sh b/install-navka-relay.sh deleted file mode 100755 index 73714c8..0000000 --- a/install-navka-relay.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ---------- paths ---------- -SRC_DIR=$(pwd) # where the script is launched -APPDIR=/opt/navka-relay -VENV=$APPDIR/venv -SERVICE=$HOME/.config/systemd/user/navka-relay.service -USR=${SUDO_USER:-$USER} - -green(){ printf '\e[1;32m==>\e[0m %s\n' "$*"; } - -############################################################################## -green "1. Packages (python + evdev) …" -############################################################################## -sudo pacman -Sy --needed --noconfirm python python-evdev >/dev/null - -############################################################################## -green "2. Install /opt/navka-relay …" -############################################################################## -sudo mkdir -p "$APPDIR" -sudo chown "$USR":"$USR" "$APPDIR" - -# use a symlink when run from dev folder so edits are live -if [[ $SRC_DIR != "$APPDIR" ]]; then - ln -sf "$SRC_DIR/navka-relay.sh" "$APPDIR/navka-relay.sh" -fi -chmod +x "$SRC_DIR/navka-relay.sh" - -############################################################################## -green "3. Build virtual-env (system pkgs visible) …" -############################################################################## -if [[ ! -d $VENV/bin ]]; then - python -m venv --system-site-packages "$VENV" -fi - -# ensure evdev importable inside venv (pacman pkg provides it; pip fallback) -"$VENV/bin/pip" install --quiet --disable-pip-version-check --upgrade pip >/dev/null -"$VENV/bin/python" - <<'PY' -import importlib, subprocess, sys -try: - importlib.import_module('evdev') -except ImportError: - subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'evdev']) -PY - -############################################################################## -green "4. systemd *user* unit …" -############################################################################## -mkdir -p "$(dirname "$SERVICE")" -cat >"$SERVICE" </dev/null 2>&1 || true - -green "✔ Install complete" -cat <<'TXT' -Start relay : systemctl --user start navka-relay -Stop relay : systemctl --user stop navka-relay -Manual run : /opt/navka-relay/navka-relay.sh -Variables : NAVKA_IP=… NAVKA_PORT=… NAVKA_KBD=… navka-relay.sh -TXT - diff --git a/navka-relay.sh b/navka-relay.sh deleted file mode 100755 index 2f9cf1b..0000000 --- a/navka-relay.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Wrapper that activates local venv then executes embedded Python - -DIR=$(dirname "$(readlink -f "$0")") -VENV=/opt/navka-relay/venv -PY="$VENV/bin/python" - -if [[ ! -x $PY ]]; then - echo "❌ venv missing: run install-navka-relay.sh first." >&2 - exit 1 -fi - -# shellcheck disable=SC2091 -exec $PY - <<'PYCODE' -import os, socket, selectors, signal, sys -from evdev import InputDevice, ecodes - -# --------- config via env ---------- -DEV = os.getenv("NAVKA_KBD", "/dev/input/event4") -IP = os.getenv("NAVKA_IP", "192.168.42.253") -PORT = int(os.getenv("NAVKA_PORT", "4000")) -# ----------------------------------- - -# full HID tables (letters, numbers, F1-F12, arrows, etc.) -HID = {**{getattr(ecodes,f"KEY_F{i}"):i+53 for i in range(1,13)}} -for ev,h in { - **{k:4+i for i,k in enumerate(range(ecodes.KEY_A,ecodes.KEY_Z+1))}, - **{k:30+i for i,k in enumerate(range(ecodes.KEY_1,ecodes.KEY_0+1))}, - ecodes.KEY_ENTER:40, ecodes.KEY_ESC:41, ecodes.KEY_BACKSPACE:42, - ecodes.KEY_TAB:43, ecodes.KEY_SPACE:44, ecodes.KEY_MINUS:45, - ecodes.KEY_EQUAL:46, ecodes.KEY_LEFTBRACE:47, ecodes.KEY_RIGHTBRACE:48, - ecodes.KEY_BACKSLASH:49, ecodes.KEY_SEMICOLON:51, ecodes.KEY_APOSTROPHE:52, - ecodes.KEY_GRAVE:53, ecodes.KEY_COMMA:54, ecodes.KEY_DOT:55, - ecodes.KEY_SLASH:56, ecodes.KEY_RIGHT:79, ecodes.KEY_LEFT:80, - ecodes.KEY_DOWN:81, ecodes.KEY_UP:82 -}.items(): HID.setdefault(ev, h) - -MOD = { ecodes.KEY_LEFTCTRL:1, ecodes.KEY_LEFTSHIFT:2, ecodes.KEY_LEFTALT:4, - ecodes.KEY_LEFTMETA:8, ecodes.KEY_RIGHTCTRL:16, ecodes.KEY_RIGHTSHIFT:32, - ecodes.KEY_RIGHTALT:64, ecodes.KEY_RIGHTMETA:128 } - -try: - dev = InputDevice(DEV) -except FileNotFoundError: - sys.exit(f"Input device {DEV} not found") - -dev.grab() -sock = socket.create_connection((IP,PORT), timeout=5) -sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - -mods, keys = 0, [] - -def send(): - sock.sendall(bytes([mods,0]+keys[:6]+[0]*(6-len(keys)))) - -def quit_handler(sig, frame): # graceful Ctrl-C - dev.ungrab(); sock.close(); sys.exit(0) - -signal.signal(signal.SIGINT, quit_handler) - -for ev in dev.read_loop(): - if ev.type != ecodes.EV_KEY: continue - pressed = ev.value == 1 - if ev.code in MOD: - mods = mods | MOD[ev.code] if pressed else mods & ~MOD[ev.code] - elif ev.code in HID: - h = HID[ev.code] - if pressed and h not in keys: keys.insert(0,h) - elif not pressed and h in keys: keys.remove(h) - else: - continue - send() -PYCODE - diff --git a/scripts/install-client.sh b/scripts/install-client.sh new file mode 100755 index 0000000..1502b10 --- /dev/null +++ b/scripts/install-client.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +sudo pacman --needed --noconfirm -S git rustup protobuf gcc evtest +rustup toolchain install stable + +WORK=$HOME/.local/src/navka +if [ ! -d "$WORK" ]; then git clone ssh://git@scm.bstein.dev:2242/brad_stein/navka.git "$WORK"; fi +cd "$WORK" && git pull + +cargo build --release --manifest-path client/Cargo.toml +install -Dm755 target/release/navka-client ~/.local/bin/navka-client + +mkdir -p ~/.config/systemd/user +cat <<'EOF' > ~/.config/systemd/user/navka-client.service +[Unit] +After=network.target +[Service] +ExecStart=%h/.local/bin/navka-client +Restart=on-failure +[Install] +WantedBy=default.target +EOF + +systemctl --user daemon-reload +systemctl --user enable --now navka-client.service +echo "navka-client installed." diff --git a/scripts/install-server.sh b/scripts/install-server.sh new file mode 100755 index 0000000..6906380 --- /dev/null +++ b/scripts/install-server.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +sudo pacman --needed --noconfirm -S git rustup protobuf gcc +rustup toolchain install stable + +WORK=/opt/navka +if [ ! -d "$WORK" ]; then sudo git clone ssh://git@scm.bstein.dev:2242/brad_stein/navka.git "$WORK"; fi +cd "$WORK" && sudo git pull + +cargo build --release --manifest-path server/Cargo.toml +sudo install -Dm755 target/release/navka-server /usr/local/bin/navka-server +sudo install -Dm755 gadget/navka-gadget.sh /usr/local/bin/navka-gadget.sh + +cat <<'EOF' | sudo tee /etc/systemd/system/navka-server.service +[Unit] +After=network.target navka-gadget.service +[Service] +ExecStart=/usr/local/bin/navka-server +Restart=always +User=root +[Install] +WantedBy=multi-user.target +EOF + +cat <<'EOF' | sudo tee /etc/systemd/system/navka-gadget.service +[Unit] +Description=USB Gadget +[Service] +Type=oneshot +ExecStart=/usr/local/bin/navka-gadget.sh +RemainAfterExit=yes +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable --now navka-gadget.service navka-server.service +echo "navka-server installed." diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000..b5e2907 --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "navka-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +navka-common = { path = "../common" } +tokio-stream = "0.1" diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000..831c715 --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,43 @@ +use navka_common::navka::{relay_server::{Relay, RelayServer}, HidReport}; +use tokio::{fs::OpenOptions, io::AsyncWriteExt, sync::mpsc}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::{transport::Server, Request, Response, Status}; + +const HID_DEV: &str = "/dev/hidg0"; + +#[derive(Default)] +struct RelaySvc; + +#[tonic::async_trait] +impl Relay for RelaySvc { + type StreamStream = ReceiverStream>; + + async fn stream( + &self, + request: Request>, + ) -> Result, Status> { + let mut inbound = request.into_inner(); + let (tx, rx) = mpsc::channel(8); + + // writer task: client → hidg0 + tokio::spawn(async move { + let mut hid = OpenOptions::new().write(true).open(HID_DEV).await.unwrap(); + while let Some(Ok(report)) = inbound.message().await { + hid.write_all(&report.data).await.unwrap(); + } + }); + + // nothing to send back yet + Ok(Response::new(ReceiverStream::new(rx))) + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let addr = "0.0.0.0:4000".parse()?; + Server::builder() + .add_service(RelayServer::new(RelaySvc::default())) + .serve(addr) + .await?; + Ok(()) +} diff --git a/server/tests/hid.rs b/server/tests/hid.rs new file mode 100644 index 0000000..a9361fe --- /dev/null +++ b/server/tests/hid.rs @@ -0,0 +1,14 @@ +#[tokio::test] +async fn hid_roundtrip() { + use navka_common::navka::*; + use navka_server::RelaySvc; // export the struct in lib.rs + let svc = RelaySvc::default(); + let (mut cli, srv) = tonic::transport::Channel::balance_channel(1); + tokio::spawn(tonic::transport::server::Server::builder() + .add_service(relay_server::RelayServer::new(svc)) + .serve_with_incoming(srv)); + + let (mut tx, mut rx) = relay_client::RelayClient::new(cli).stream().await.unwrap().into_inner(); + tx.send(HidReport { data: vec![0,0,4,0,0,0,0,0] }).await.unwrap(); + assert!(rx.message().await.unwrap().is_none()); // nothing echoed yet +}