Phase 1 (3-DoF) implementation of DEVELOPMENT_SPEC.md:
reads the Nreal Light IMU (accelerometer + gyroscope) over USB, fuses it with a
Mahony complementary filter, and streams yaw/pitch/roll to Opentrack over
UDP, with a 2 Hz on-screen HUD for visual verification, plus a GUI
(Linux + Windows) with a settings file and a 3D cube visualization.
Nreal Light ──USB──▶ [ImuTracker] ──▶ [Mahony AHRS] ──▶ [UDP → Opentrack]
│ └─▶ [HUD 2 Hz]
└─▶ [neuromancer-tracker-gui: HUD + 3D cube + settings]
| Crate | Role |
|---|---|
crates/neuromancer-ahrs |
Dependency-free AHRS: Mahony filter, quaternion math, YXZ Tait-Bryan YPR (RUB frame) — reusable standalone |
crates/neuromancer-tracker |
Main tracker: minimal IMU-only core (lib + CLI) — no visual/VO code linked |
crates/neuromancer-tracker-gui |
GUI (Linux + Windows, egui/eframe): embeds the main tracker, TOML settings menu ↔ CLI switches, HUD, 3D cube of glasses rotation |
crates/neuromancer-tracker-full |
Full Phase-2 tracker (stereo VO + ESKF fusion) — research crate, kept for the visual/fusion work |
cargo build --release # Linux dev build
cargo test --workspace # ahrs + fusion + vo + tracker unitsWindows (mini @ 192.168.0.231, P4): native build preferred — cargo build --release --target x86_64-pc-windows-msvc (Rust 1.97.1 + VS Build Tools already verified there). The GUI uses egui/eframe which builds on both Linux and Windows.
- Plug the Nreal Light into USB.
- Start Opentrack with the UDP over network tracker listening on
127.0.0.1:4242. - Run the CLI:
neuromancer-tracker --hud # UDP out (default) + 2 Hz HUDor the GUI:
neuromancer-tracker-gui # settings menu + HUD + 3D cubeRotate the headset — HUD shows live degrees, Opentrack gets the pose.
Both the GUI and the CLI read/write the same TOML settings file
(~/.config/neuromancer-tracker/settings.toml on Linux, %APPDATA%\neuromancer-tracker\settings.toml
on Windows). Every settings field maps 1:1 to a CLI switch:
| Settings field | CLI switch |
|---|---|
no_udp |
--no-udp |
host, port, udp_rate |
--host, --port, --udp-rate |
hud_rate |
--hud-rate |
gyro_calib |
--gyro-calib |
kp, ki |
--kp, --ki |
rad |
--units deg|rad |
show_cube |
(GUI only — 3D cube toggle) |
CLI ↔ file interop: neuromancer-tracker --config PATH loads a file,
--save-config writes the current settings out.
These replay/log flags belong to the full tracker (
neuromancer-tracker-full, the Phase-2 research crate). The mainneuromancer-trackeris USB-exclusive by design (no replay, no IMU log — spec §2.3).
The full tracker's production input is USB-exclusive, but for
development/verification you can feed a JSONL IMU log through the same
ImuSource abstraction:
# record once (needs glasses): append raw IMU to a file
neuromancer-tracker-full --log-imu /tmp/imu.jsonl --no-udp
# replay anywhere (dev box, CI): paced like a live stream
neuromancer-tracker-full --replay /tmp/imu.jsonl --hudReplay files use the spec §4.4 IMU log format: {"t": <monotonic s>, "ax": .., "ay": .., "az": .., "gx": .., "gy": .., "gz": ..}.
neuromancer-tracker [OPTIONS]
--no-udp Disable UDP output (HUD-only mode)
--host <IP> Opentrack host (default 127.0.0.1)
--port <PORT> Opentrack UDP port (default 4242)
--udp-rate <HZ> UDP output rate (default 60)
--units <U> deg|rad for UDP + HUD (default deg)
--hud-rate <HZ> HUD update rate (default 2; 0 = off)
--kp <FLOAT> Mahony proportional gain (default 1.0)
--ki <FLOAT> Mahony integral gain (default 0.005)
--gyro-calib <SEC> Startup gyro-bias calibration window (default 2.0, 0=off)
--config <PATH> Settings file to load (default: platform config dir)
--save-config Write the current settings to the config file and exit
--help Print help and exit
The GUI's Settings menu edits exactly these fields and saves them to the
same TOML file (--save-config from the CLI does the same).
- Classic (default): 48 bytes, native-endian 6×f64
[TX, TY, TZ, Yaw, Pitch, Roll], translation0(3-DoF). - Extended: 80 bytes, 10×f64 — the same 6 +
[1.0, 0, 0, 0](pose-valid default + 3 reserved doubles). - Stateless datagrams, no handshake; sent at
--udp-rateHz (rate-gated, max 60 by default). With a 200 Hz IMU stream the decimator yields 50 Hz (every 4th sample — "60 Hz max, no minimum" per spec §4.2; set--udp-ratehigher to emit every 3rd sample if you need ≥ 60 Hz). - Units: degrees by default (
--units deg|rad).
⚠ Deliberate deviation from the spec text: DEVELOPMENT_SPEC.md says "radians on the wire" (§3.3, §4.2, §4.4), but the stock Opentrack UDP tracker copies the received doubles raw and adds ±90/±180° offset options (
tracker-udp/ftnoir_tracker_udp.cpp) — it interprets rotation as degrees. Sending radians would overshoot ~57× in game. Resolved by user decision: degrees default,--units radkeeps the spec's variant reachable. The pose log follows the wire units.
| Code | Meaning |
|---|---|
| 0 | Clean shutdown (Ctrl-C / replay EOF) |
| 1 | No Nreal Light on USB / replay file error / IMU start failure |
| 2 | CLI usage error |
| 3 | IMU read failure mid-run (USB unplug — restart to reconnect) |
- First Ctrl-C → flush logs, clean exit 0. Second Ctrl-C → immediate exit 1.
- Startup prints a confirmation line:
device=... kp=... ki=... out=... protocol=... units=... udp_rate=...Hz hud=....
This is the full tracker's feature (
neuromancer-tracker-full). The mainneuromancer-trackeris IMU-only by design.
Stereo visual odometry from the Nreal Light SLAM cameras (spec Appendix D): 640×480 grayscale stereo at ~30 fps → FAST corners → KLT tracking → epipolar stereo depth → RANSAC motion → incremental 6-DoF pose. The IMU is never opened in this mode.
./target/release/neuromancer-tracker-full --input visual --hud # hardware cameras
./target/release/neuromancer-tracker-full --input visual --replay-visual <dir> --hud--replay-visual <DIR>reads raw frames (left_XXXX.raw/right_XXXX.raw);--record-visual <DIR>writes them (record a hardware session, replay it offline — mirrors--log-imu/--replay).- Outputs are 6-DoF: UDP
TX/TY/TZ(in cm, Opentrack's translation convention), HUD gainsX/Y/Z(meters), pose log gainsx/y/z. - M5 uses a canonical rectified rig; hardware intrinsics + fisheye rectification + head-frame alignment are the M7 hardware spike.
Messages go to stderr, gated by verbosity. Default is error — the run is
quiet: only errors print (plus the startup line and HUD on stdout).
--log-level warning— also shows throttled warnings (e.g. "UDP send failed … is Opentrack listening?" at most once per 5 s).--log-level info— also shows the gyro-bias calibration result and the measuredimu_rate.--log-level debug— shows every UDP send failure (unthrottled).
The Nreal Light has no magnetometer, so yaw has no absolute reference and any residual gyro bias integrates into linear yaw drift. Measured on real glasses (2026-08-04): ~15°/60 s ≈ 0.25°/s constant — classic turn-on bias left after the driver's static device calibration.
By default the tracker runs a 2 s stationary gyro-bias calibration at
startup: keep the glasses still (table is ideal), it measures the mean gyro
while still and subtracts it from every sample. Drift after calibration is
well under the T3 budget. Adjust with --gyro-calib <SECONDS> (0 disables).
Calibration waits for stillness (max 5× the window) and logs a warning if the
device was moving.
Because the bias also drifts with temperature during a session (thermal
warm-up), the tracker refreshes it in-run: whenever the glasses rest still
for the same window mid-session, the bias is re-measured and swapped in
silently (visible as "in-run gyro bias refreshed" at --log-level info). No
persistence across boots — the turn-on bias is random per power cycle, so a
fresh 2 s measurement is always more accurate than a saved value.
start-tracker.sh— launcher: HUD + UDP output with the protocol switch (env-overridable:PROTOCOL,HOST,PORT,UDP_RATE,UNITS,LOG_LEVEL,GYRO_CALIB). Builds the release binary on first use.tools/udp_listen.py— UDP diagnostic that parses packets exactly like Opentrack's "UDP over network" tracker (48 B / 80 B, degrees, rejects NaN/Inf). Use it to isolate the tracker's UDP output from Opentrack's config:python3 tools/udp_listen.py 10in one terminal, run the tracker in another.CHANGELOG.md— per-release notes; P1 is pinned as tagv0.1.0.
- Frames: RUB body frame (+X right, +Y up, +Z back — matches
ar-driversand the Android sensor frame). Quaternion rotates world → body. Euler angles are YXZ Tait-Bryan (yaw → pitch → roll), extracted inneuromancer-ahrs::quat_to_ypr. - Yaw has no absolute reference (no magnetometer in the Nreal Light): yaw is gyro integration, corrected only indirectly; slow drift is expected (spec §2.4, T3). Pitch/roll are gravity-stabilized.
dtfrom sample timestamps (monotonic), clamped to 100 ms — robust to USB jitter (spec §3.3). Every IMU sample is consumed; outputs are rate-gated decimators inside each sink, single-threaded loop (spec §2.7).- f64 math in
neuromancer-ahrs(spec §2.4 mentioned f32): f64 chosen for deterministic drift behavior over long runs; the crate stays dependency-free. - Dependency tree is minimal:
neuromancer-ahrshas zero deps; the binary adds onlyar-drivers(required USB input, spec §2.6),ctrlc(cross-platform Ctrl-C, needed for P4), and the path dep on the AHRS crate. No async runtime, no GUI. ar-drivers0.4.3 quirk: itsnrealfeature map omitsrusb, but the Nreal Light backend uses rusb/libusb unconditionally — the tracker enablesfeatures = ["nreal", "rusb"]explicitly (spec §5.4: libusb/WinUSB on Windows).
- Development spec:
DEVELOPMENT_SPEC.md(approved; open questions OQ1/OQ3 are being resolved in implementation — see this README). cargo doc --openfor theneuromancer-ahrsAPI documentation.