No description
  • Rust 99.7%
  • Nix 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
sid b3d04cff7e Implement spnav-config: file-based config for sensitivity, dead-zone, invert, socket path
Adds the spnav-config crate: a simplified, spnavrc-inspired key=value config
file parser (sensitivity, per-axis sensitivity, dead-zone, invert-trans/rot,
swap-yz, socket path), a default search path
(~/.config/spacenavd-rs/config.conf, else /etc/spacenavd-rs.conf), and a
thiserror ConfigError where a missing file falls back to defaults but a
malformed existing file logs per-line warnings without aborting. Wires it
into bin/spacenavd-rs via a new --config flag: sensitivity is applied as a
multiplier on every emitted MotionEvent axis, and the config's socket path
is used as the --socket-path default (CLI flag still wins if both given).
Module doc comments document exactly which C daemon cfgfile.c options are
covered vs. deferred (profiles, per-axis dead-zone/axis/button maps, kbmap,
led/grab/serial/device-id). README updated accordingly.

Claude-Session: https://claude.ai/code/session_01XX7nhvqCdu37uKzXndazX2
2026-08-20 07:45:18 -06:00
bin/spacenavd-rs Implement spnav-config: file-based config for sensitivity, dead-zone, invert, socket path 2026-08-20 07:45:18 -06:00
crates Implement spnav-config: file-based config for sensitivity, dead-zone, invert, socket path 2026-08-20 07:45:18 -06:00
.gitignore Scaffold Cargo workspace for the cross-platform Rust daemon (spnav-devices/spnav-hid/spnav-proto/spnav-server/spacenavd-rs) 2026-08-20 07:13:42 -06:00
Cargo.lock Implement spnav-config: file-based config for sensitivity, dead-zone, invert, socket path 2026-08-20 07:45:18 -06:00
Cargo.toml Implement spnav-config: file-based config for sensitivity, dead-zone, invert, socket path 2026-08-20 07:45:18 -06:00
README.md Implement spnav-config: file-based config for sensitivity, dead-zone, invert, socket path 2026-08-20 07:45:18 -06:00
shell.nix Scaffold Cargo workspace for the cross-platform Rust daemon (spnav-devices/spnav-hid/spnav-proto/spnav-server/spacenavd-rs) 2026-08-20 07:13:42 -06:00

spacenavd-rs

A new, standalone, cross-platform Rust reimplementation of the SpaceMouse USB/HID driver + daemon — not a replacement of ../spacenavd (the existing C daemon stays as-is; see ../RESEARCH.md §6 for why). This is a separate project aimed at platforms the C daemon serves poorly today (its dev_usb_darwin.c is 97 lines and multiple community forks report "macOS support, not working"; FreeBSD support exists but is clearly secondary to Linux).

Read these first

  • ../RESEARCH.md — background on the whole SpaceMouse stack and why the existing C daemon isn't being rewritten in place (§6), plus the sibling spacenav-ws-rs Rust bridge project (§7) — that project's own README documents useful Rust ecosystem findings (nusb, hidapi, prior art) that fed into the decisions here.
  • ../spacenavd/src/dev.c — the known-device table (usbdb[]) and blacklist this project's spnav-devices crate ports. See §1 below for the full table reproduced inline (don't re-derive it from the C source — it's already extracted).
  • ../spacenavd/src/proto.h — the wire protocol spnav-proto in this workspace ports to the server side. Note the fork-local profile/LCD requests live at 0x6000+ in the C daemon (post-renumbering, see RESEARCH.md §3) — not relevant here, this project doesn't carry that fork feature (it's a from-scratch reimplementation of the upstream FreeSpacenav protocol only).

Architecture decisions (Opus research, 2026-08-20)

HID/USB access: hidapi, not nusb. nusb (favored in earlier bridge-project research) turns out to be the wrong layer for HID-class devices specifically: its own docs say so directly, it requires WinUSB rebinding on Windows (replacing the OS's own HID handling — a non-starter for a device you want normal OS coexistence with), and it cannot detach the kernel HID driver on macOS/Windows at all. hidapi (the hidapi/hidapi-rs crate wrapping the well-established cross-platform C library) is the only ecosystem option that genuinely covers Linux (hidraw), macOS (IOHIDManager), Windows (hid.dll), AND FreeBSD (via its libusb backend) — one crate, all four target families. Cost: one vendored C dependency, statically built by the crate (no system package required). Strong precedent: PySpaceMouse drives every SpaceMouse model on all three major platforms through raw hidapi with per-device axis specs — the same design this project follows.

This means reading raw HID reports, not evdev (which is what the C daemon does on Linux, via dev_usb_linux.c's linux/input.h — Linux-specific and not portable). The per-device HID report decode (report ID 1 = translation, 2 = rotation, 3 = buttons on older devices; a single combined report on newer models) becomes the one thing shared across all four platforms instead of being reimplemented per-OS four times — that's the actual architectural payoff of doing this in Rust at all.

Protocol: stay wire-compatible with proto.h on Unix platforms. On Linux/BSD/macOS, serve the same Unix-socket protocol the C daemon does (v0 raw packets + v1 negotiation via REQ_CHANGE_PROTO) — this is free compatibility with the existing consumer ecosystem (Blender, FreeCAD, OpenSCAD, KiCad, Solvespace, ROS's spacenav_node — see RESEARCH.md §4 for the full list) and the only way to meaningfully validate correctness, since libspnav-based apps become a real conformance test. On Windows (which has no existing Unix-socket-protocol consumers to be compatible with), use a TCP loopback listener instead of a named pipe — simplest, most portable, same event/request semantics. The spnav-server crate is transport-agnostic internally so both listener types share one core.

Prior art: buumotfa4djz6-tech/spnav-rs has a from-scratch Rust implementation of the v0/v1 wire framing plus the REQ_* config API — the best reference (and a ready conformance-test client) for spnav-proto. tophcodes/space-elevator doesn't touch device I/O at all (it's a libspnav client that only owns the Enterprise LCD) but its transport-agnostic message-handler pattern is a useful reference for spnav-server. No cross-platform Rust 6DOF HID driver exists anywhere (verified via broad gh search repos across spacemouse/3dconnexion/spacenav) — this project is the first.

Hotplug: no crate gives this to you for HID devices. hidapi-rs has no hotplug support (upstream RFC/PRs still open as of this writing). Phase 1 uses a polling enumeration loop (1-2s interval) — genuinely adequate for this device class and ships on all four platforms immediately; real push-based hotplug (udev netlink on Linux, IOKit notifications on macOS, WM_DEVICECHANGE on Windows, devd on FreeBSD) is explicit follow-on work, designed for behind the same DeviceEvent stream interface so it's additive later, not a rewrite.

Workspace layout

spacenavd-rs/
├── crates/
│   ├── spnav-devices/  — known-device table (VID/PID → name/flags), blacklist,
│   │                     DF_SWAPYZ/DF_INVYZ-equivalent axis normalization, per-device
│   │                     HID report layout + button-remap hooks (bnhack_smpro/
│   │                     bnhack_sment equivalents)
│   ├── spnav-hid/      — HID backend trait + `hidapi` implementation: enumerate
│   │                     devices matching spnav-devices' table, open, read reports
│   ├── spnav-proto/    — proto.h-compatible wire protocol, SERVER side (this is a
│   │                     separate crate from spacenav-ws-rs's client-side spnav-proto
│   │                     — see "Relationship to spacenav-ws-rs" below)
│   └── spnav-server/   — client registry, event fan-out, per-client sensitivity/
│                         evmask state; Unix socket listener (Linux/BSD/macOS) + TCP
│                         loopback listener (Windows), sharing one transport-agnostic
│                         core
└── bin/spacenavd-rs/   — the daemon binary: polling hotplug loop + HID read loop +
                          server, CLI, logging

Dependency order: spnav-devices, spnav-hid (depends on spnav-devices for the device table), and spnav-proto are the foundation. spnav-server depends on spnav-proto. The binary depends on all four.

Relationship to spacenav-ws-rs

../spacenav-ws-rs/crates/spnav-proto is a client of spacenavd's socket (used by the websocket bridge). This workspace's spnav-proto is a server implementation of the same wire format. They are deliberately kept as separate crates in separate projects for now (this project is meant to "stand alone" per its own brief) rather than factored into a shared crate — but the byte-level wire format must stay identical between them, since both ultimately need to interoperate with the same real-world ecosystem (a spacenav-ws-rs bridge should be able to talk to a spacenavd-rs daemon exactly as it talks to the C one). If they drift, that's a bug in one of them, not an intentional divergence. Unifying them into one shared crate is reasonable future work once both have stabilized.

Phase 1 scope (this implementation pass)

Linux only. hidraw via hidapi, proto v0+v1 Unix-socket server, polling hotplug, the known-device table with axis-normalization flags and button-remap hooks. No LCD/profile support (not part of upstream FreeSpacenav's protocol, that's a fork-only feature on the C daemon side per RESEARCH.md, out of scope for a from-scratch reimplementation), no daemonization/service integration (systemd unit, launchd plist, Windows service, rc.d — all explicit follow-on work per platform).

Explicit follow-on phases, each additive against the trait/transport boundaries designed in Phase 1, not a rewrite:

  • macOS (IOHIDManager backend already covered by hidapi; add IOKit hotplug notifications to replace polling)
  • Windows (hid.dll backend already covered by hidapi; add the TCP loopback listener; add WM_DEVICECHANGE hotplug)
  • FreeBSD (hidapi's libusb backend; add devd-socket hotplug)
  • Per-application profiles, live REQ_SCFG_/REQ_GCFG_ remote config protocol, X11 Magellan protocol support, uinput/keyboard-emu support, per-platform service integration, LCD/profile parity with the C fork if ever wanted

Config file parsing (Phase 2 follow-on, implemented 2026-08-20)

The spnav-config crate adds file-based configuration, loaded once at daemon startup by bin/spacenavd-rs. It is deliberately a much smaller, line-based key = value parser than the C daemon's cfgfile.c tokenizer — see crates/spnav-config/src/ lib.rs's module doc comment for the exhaustive comparison. Summary:

Covered: global sensitivity (f32, default 1.0, applied as a multiplier to every emitted MotionEvent axis), per-class/per-axis sensitivity-translation[-x/y/z] and sensitivity-rotation[-x/y/z], a single global dead-zone (not per-axis), invert-trans/invert-rot (letter combinations of x/y/z), swap-yz, and socket (the Unix socket path — used as the default --socket-path when the CLI flag isn't given explicitly; --socket-path always wins if both are set). # comments, blank-line and whitespace tolerance, and the C daemon's own boolean spellings (true/false, yes/no, on/off) are all supported.

Deferred (explicit future work): per-application profile "Name" class=... end blocks, per-axis dead-zoneN/axismapN/bnmapN/bnactN/kbmapN tables, and the led/grab/serial/device-id/repeat-interval/kbmap_use_x11 settings — none of these have any representation in spnav_config::Config yet. Also out of scope for this pass: the daemon's live REQ_SCFG_*/REQ_GCFG_* remote-config wire protocol (only file-based config loaded once at startup is implemented; spnav-server's ClientState::sensitivity still defaults to 1.0 per-client rather than being seeded from the loaded config — wiring the global config default into new client connections is a small additive follow-up, not done in this pass).

Default search path (spnav_config::default_config_path()): per-user ~/.config/spacenavd-rs/config.conf if it exists, else /etc/spacenavd-rs.conf — a deliberately different filename from the C daemon's own /etc/spnavrc/~/.spnavrc, since this parser does not understand that file's full grammar and silently partially-parsing an existing C-daemon config would be misleading. Override with spacenavd-rs run --config <path>. A missing config file is not an error (logged at info, daemon runs with built-in defaults); a malformed existing file logs a warn per bad line and keeps parsing, mirroring the C daemon's own line-skipping behavior.

Status

Phase 1 implemented (2026-08-20) — all four crates plus the spacenavd-rs binary have real implementations. cargo test --workspace passes (65 unit/integration tests across spnav-devices/spnav-hid/spnav-proto/spnav-server/spacenavd-rs), cargo fmt/cargo clippy -- -D warnings are clean, and cargo build --workspace links a real spacenavd-rs binary. Verified smoke tests (no real hardware available in this environment):

  • spacenavd-rs list-devices runs cleanly and reports "No known SpaceMouse devices found." (not a crash) with nothing plugged in.
  • spacenavd-rs run --socket-path <tmp path> starts, binds and creates the Unix socket file, logs startup; sending it SIGINT (Ctrl-C) logs a clean shutdown message, the process exits promptly (no hang), and the socket file is removed automatically.

Not yet verified — needs a human with a real SpaceMouse before this is trustworthy as a daemon:

  • No real HID device was ever opened or read in this sandbox — HidBackend::open, OpenDevice::read_decoded, the per-device std::thread read loop, and the hotplug-detect/cleanup logic in bin/spacenavd-rs/src/main.rs's hotplug_loop are only exercised by list_devices() returning an empty list. Plugging in an actual device and confirming motion/button events arrive at a connected client (libspnav-based app, or a raw socket reader) is the first thing to check.
  • The translation/rotation report-pairing heuristic (MotionState in main.rs: caches the last-seen translation and rotation per device, pairing whichever half just arrived with the other half's cached value to build a combined MotionEvent) is unit-tested for its own internal logic but unverified against real report timing — it assumes split-device translation/rotation reports arrive close enough together that pairing "current + last cached" never visibly lags, which is only provable by feel on real hardware.
  • The button bitmask-diffing logic (MotionState::diff_buttons, emitting press/release edges between successive raw bitmasks) is unit-tested but the actual HID bit-position-to-button mapping for any given device, and whether spnav_devices::ButtonRemap (evdev BTN_*-numbered) ever needs to be bridged to the raw-HID-bit numbering DecodedReport::Buttons uses (spnav-devices' own doc comment flags this as unresolved — no known public source maps one to the other), are both unverified.
  • spnav_devices::report_layout()'s guessed "split" layout for several devices not present in PySpaceMouse's devices.toml (see that crate's doc comment for the exact list) is unverified for any of them.
  • The 1.5s hotplug poll interval is a documented judgment call, not measured against real plug/unplug responsiveness.
  • No libspnav-based client (Blender, FreeCAD, etc.) was connected to a running spacenavd-rs instance in this pass — protocol-level interop is only exercised by spnav-server's and spnav-proto's own unit/integration tests, not a real consumer.

Nothing published/pushed anywhere — local commits only.