Files
prosolis 202adacf7a 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
2026-08-27 23:06:11 -07:00

128 lines
3.8 KiB
Bash
Executable File

#!/bin/bash
#
# Drives the firmware's calibration patterns from the PC over ALSA MIDI.
# See "Calibration" in README.md for the procedure.
#
# Usage:
# tools/calibrate.sh list list MIDI ports
# tools/calibrate.sh ends pixel 0 (red) and 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 <n> light pixel n only
# tools/calibrate.sh sweep [ms] walk every pixel in turn
# tools/calibrate.sh all every pixel, for voltage droop
# tools/calibrate.sh off back to normal operation
# tools/calibrate.sh note <n> [vel] play one MIDI note
# tools/calibrate.sh chromatic [ms] play every key, low to high
#
# Set PORT to the device's ALSA port (e.g. PORT=24:0). If unset, the script
# picks the first port whose name matches PORT_MATCH.
#
set -e
PORT_MATCH=${PORT_MATCH:-Piano LED}
CC_PATTERN=20
CC_INDEX_HI=21
CC_INDEX_LO=22
PAT_OFF=0; PAT_ENDS=1; PAT_OCTAVES=2; PAT_KEYS=3; PAT_WALK=4; PAT_ALL=5
need() {
command -v "$1" >/dev/null 2>&1 || {
echo "error: $1 not found (install alsa-utils)" >&2; exit 1; }
}
find_port() {
if [ -n "$PORT" ]; then echo "$PORT"; return; fi
local p
p=$(aconnect -l | awk -v m="$PORT_MATCH" '
/^client /{ cl=$2; sub(":","",cl); name=$0 }
/^ +[0-9]+ / { if (name ~ m) { port=$1; print cl ":" port; exit } }')
if [ -z "$p" ]; then
echo "error: no MIDI port matching \"$PORT_MATCH\"." >&2
echo " Run '$0 list', then set PORT=client:port" >&2
exit 1
fi
echo "$p"
}
# Send raw MIDI bytes to the port.
send() {
need amidi
local hex="$*"
amidi -p "$(alsa_rawmidi_port)" -S "$hex" 2>/dev/null && return 0
# amidi needs a rawmidi device; fall back to the sequencer via aplaymidi
echo "error: could not send. Set PORT and ensure the device is connected." >&2
exit 1
}
# amidi addresses rawmidi (hw:X,Y), not sequencer ports.
alsa_rawmidi_port() {
if [ -n "$RAWMIDI" ]; then echo "$RAWMIDI"; return; fi
need amidi
local p
p=$(amidi -l | awk -v m="$PORT_MATCH" '$0 ~ m { print $2; exit }')
if [ -z "$p" ]; then
echo "error: no rawmidi device matching \"$PORT_MATCH\"." >&2
echo " Run 'amidi -l', then set RAWMIDI=hw:X,Y" >&2
exit 1
fi
echo "$p"
}
cc() { printf 'B0 %02X %02X' "$1" "$2"; }
pattern() { send "$(cc $CC_PATTERN $1)"; }
walk() {
local n=$1
send "$(cc $CC_PATTERN $PAT_WALK) $(cc $CC_INDEX_HI $((n >> 7))) $(cc $CC_INDEX_LO $((n & 127)))"
}
case "${1:-}" in
list)
echo "--- sequencer ports (aconnect -l) ---"; aconnect -l || true
echo; echo "--- rawmidi devices (amidi -l) ---"; amidi -l || true
;;
ends) pattern $PAT_ENDS; echo "pattern: ends" ;;
octaves) pattern $PAT_OCTAVES; echo "pattern: octaves" ;;
keys) pattern $PAT_KEYS; echo "pattern: keys" ;;
all) pattern $PAT_ALL; echo "pattern: all (watch the far end for warm colour)" ;;
off) pattern $PAT_OFF; echo "pattern: off" ;;
walk)
[ -n "${2:-}" ] || { echo "usage: $0 walk <pixel>" >&2; exit 1; }
walk "$2"; echo "pixel $2"
;;
sweep)
MS=${2:-120}
COUNT=${LED_COUNT:-176}
echo "sweeping 0..$((COUNT-1)); Ctrl-C to stop"
for ((i=0; i<COUNT; i++)); do
walk "$i"; printf '\rpixel %3d' "$i"; sleep "$(echo "$MS/1000" | bc -l)"
done
echo
;;
note)
[ -n "${2:-}" ] || { echo "usage: $0 note <midi-note> [velocity]" >&2; exit 1; }
V=${3:-100}
send "$(printf '90 %02X %02X' "$2" "$V")"
echo "note $2 on (velocity $V); '$0 off' or send note-off to clear"
;;
chromatic)
MS=${2:-150}
echo "playing notes 21..108; Ctrl-C to stop"
for ((n=21; n<=108; n++)); do
send "$(printf '90 %02X 64' "$n")"
printf '\rnote %3d' "$n"
sleep "$(echo "$MS/1000" | bc -l)"
send "$(printf '80 %02X 00' "$n")"
done
echo
;;
*)
sed -n "2,/^# Set PORT/p" "$0" | sed 's/^# \{0,1\}//'
exit 1
;;
esac