- Rust 89.4%
- Svelte 5.7%
- TypeScript 4.7%
- HTML 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
env!("CARGO_MANIFEST_DIR") is a compile-time constant pointing at the build
sandbox path — fine for `cargo run` in place, but a Nix-packaged (or any
installed/relocated) binary would compile in an ephemeral sandbox that
doesn't exist at runtime, so cert loading and the SvelteKit static-file
serving would silently fail to find their assets. Both now check a runtime
SPACENAV_WS_ASSETS_DIR env var first, falling back to the old
CARGO_MANIFEST_DIR-relative behavior for local dev.
|
||
| bridge | ||
| crates | ||
| web | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
spacenav-ws-rs
Rust rewrite of ../spacenav-ws (the Python/FastAPI websocket bridge that feeds
3Dconnexion SpaceMouse motion into browser-based CAD, currently Onshape). This is
Phase 1 of the plan below — a from-scratch Rust implementation developed alongside
the working Python version, not a replacement for it yet. The Python bridge keeps
running real hardware sessions until this reaches verified feature parity.
Read these first
../RESEARCH.md— full fork-research writeup for the whole SpaceMouse stack (spacenavd/spnavcfg/spacenav-ws): what was cherry-picked from which fork and why, the protocol-blast-radius analysis (§4 — whyspacenavditself stays C and untouched), and the architecture/language consensus (§6) this rewrite follows.../spacenav-ws/docs/RUST_REWRITE_PLAN.md— the actual implementation plan: module-by-module breakdown of the current Python code, crate choices, and the phased plan (Phase 1 = this workspace, Phase 2 = fold in a config web UI replacingspnavcfg, Phase 3 = optional LCD support later).../spacenavd/src/proto.h— the daemon's wire protocol. Single source of truth forcrates/spnav-proto. Note the fork-local profile/LCD requests now live at0x6000+(renumbered in commite36eccd, out of upstream's reserved0x1000block — see RESEARCH.md §3).../spacenav-ws/src/spacenav_ws/— the Python baseline being ported. Read the specific file named in each crate's own README before touching that crate (see below) — don't work from memory of the plan doc, read the actual source.
Prior art (read before writing new code, don't reinvent)
- tophcodes/space-elevator — Rust + Nix-flake project that has already built a SpaceMouse Enterprise LCD driver and scoped an Onshape websocket bridge. The most relevant prior art that exists; skim it even for Phase 1 (bridge architecture), not just the Phase 3 LCD work.
mfs/spacenav— small, stale (2016) Rust client crate for spacenavd's non-X protocol. Reference only, not a dependency (dead, 6 downloads/90d).sjkillen/spacenav-plus(libspnav-rust) — safe idiomatic Rust wrapper aroundlibspnav. Reference only, also stale (2022).kKdH/spacenav-rsandbuumotfa4djz6-tech/spnav-rs(async client, more recently active) — worth a look forcrates/spnav-protoAPI design.crates.io/spacemouse-proxy(repo) — checked 2026-08-20: macOS-only, talks to Apple's native 3DConnexion framework via FFI/ObjC (not spacenavd's Unix socket protocol at all), built for Figma specifically. Not directly reusable code, but confirms the general shape (raw JSON motion events over a local websocket, ~60fps, EMA-smoothed) is a reasonable pattern elsewhere — our protocol is constrained instead by Onshape's WAMP-v1-ish client, which is whycrates/wampexists and isn't just raw JSON.
Workspace layout
spacenav-ws-rs/
├── crates/
│ ├── spnav-proto/ — spacenavd Unix socket client: motion/button event parsing
│ │ (Phase 1), daemon config protocol (Phase 2)
│ ├── wamp/ — hand-rolled WAMP v1 subset Onshape's client actually speaks
│ │ (message types, registry/dispatch, RPC call/result gating)
│ └── controller/ — the CAD camera math: affine/pivot/rotation/pan/zoom, using
│ `nalgebra`; the highest-value, highest-risk port (needs
│ numerical verification against the Python implementation)
└── bridge/ — the axum binary: HTTP/websocket server, TLS, CLI, wires the
three crates together into the actual `spacenav-ws` replacement
Dependency order: spnav-proto and wamp are independent of each other and of
everything else. controller depends on both. bridge depends on all three. Each
crate's own README.md (once added) points at the exact Python file(s) it replaces and
what "done" looks like for that crate specifically.
Status
Phase 1 implemented (2026-08-20) — all four crates have real implementations,
cargo test --workspace passes (54 unit tests total across spnav-proto/wamp/
controller/bridge), cargo build --workspace links a real spacenav-ws binary, and
a manual HTTPS smoke test (curl against the running binary) confirmed the
/3dconnexion/nlproxy JSON endpoint, the debug HTML page, and the CORS
private-network-access preflight header all work.
Phase 2 (config UI) wired up (2026-08-20) — the bridge binary now serves the
SvelteKit config UI (built separately under web/) and its API:
GET/POST /api/config— reads/writes sensitivity and the motion/button event mask viaspnav-proto'sSpnavClient::get_sensitivity/set_sensitivity/get_evmask/set_evmask, and reports connected-device info viadev_name/dev_usbid(best-effort: a device query failure collapses to"device": nullrather than a 503, since "no device plugged in" is a normal documented state). Each request opens its own short-livedSpnavClient::connect(), perspnav-proto's documented constraint that request/response calls must not share a socket with the motion-event stream reader. Sensitivity is validated to the0.1-10.0range (matching the frontend's range input), rejected out-of-range with400; an unreachable daemon (the expected state in any environment without spacenavd running, including this sandbox) returns a clean503 { "error": ... }, verified deterministically bycargo test -p bridge.- The SvelteKit static site (
web/build/, built viapnpm run build) is served at/config, with SPA fallback to200.htmlfor unmatched sub-paths (client-side routing/refreshes)./is left untouched — it still serves the debug HTML page / WAMP websocket upgrade, since Onshape's client negotiates its websocket against that exact path. bridge's test suite (11 tests) now also covers: the/api/config503 path with no spacenavd socket present, sensitivity out-of-range 400 validation (pure unit test, no daemon needed), evmask bit decode/encode, and real static-file responses from/config(index.html) and its SPA fallback. A manual smoke test (cargo run -p bridge -- serve, curled, then killed) confirmed/api/configreturns a clean 503,/configserves real SvelteKit HTML, and/,/3dconnexion/nlproxystill return200.web/build/is not committed — it stays gitignored (web/.gitignore) and must be built withpnpm install && pnpm run build(ornpm) before packaging/running from a fresh checkout. Flag for whoever updates the Nix packaging (pkgs/spacenav-ws-rs.nixindiy-nixos-installer): it needs an npm/pnpm build step added forweb/soweb/build/exists at build time, or the/configroute will 404 everything (ServeDir over a missing directory) in the packaged binary — this wiring task did not touch the Nix side.
Not yet verified — needs a human with real hardware before this replaces the Python bridge:
- No actual spacenavd socket or live Onshape/browser WAMP session was available to the
implementing agents — the websocket/RPC/motion-event wiring is exercised only by
unit tests and one manual HTTPS smoke test, not an end-to-end real session. Same
caveat applies to Phase 2:
/api/config's actual read/write round-trip against a realspacenavd(orspacenavd-rs) socket is unverified — only the connection-failure path and pure validation/decoding logic could be tested in this sandbox. controller's camera math (rotation/pan/zoom) is unit-tested for internal consistency (pivot matrices, Euler convention, SVD invariance) but the overall rotation/pan direction and handedness, and the assumption that Onshape'sview.affineis camera→world in row-vector-major layout, are only provably right by feel on real hardware — see the crate's own doc comments and RESEARCH.md §7 for the exact list of what to check first.--hot-reloadis a deliberate no-op (axum has no uvicorn-style process-reload equivalent) — logged as a warning, not silently ignored.
Nothing published/pushed anywhere — local commits on master in this repo.