No description
  • Rust 89.4%
  • Svelte 5.7%
  • TypeScript 4.7%
  • HTML 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
sid 4293427d6a Fix cert/static-asset path resolution for packaged (non-dev) binaries
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.
2026-08-20 07:56:07 -06:00
bridge Fix cert/static-asset path resolution for packaged (non-dev) binaries 2026-08-20 07:56:07 -06:00
crates spnav-proto: add Phase 2 request/response protocol (sens/evmask/dev queries) 2026-08-20 07:47:23 -06:00
web Add SvelteKit static config UI for spacenavd-rs bridge 2026-08-20 07:46:15 -06:00
.gitignore Scaffold Cargo workspace for the Rust rewrite (spnav-proto/wamp/controller/bridge) 2026-08-20 06:38:07 -06:00
Cargo.lock bridge: wire up Phase 2 config UI (GET/POST /api/config, serve web/build at /config) 2026-08-20 07:54:18 -06:00
Cargo.toml bridge: wire up Phase 2 config UI (GET/POST /api/config, serve web/build at /config) 2026-08-20 07:54:18 -06:00
README.md bridge: wire up Phase 2 config UI (GET/POST /api/config, serve web/build at /config) 2026-08-20 07:54:18 -06:00

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 — why spacenavd itself 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 replacing spnavcfg, Phase 3 = optional LCD support later).
  • ../spacenavd/src/proto.h — the daemon's wire protocol. Single source of truth for crates/spnav-proto. Note the fork-local profile/LCD requests now live at 0x6000+ (renumbered in commit e36eccd, out of upstream's reserved 0x1000 block — 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 around libspnav. Reference only, also stale (2022).
  • kKdH/spacenav-rs and buumotfa4djz6-tech/spnav-rs (async client, more recently active) — worth a look for crates/spnav-proto API 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 why crates/wamp exists 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 via spnav-proto's SpnavClient::get_sensitivity/set_sensitivity/get_evmask/ set_evmask, and reports connected-device info via dev_name/dev_usbid (best-effort: a device query failure collapses to "device": null rather than a 503, since "no device plugged in" is a normal documented state). Each request opens its own short-lived SpnavClient::connect(), per spnav-proto's documented constraint that request/response calls must not share a socket with the motion-event stream reader. Sensitivity is validated to the 0.1-10.0 range (matching the frontend's range input), rejected out-of-range with 400; an unreachable daemon (the expected state in any environment without spacenavd running, including this sandbox) returns a clean 503 { "error": ... }, verified deterministically by cargo test -p bridge.
  • The SvelteKit static site (web/build/, built via pnpm run build) is served at /config, with SPA fallback to 200.html for 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/config 503 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/config returns a clean 503, /config serves real SvelteKit HTML, and /, /3dconnexion/nlproxy still return 200.
  • web/build/ is not committed — it stays gitignored (web/.gitignore) and must be built with pnpm install && pnpm run build (or npm) before packaging/running from a fresh checkout. Flag for whoever updates the Nix packaging (pkgs/spacenav-ws-rs.nix in diy-nixos-installer): it needs an npm/pnpm build step added for web/ so web/build/ exists at build time, or the /config route 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 real spacenavd (or spacenavd-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's view.affine is 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-reload is 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.