Skip to content

RP2350 port, firmware fixes, Linux setup, and HID-based calibration - #44

Open
iplayfast wants to merge 13 commits into
sb-ocr:mainfrom
iplayfast:main
Open

iplayfast wants to merge 13 commits into
sb-ocr:mainfrom
iplayfast:main

Conversation

@iplayfast

@iplayfast iplayfast commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Comprehensive improvements since the MK2 was ported to the XIAO RP2350. All changes tested on real hardware.

RP2350 port

  • Updated controller from RP2040 to Seeed XIAO RP2350 — USB product string, NeoPixel wiring, build config.

Sensor decoupling matrix

  • 6×9 calibration matrix (`firmware/include/Calibration.h`) maps raw 9-channel sensor readings to clean 6-DoF outputs, eliminating cross-talk (e.g. a sideways slide no longer produces spurious tilt).

Firmware quality fixes

  • Removed double dead-zone application that was adding motion latency.
  • `SensorController::readRaw()` returns `bool` and checks TLx493D return values; pipeline skips the frame on sensor failure instead of emitting garbage.
  • Removed `hardZero()`, dead constants (`GAIN_T`, `GAIN_R`, `SIGN_AXIS`), and boolean sentinel patterns.

HID-based calibration — no reflashing required

  • Report ID 4 (HID feature report): firmware fills 9 baseline-subtracted sensor-delta floats each loop cycle; host reads via `HIDIOCGFEATURE` ioctl on `/dev/hidrawN`.
  • Report ID 5 (HID output report): host sends the fitted 6×9 matrix via `write()` on hidraw; firmware applies it immediately.
  • LittleFS persistence: matrix is saved to flash on receipt and loaded on every boot. Falls back to compiled-in `Calibration.h` if no saved matrix exists.
  • `monitor.py` rewritten to poll the HID feature report instead of serial.
  • `calibrate.py` sends the fitted matrix to the device after fitting — no reflash step.
  • `run_calibration.sh` simplified: no serial port, no second firmware flash, one build environment.

Linux setup

  • `linux-setup.sh`: one-script setup for spacenavd, udev rules (hidraw + evdev), X11 autostart, and the button mapper service.
  • `spacemouse-buttons.py`: reads HID input reports from `/dev/hidrawN` (not evdev, which spacenavd grabs exclusively) and maps button presses to per-app keystrokes via `xdotool`.
  • `button-map.conf`: per-app button→key mappings (FreeCAD, SOLIDWORKS, Inventor, Rhino, Fusion, Blender).
  • Systemd user service with `Restart=always` for automatic recovery on device replug.

Documentation

  • README covers Linux setup, button mapping, and the updated calibration workflow.

Test plan

  • Flash firmware, verify LED turns green after startup calibration, verify 6DoF motion via spacenavd
  • Run `./run_calibration.sh` end-to-end, verify matrix is sent to device and persists across power cycle
  • Press buttons, verify keystrokes reach the active CAD window
  • Verify cross-talk reduction in calibrated vs uncalibrated motion

🤖 Generated with Claude Code

iplayfast and others added 7 commits June 25, 2026 19:59
The hand-coded translation/rotation formulas caused large spurious outputs
on Z, Rx, Ry during pure XY translation (confirmed via log analysis).

Replaces the formulas with a 6x9 calibration matrix (Config::DECOUPLING_M)
fitted from per-DOF calibration movements using least squares, so each axis
responds only to its intended physical input.

Changes:
- MotionController: matrix multiply replaces hand-coded formulas
- Calibration.h: generated file holding the matrix (included by Config.h)
- TelemetryController: now emits 9 baseline-subtracted sensor deltas
  (s0x..s2z) alongside motion values, for use by calibration tooling
- monitor.py: logs sensor deltas + motion in tab-separated format with header
- calibrate.py: fits the 6x9 matrix from 6 labelled log files, writes Calibration.h
- run_calibration.sh: walks builder through each DOF movement end-to-end

Other builders run ./run_calibration.sh then reflash to generate their own
Calibration.h matched to their specific hardware assembly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the purpose of the sensor decoupling matrix, step-by-step
calibration instructions, and how the least-squares fit works in README.md.
Includes the updated run_calibration.sh script and regenerated Calibration.h.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…el bugs

- Remove double dead-zone: hardZero() on output was zeroing motion while
  the lowpass filter ramped up from 0, causing a sticky delay at the start
  of every movement. Output is now just clampf(filt_[i], ±AXIS_LIMIT).
- Delete unused GAIN_T, GAIN_R, SIGN_AXIS from Config.h (pre-decoupling-matrix
  leftovers with no callers).
- Default ENABLE_TELEMETRY to false so production builds don't ship with
  serial output and a 200ms startup delay.
- Make readRaw() return bool and check TLx493D read success; IdleState and
  updateCalibration now skip the frame/sample on I2C failure.
- Replace millis()==0 sentinels with explicit bool flags (calibrationSampleStarted_,
  spinnerStarted_, bothHeldActive_) in SensorController, LEDController,
  and InputController.
- Move baseline subtraction for telemetry into TelemetryController::publish()
  to eliminate the redundant delta loop in IdleState.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds spacemouse-buttons.py, a hidraw-based listener that fires xdotool
key sequences when a button is pressed, matched against the active window
title. Reads from /dev/hidrawN directly (non-exclusive) so it coexists with
spacenavd which holds an exclusive grab on the evdev node.

Key design decisions:
- buffering=0 on hidraw open — preserves one-report-per-read kernel guarantee
- SIGHUP reloads config without restarting the service
- Restart=always in the service so it recovers automatically after device replug
- button-map.conf ships with common CAD app mappings (FreeCAD, SolidWorks,
  Inventor, Rhino, Fusion, Blender) with a * fallback; user config at
  ~/.config/spacemouse/button-map.conf is never overwritten on reinstall

linux-setup.sh updated to install xdotool, write udev rules for both
input and hidraw subsystems, and deploy the button mapper as a systemd
user service alongside the existing spacenavd setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… needed

- HID descriptor gains report ID 4 (feature: 9 sensor-delta floats) and
  report ID 5 (output: 6×9 calibration matrix). TinyUSB callbacks route
  GET/SET report to HIDController.
- MotionController.setDecouplingMatrix() lets the matrix be updated at
  runtime without reflashing. Falls back to compiled-in Calibration.h.
- CalibrationStore reads/writes the matrix to LittleFS flash so it
  survives power cycles. Loaded on boot in main.cpp setup().
- IdleState exposes baseline-subtracted sensor deltas each loop cycle
  (for monitor.py to poll) and applies any incoming matrix from the host.
- monitor.py rewritten: polls HID feature report via HIDIOCGFEATURE ioctl
  instead of reading serial. Reads motion axes from HID input report ID 1.
  Same log format, so calibrate.py is unchanged.
- calibrate.py sends the fitted matrix to the device via output report ID 5
  (write on hidraw) after fitting, giving immediate effect without reflash.
- run_calibration.sh no longer mentions serial ports or firmware builds.
- platformio.ini collapsed back to one build environment.
- mouse_monitor.log untracked (was accidentally committed earlier).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…kflow

- Add Linux Setup section covering linux-setup.sh, spacenavd, and the
  button mapper (config, reload, debug commands).
- Update Calibration section: remove pyserial requirement and reflash
  step; explain that the matrix is sent to the device over USB and
  persisted to LittleFS flash.
- Expand "How it works" to describe HID feature report polling, matrix
  fitting, and the output report + LittleFS persistence path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@iplayfast iplayfast changed the title Add RP2350 controller support, sensor decoupling matrix, and calibration docs RP2350 port, firmware fixes, Linux setup, and HID-based calibration Jun 26, 2026
When hardZero() was removed in bb5b9c8 to eliminate the start-of-motion
lag, it inadvertently broke standby. The old code only set motionActive_
when filt_[i] had ramped above the dead zone; sensor noise spikes could
briefly push y[i] above dead, but filt_[i] would snap back to 0 on the
next frame and never accumulate, so motionActive_ stayed false at rest.

With out[i] != 0 as the check, any noise spike that briefly moved filt_[i]
off zero would set motionActive_ = true, keeping lastActivityMs_ fresh and
preventing the 2-minute sleep timeout from ever firing.

Fix: check fabs(filt_[i]) >= dead instead. Since filt_[i] snaps to 0
whenever y[i] drops below dead, noise cannot accumulate in the filter —
only sustained motion builds filt_[i] above the threshold. The HID output
(out[i] = clampf(filt_[i], ...)) is unchanged, so there is no regression
to the start-of-motion responsiveness fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@iplayfast iplayfast mentioned this pull request Jun 27, 2026
@maximweb

maximweb commented Jul 22, 2026

Copy link
Copy Markdown

Hey,

I also opted for an RP2350 and made some significant progress with regards to secondary core filtering: #19 (EDIT: new repo)

Yet, I am struggling with the HID controller configuration and it seems you know what you are doing.

Would you be willing to briefly explain your approach?

@iplayfast

Copy link
Copy Markdown
Author

My approach was to use vibe coding with it, :) It had a lot of suggestions and it seemed to help, but still not good enough to be usable. Going to get back into it soon.

@maximweb

Copy link
Copy Markdown

I made some progress.

Still not sure what the purpose of your additional HID reports is.

But without it - just sticking to the 0x1 and 0x3 report as already done by the original implementation - seems to work just fine for me.

I refrained from adding the magic four lines to platform.ini to my repo, though. (#51 )

iplayfast and others added 5 commits July 26, 2026 00:56
Every caller (real-time motion, baseline calibration, and the HID
feature report calibrate.py reads) was seeing a single unsmoothed I2C
sample, which fed noise straight into the dead-zone comparison and
into calibration data collection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…itives

Hold both buttons 10s to reboot into the UF2 bootloader for drag-and-drop
reflashing without opening the case. Hold the right button alone for 3s
to cycle the idle LED color, persisted in flash across power cycles.

Also adds setPixel/setPixelGroup/setTwoGroups/setPixelGroupOnBackground
to LEDController, used by the live direction indicator and diagnostic
HID LED commands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MotionController now computes a 3D point and orientation from the 3
sensors' real PCB mounting positions instead of mapping raw sensor
deltas straight through a single fitted linear matrix. A second,
ridge-regularized 6x6 matrix (GEO_M, GeoCalibration.h) then decouples
the crosstalk this introduces (the short sensor baseline amplifies any
Z-asymmetry into a large apparent tilt angle), and a per-axis gain lets
translation axes compete fairly against the rotation crosstalk they
induce. Ported from interactive_calibrate.py, where this was developed
and verified live.

Also:
- Adds diagnostic HID output reports (6/7) for host-driven LED color/
  pixel control, and wakes SleepState to apply them or a pushed
  calibration matrix instead of silently dropping them.
- Rewrites the idle-state LED direction indicator to match
  lightpattern.md exactly: idle cyan, push/pull full-ring red/green,
  tilt shows 2 green LEDs on a cyan ring, slide shows a green/red
  half-split, twist shows a blue dot circling a cyan ring. All active
  indicators blink/animate at a rate proportional to magnitude instead
  of a fixed interval.
- Fixes board_build.filesystem_size (was unset, silently giving
  LittleFS a 0-byte partition so calibration/LED-color persistence
  never actually worked).
- Swaps which physical gesture drives HID Y vs Z (slide fwd/back vs
  push/pull) — spacenavd/FreeCAD treats HID Y as dolly/zoom, which
  made slide fwd/back zoom instead of pan.
- GEO_AXIS_GAIN[TX] and GEO_AXIS_GAIN... TZ (existing AXIS_GAIN) are
  negative to fix inverted CAD view motion found in live testing; the
  LED direction logic compensates so indicators still match the
  physical motion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Self-paced host tool (run directly in a terminal, not through an AI
assistant) for tuning and verifying the geometric motion model live:
menu 1-6 captures 5s of motion per axis, 'g' fits the GEO_M decoupling
matrix from those captures, 'c' runs the existing linear-matrix
calibrate.py, +/- tune the 3D-point scale factor live. Shows raw
per-sensor deltas, the fitted 3D point/orientation, and a plain-English
"THE MOUSE THINKS: ..." line reflecting the same logic as the firmware's
LED indicator.

geo_matrix.json is fit from one physical unit but included as a
starting point for similarly-built units, since the sensor mounting
geometry is fixed by the PCB design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Geometric motion model, LED direction indicator, and misc firmware fixes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants