poc to app overhaul
This commit is contained in:
parent
a8db1ba2b3
commit
c1580fb2bf
10
client/Cargo.toml
Normal file
10
client/Cargo.toml
Normal file
@ -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"
|
||||
32
client/src/main.rs
Normal file
32
client/src/main.rs
Normal file
@ -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?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
common/Cargo.toml
Normal file
12
common/Cargo.toml
Normal file
@ -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"
|
||||
8
common/build.rs
Normal file
8
common/build.rs
Normal file
@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
tonic_build::configure()
|
||||
.build_client(true)
|
||||
.build_server(true)
|
||||
.out_dir("src/")
|
||||
.compile(&["proto/navka.proto"], &["proto"])
|
||||
.unwrap();
|
||||
}
|
||||
8
common/navka.proto
Normal file
8
common/navka.proto
Normal file
@ -0,0 +1,8 @@
|
||||
syntax = "proto3";
|
||||
package navka;
|
||||
|
||||
message HidReport { bytes data = 1; }
|
||||
|
||||
service Relay {
|
||||
rpc Stream (stream HidReport) returns (stream HidReport);
|
||||
}
|
||||
@ -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" <<EOF
|
||||
[Unit]
|
||||
Description=Navka keyboard relay
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/opt/navka-relay/navka-relay.sh
|
||||
Restart=on-failure
|
||||
EnvironmentFile=%h/.config/navka-relay.env
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable navka-relay >/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
|
||||
|
||||
@ -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
|
||||
|
||||
26
scripts/install-client.sh
Executable file
26
scripts/install-client.sh
Executable file
@ -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."
|
||||
39
scripts/install-server.sh
Executable file
39
scripts/install-server.sh
Executable file
@ -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."
|
||||
9
server/Cargo.toml
Normal file
9
server/Cargo.toml
Normal file
@ -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"
|
||||
43
server/src/main.rs
Normal file
43
server/src/main.rs
Normal file
@ -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<Result<HidReport, Status>>;
|
||||
|
||||
async fn stream(
|
||||
&self,
|
||||
request: Request<tonic::Streaming<HidReport>>,
|
||||
) -> Result<Response<Self::StreamStream>, 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(())
|
||||
}
|
||||
14
server/tests/hid.rs
Normal file
14
server/tests/hid.rs
Normal file
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user