Skip to content

mgth/nx-tracker-osc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nx-tracker-osc (nxosc)

Read the Waves Nx Head Tracker (NXIMU010) over Bluetooth LE on Linux (BlueZ), decode head orientation, and re-emit it as OSC compatible with the IEM SceneRotator (IEM Plugin Suite). Built as a standalone crate, kept modular (ble / decode / osc) for later integration into the Omniphony spatial renderer. Licensed GPLv3 (GPL-3.0-only), matching Omniphony.

Status: Phase 3 done — run streams decoded orientation over OSC (IEM SceneRotator or a Sensors2OSC-style feed for Omniphony), with software recenter, smoothing, rate-limit and auto-reconnect.

Roadmap

Phase Command State
0 skeleton + CLI (scan/raw/run) done
1 raw — connect, start, dump frames + live candidate interpretations + CSV done
2 decodedecode(&[u8]) -> Orientation (quaternion + yaw/pitch/roll), unit-tested on captured frames done
3 run — stream orientation as OSC (SceneRotator + Sensors2OSC/Omniphony), recenter/smoothing/rate-limit done

Payload layout (a015), determined empirically

10-byte notification:

bytes meaning
0..2 xint16 LE, Q1.14 (value / 16384)
2..4 yint16 LE, Q1.14
4..6 zint16 LE, Q1.14
6..8 wint16 LE, Q1.14
8..10 constant 00 03 (footer/flags) — ignored

(w, x, y, z) is a unit quaternion (measured norm = 1.0000, sd 2e-5 across all captures). Device body axes match the ADM convention used by Omniphony: X = pitch, Y = roll, Z = yaw. yaw/pitch/roll are extracted for the intrinsic Z-X-Y sequence (inverse of HeadPose::from_euler_deg). Near pitch = ±90° the Euler triple hits gimbal lock; the quaternion stays valid.

Bluetooth prerequisites (BlueZ)

nxosc talks to the system BlueZ daemon over D-Bus; it does not pair the device itself. Pair and trust the tracker once with bluetoothctl:

rfkill unblock bluetooth          # if the adapter is soft-blocked
bluetoothctl
[bluetooth]# power on
[bluetooth]# scan on
# ... wait until a device named like "nx tracker" appears, note its MAC ...
[bluetooth]# pair  AA:BB:CC:DD:EE:FF
[bluetooth]# trust AA:BB:CC:DD:EE:FF
[bluetooth]# scan off
[bluetooth]# quit

Notes:

  • The tracker advertises a name containing "nx tracker" (case-insensitive).
  • trust lets it reconnect automatically after sleep — required for the auto-reconnect behaviour of raw/run.
  • Permissions: using the BlueZ D-Bus API normally needs no extra privileges or group membership. If scan/raw reports no adapter, check systemctl status bluetooth and bluetoothctl show (adapter Powered: yes).
  • If the device is busy/connected elsewhere (e.g. the macOS NXOSC app or a phone), disconnect it there first.

Build

cargo build --release    # binary at target/release/nxosc

Linux build dependency: the BlueZ backend talks D-Bus; ensure a D-Bus development setup is present if the build complains (pkg-config, dbus).

Usage

# 1. Find the tracker (Nx devices are listed first and marked).
nxosc scan
nxosc scan --secs 8

# 2. Capture (Phase 1). Move your head on ONE axis at a time and watch which
#    rows/columns develop a wide RANGE — those slots encode that axis.
nxosc raw                                  # discover by name, live range table
nxosc raw --address AA:BB:CC:DD:EE:FF       # force a MAC
nxosc raw --csv capture.csv                 # also log every raw frame for offline analysis
nxosc raw --stream                          # one block per packet instead of the live table
nxosc raw --decode                          # also show decoded quaternion + yaw/pitch/roll (Phase 2)
nxosc raw --csv yaw.csv                     # suggested: one CSV per axis (yaw/pitch/roll/still)

# verbosity
nxosc -v raw           # debug logs   (RUST_LOG overrides, e.g. RUST_LOG=trace)

What raw shows

For each notification it tracks four competing readings of the payload — i16 LE, i16 BE, f32 LE, f32 BE — and, per slot, the live value plus the min / max / range observed since start, with a bar scaled within each group. A slot whose bar grows when you yaw (and stays flat otherwise) is a yaw component; flat constant bytes are header; a slot that only ever increases is a packet counter. --csv always records every frame as ts_us,len,hex for offline analysis.

run — stream orientation as OSC (Phase 3)

# IEM SceneRotator (default profile). yaw/pitch/roll in degrees:
nxosc run --address AA:BB:CC:DD:EE:FF
nxosc run --mode quaternion                  # send /SceneRotator/quaternions w x y z instead
nxosc run --osc-target 127.0.0.1:9000

# Omniphony / Sensors2OSC emulation (test directly in Omniphony):
nxosc run --profile omniphony --osc-address /gamerotationvector --osc-target 127.0.0.1:9000

OSC wire formats (verified against IEM docs / a reference head-tracker script and Omniphony's own parser):

profile mode message
scenerotator ypr (default) /SceneRotator/ypr <yaw> <pitch> <roll> (degrees)
scenerotator quaternion /SceneRotator/quaternions <w> <x> <y> <z>
omniphony quaternion (default) <--osc-address> <x> <y> <z> <w> (Android order; Omniphony reads from_quat(w,x,y,z))
omniphony ypr <--osc-address> <yaw> <pitch> <roll> (requires Omniphony format = euler)

To test in Omniphony, set render.binaural.head_tracking.osc_address to the same value as --osc-address (and format to auto/quat for quaternion).

Extras:

  • Recenter: press Enter while run is active to set "forward = current heading" (yaw-only; pitch/roll stay gravity-referenced). --recenter-on-start zeroes at launch.
  • --smoothing <0..1>: exponential quaternion smoothing (0 = instant).
  • --max-hz <n>: cap the OSC send rate (default 60).
  • Sleep-aware reconnect: the tracker auto-sleeps when idle. run detects the no-data state (no a015 within a few seconds) and tells you to "move it or short-press its button to wake it". When the link merely goes silent but stays connected, run holds the connection and waits — moving the tracker or a short button press resumes the stream on the same link (reconnecting instead would only churn the BLE link, which is what wedges this device). It reconnects only if the link actually drops, and keeps retrying if the tracker is out of range — no need to restart nxosc.

Device exploration (gatt / probe)

# Map the device: connect WITHOUT sending start, print every service +
# characteristic and read the readable ones (battery, firmware, manufacturer…).
nxosc gatt

# Experiment on the a011 command characteristic and measure the a015 rate.
nxosc probe --rate 100      # write [100 u32 LE, 0x01] -> expect ~100 Hz
nxosc probe --stop          # write [0x32,0,0,0,0x00]  -> expect the stream to stop
nxosc probe --cmd "32 00 00 00 01"   # write arbitrary bytes to a011
nxosc probe --sweep 50:100:5         # one connection; sweep rates on one link

# Try to recover a stalled/degraded stream over GATT (re-arm notifications).
nxosc kick

probe --sweep takes a comma list ("50,60,70") or a range ("lo:hi:step") and writes each rate then measures on the same connection — avoiding the reconnect churn that can wedge the BLE link / send the tracker to sleep.

nxosc kick measures the rate, then re-arms notifications (CCCD unsubscribe→subscribe) and re-sends start, and measures again. A full stall (0 Hz) is firmware-internal: neither the re-arm, a plain reconnect, nor the vendor characteristics revive it (all tested) — only a short press of the device button does (a long press resets the tracker, with a red flash).

A degraded stream (e.g. a half-rate ~25 Hz drop) was once seen to climb back to ~50 Hz across a re-arm, but that is a single observation and not validated — the stream may simply recover on its own. Use kick --no-rearm on a degraded stream as a control: if the rate rises in the second window without any re-arm, the recovery is spontaneous and the re-arm is not the cause.

The start command is [rate (u32 LE), enable (u8)] (0x32 = 50 = the default 50 Hz). On a fresh connection the byte maps essentially 1:1 to the output rate across ~20–100 Hz:

requested measured requested measured
100 100 Hz 30 30 Hz
90 91 Hz 25 25 Hz
75 77 Hz 20 20 Hz
50 50 Hz ≤ 15 collapses (15→11, 12→6, 10→4, …)

So the useful range is 20–100 Hz (rate ≈ Hz); --rate 100 genuinely gives ~100 Hz, halving the head-tracking sampling latency vs the 50 Hz default. Below ~20 Hz the rate falls off in plateaus and is unusable.

⚠️ The rate is reliable only on the first clean connection after a device reset/power-cycle. The tracker degrades after a couple of rapid reconnects (writes still ACK but the stream stalls), which also skews any --sweep. The 5th byte is not a clean enable (writing 0 does not stop the stream — to stop, just disconnect). gatt is read-only and probe only ever writes to a011, so a DFU/firmware service (if present) is never touched.

Both raw and run accept --rate <hz> (default 50), e.g. nxosc run --rate 100 … for lower-latency head tracking.

GATT map (from nxosc gatt): Device Information reports firmware v100, hardware v4.4, software A v1.30 B v1.13, "Waves Audio"; a Battery service; and vendor characteristics including a018 (the writable device name) and two further vendor services (a030, a050) of unknown purpose — a030 looks like a DFU control point, so it is left untouched.

Capture protocol for Phase 2

To pin down the layout, capture a few short, labelled runs and send the CSVs:

  1. still.csv — tracker flat and motionless (baseline / constant bytes).
  2. yaw.csv — slow full left/right rotation only.
  3. pitch.csv — slow nod up/down only.
  4. roll.csv — slow tilt left/right only.

License

GPL-3.0-only. See LICENSE.

About

Waves Nx Head Tracker (NXIMU010) → OSC bridge over Bluetooth LE (Linux/BlueZ): decode head orientation and stream it to IEM SceneRotator or a Sensors2OSC-style feed. Rust.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages