Add calibration patterns and a geometric note-to-LED map

Hardware arrives tomorrow, which makes this the blocking work: the appliance
has no console, so without it there is no way to answer "is pixel 0 at the end
I think it is" except by guessing.

Geometric mapping. NOTE_MAP_GEOMETRIC (default on) derives each key from
white-key geometry rather than semitone index: 52 white keys span the strip,
so a white key is LED_COUNT/52 pixels - about 3.38 at 176 LEDs, not 2 - with
black keys on the boundaries. The old linear map drifts within each octave,
worst at F, by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale.
Set NOTE_MAP_GEOMETRIC=0 to restore it.

This exposed a real bug. Under the geometric map adjacent key spans overlap,
because the semitone pitch (~1.7 LEDs) is narrower than LEDS_PER_KEY. The
renderer painted unlit keys black, so a key erased its lit neighbour's pixels.
It now clears once and paints only lit keys. The linear map never overlapped,
so this could not have been found without the geometry change.

LED_OFFSET shifts every key, absorbing where the strip was actually cut and
where the profile ended up. Off-strip pixels are clipped, never wrapped.

Calibration patterns, selected by CC 20, with CC 21/22 setting the pixel for
the walk: ends (orientation and length), octaves (mapping drift), keys (whole
mapping at once), walk (finding LED_OFFSET), all (voltage droop at the far
end). Patterns run at the same brightness ceiling as normal operation, so none
can exceed the current budget the design already allows.

tools/calibrate.sh drives all of it from the PC over ALSA MIDI, and README
carries the six-step procedure in dependency order.

Verified: tests pass across fourteen configurations, now including both
mapping modes and positive, negative and reversed offsets. Both platforms
build clean - pianoled.uf2 for RP2350 and both Circle kernel images - with no
warnings from project sources.

Claude-Session: https://claude.ai/code/session_01TVCB25LBsmeteWvaSMz4Ne
This commit is contained in:
prosolis
2026-08-27 23:06:11 -07:00
parent 469b321a40
commit 202adacf7a
7 changed files with 676 additions and 70 deletions
+64 -6
View File
@@ -149,12 +149,68 @@ make -C firmware EXTRADEFINE=-DSTRIP_REVERSED=1 # Circle
cmake -B pico/build -S pico -DCMAKE_CXX_FLAGS=-DSTRIP_REVERSED=1 # Pico
```
> **Known limitation:** the note-to-LED mapping is linear in semitone index,
> but a real keybed is not — 52 white keys span the same 1222mm, so one white
> key is ~3.38 LEDs rather than 2. This drifts within each octave, worst at F,
> by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale. A
> geometric map derived from white-key positions would remove it. Not yet
> implemented.
### Note-to-LED mapping
`NOTE_MAP_GEOMETRIC` (default 1) derives each key's position from white-key
geometry: 52 white keys span the strip, so a white key is `LED_COUNT / 52`
pixels — **~3.38 at 176 LEDs, not 2** — with black keys on the boundaries.
Setting it to 0 restores the plan's original `(note - 21) * 2`. That map is
linear in semitone index, but a keybed is not: it drifts within each octave,
worst at F, by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale.
Keep it only to reproduce the original behaviour.
One consequence of the geometric map: adjacent key spans **overlap**, because
the semitone pitch (~1.7 LEDs) is narrower than `LEDS_PER_KEY`. That is
expected, and the renderer paints only lit keys so a neighbour cannot erase
them.
## Calibration
This is a headless appliance, so calibration runs over MIDI — the one channel
that already exists. `tools/calibrate.sh` drives it from the PC:
```sh
tools/calibrate.sh list # find the port
tools/calibrate.sh ends # pixel 0 (red), last pixel (green)
tools/calibrate.sh octaves # every C, middle C in red
tools/calibrate.sh keys # every key: white green, black blue
tools/calibrate.sh walk 37 # one pixel only
tools/calibrate.sh sweep # walk every pixel in turn
tools/calibrate.sh all # every pixel — voltage droop test
tools/calibrate.sh off # back to normal
```
A pattern replaces the note display entirely while it is active; `off`
restores it.
### Procedure
Work in this order — each step depends on the one before.
1. **`ends`** — one pixel lights at each end of the strip. If red is at the
treble end, set `STRIP_REVERSED 1` and rebuild. If either end is dark, the
strip is not the length `LED_COUNT` assumes.
2. **`all`** — every pixel white. Watch the far end: if it drifts dim or warm,
the strip needs 5V injected at that end too. This draws roughly
`LED_COUNT × 3 × GLOBAL_BRIGHTNESS/255 × 20mA` — about 4A at the defaults,
inside a 6A supply but well beyond normal play, which caps at
`MAX_LIT_KEYS`.
3. **`keys`** — every key lit, whites and blacks in different colours. Check
the colours line up with the actual keys across the whole span. This is the
fastest way to see a mapping or length error.
4. **`octaves`** — every C, middle C in red. Drift shows up as the marks
walking off the keys as you move up the keyboard. With the geometric map
they should stay put.
5. **`sweep`** or **`walk <n>`** — step one pixel at a time until you find the
pixel sitting over A0. If that is not the pixel the firmware expects, the
difference is your `LED_OFFSET`. Set it and rebuild.
6. **`chromatic`** — plays every key in turn. Watch for the lit span leading
or lagging the key as it climbs.
Then decide the product questions the firmware cannot: colours *through the
diffuser* (not bare), brightness, and whether velocity should modulate
anything. All of them live in `src/config.h`.
## MIDI behaviour
@@ -165,6 +221,8 @@ cmake -B pico/build -S pico -DCMAKE_CXX_FLAGS=-DSTRIP_REVERSED=1 # Pico
for the plan's Phase 3 "light the next key to play". A key actually being
played takes precedence over a hint on the same key.
- Notes held when the USB host suspends are cleared, so nothing stays lit.
- CC 20 selects a calibration pattern; CC 21/22 set the pixel for the walk
pattern. See **Calibration** above.
## Tests