Files
PianoLED-Circle-Edition/tests/test_pianoleds.cpp
T
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

330 lines
9.9 KiB
C++

//
// Host-side tests for the note-to-LED mapping and the power clamps.
// Compiles the real firmware/pianoleds.cpp against stubbed Circle headers.
//
#include "pianoleds.h"
#include "capturestrip.h"
#include <cstdio>
#include <cstring>
static int g_nFail = 0;
static void Check (const char *pName, bool bCond)
{
printf ("%-58s %s\n", pName, bCond ? "ok" : "FAIL");
if (!bCond) g_nFail++;
}
static CCaptureLEDStrip Strip (LED_COUNT);
static CPianoLEDs LEDs (Strip);
static unsigned CountLit (void)
{
unsigned n = 0;
for (auto &p : Strip.m_Pixels)
if (p[0] || p[1] || p[2]) n++;
return n;
}
static bool Dark (int i)
{
// Off-strip is not lit. Signed, because a negative LED_OFFSET can push
// a key's start below zero.
if (i < 0 || i >= (int) Strip.m_Pixels.size ()) return true;
auto &p = Strip.m_Pixels[i];
return !p[0] && !p[1] && !p[2];
}
static bool OnStrip (int nBase)
{
return nBase >= 0 && nBase + (int) LEDS_PER_KEY <= (int) LED_COUNT;
}
// Deliver a plain MIDI message the way a platform backend would.
static void Inject (uint8_t a, uint8_t b, uint8_t c)
{
uint8_t packet[3] = {a, b, c};
LEDs.OnMIDIPacket (packet, 3);
}
// every pixel of one key's span is lit
static bool Span (int nBase)
{
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
if (Dark (nBase + (int) i)) return false;
return true;
}
static int LedFor (uint8_t ucNote)
{
return LEDs.GetKeyLED (ucNote - MIDI_NOTE_MIN);
}
#if LED_OFFSET != 0
static void AllOffAndClear (void)
{
LEDs.AllOff ();
LEDs.Update ();
}
#endif
int main (void)
{
printf ("STRIP_REVERSED=%d LED_COUNT=%d MAX_LIT_KEYS=%d GLOBAL_BRIGHTNESS=%d\n\n",
STRIP_REVERSED, LED_COUNT, MAX_LIT_KEYS, GLOBAL_BRIGHTNESS);
LEDs.Initialize ();
// --- lowest key, A0 = note 21 -------------------------------------
Inject (0x90, 21, 127);
LEDs.Update ();
int nLow = LedFor (21);
if (OnStrip (nLow))
{
Check ("note 21 lights its whole key span", Span (nLow));
Check ("note 21 lights exactly LEDS_PER_KEY LEDs",
CountLit () == LEDS_PER_KEY);
}
// --- highest key, C8 = note 108 -----------------------------------
Inject (0x80, 21, 0);
Inject (0x90, 108, 127);
LEDs.Update ();
int nHigh = LedFor (108);
if (OnStrip (nHigh))
{
Check ("note 108 lights its whole key span", Span (nHigh));
Check ("note 108 lights exactly LEDS_PER_KEY LEDs",
CountLit () == LEDS_PER_KEY);
}
Check ("the two extremes are at opposite ends", nLow != nHigh);
// --- note off ------------------------------------------------------
Inject (0x80, 108, 0);
LEDs.Update ();
Check ("note off extinguishes the key", CountLit () == 0);
// --- note on with velocity 0 is a note off -------------------------
Inject (0x90, 60, 100);
LEDs.Update ();
Check ("note on lights middle C", CountLit () == LEDS_PER_KEY);
Inject (0x90, 60, 0);
LEDs.Update ();
Check ("note on velocity 0 acts as note off", CountLit () == 0);
// --- out-of-range notes are dropped, not clamped into the strip ----
Inject (0x90, 20, 127);
Inject (0x90, 109, 127);
Inject (0x90, 0, 127);
Inject (0x90, 127, 127);
LEDs.Update ();
Check ("notes outside 21-108 are ignored", CountLit () == 0);
// --- brightness ceiling --------------------------------------------
for (uint8_t n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++)
Inject (0x90, n, 127);
LEDs.Update ();
bool bWithinCeiling = true;
#if GLOBAL_BRIGHTNESS < 255 // at 255 a u8 channel cannot exceed the ceiling by construction
for (auto &p : Strip.m_Pixels)
for (int c = 0; c < 3; c++)
if (p[c] > GLOBAL_BRIGHTNESS) bWithinCeiling = false;
#endif
Check ("no channel ever exceeds GLOBAL_BRIGHTNESS", bWithinCeiling);
// --- simultaneous-key cap ------------------------------------------
Check ("all 88 keys held stays within MAX_LIT_KEYS",
CountLit () <= MAX_LIT_KEYS * LEDS_PER_KEY);
// --- all notes off --------------------------------------------------
Inject (0xB0, 123, 0);
LEDs.Update ();
Check ("CC 123 (all notes off) clears the strip", CountLit () == 0);
for (uint8_t n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++)
Inject (0x90, n, 127);
Inject (0xB0, 120, 0);
LEDs.Update ();
Check ("CC 120 (all sound off) clears the strip", CountLit () == 0);
// --- velocity sensitivity -------------------------------------------
Inject (0x90, 60, 127);
LEDs.Update ();
auto Loud = Strip.m_Pixels.at (((LedFor (60)) + LED_COUNT) % LED_COUNT);
Inject (0x90, 60, 1);
LEDs.Update ();
auto Soft = Strip.m_Pixels.at (((LedFor (60)) + LED_COUNT) % LED_COUNT);
#if VELOCITY_SENSITIVE
Check ("a soft note is dimmer than a loud one", Soft[2] < Loud[2]);
Check ("a soft note is still visible", Soft[2] > 0);
#else
Check ("velocity does not change brightness", Soft[2] == Loud[2]);
#endif
Inject (0x80, 60, 0);
// --- hint channel ----------------------------------------------------
#if HINT_MIDI_CHANNEL != MIDI_CHANNEL_NONE
Inject (0x90 | HINT_MIDI_CHANNEL, 64, 127);
LEDs.Update ();
auto Hint = Strip.m_Pixels.at (((LedFor (64)) + LED_COUNT) % LED_COUNT);
Check ("a hint note lights in the hint colour", Hint != Loud && (Hint[0] || Hint[1] || Hint[2]));
// a key actually played wins over a hint on the same key
Inject (0x90, 64, 127);
LEDs.Update ();
auto Both = Strip.m_Pixels.at (((LedFor (64)) + LED_COUNT) % LED_COUNT);
Check ("a played note overrides a hint on the same key", Both == Loud);
// releasing the played note falls back to the still-pending hint
Inject (0x80, 64, 0);
LEDs.Update ();
auto Back = Strip.m_Pixels.at (((LedFor (64)) + LED_COUNT) % LED_COUNT);
Check ("releasing a played note reveals the hint again", Back == Hint);
#endif
// --- reconnect clears held notes -------------------------------------
Inject (0x90, 60, 127);
LEDs.AllOff ();
LEDs.Update ();
Check ("re-enumeration clears notes held at suspend", CountLit () == 0);
// --- short packets are not parsed as notes ----------------------------
uint8_t Short[1] = {0xF8}; // clock, 1 byte
LEDs.OnMIDIPacket (Short, 1);
LEDs.Update ();
Check ("a 1-byte realtime message lights nothing", CountLit () == 0);
// --- every key lands on the strip -----------------------------------
bool bOnStrip = true;
for (unsigned k = 0; k < KEY_COUNT; k++)
{
int nStart = LEDs.GetKeyLED (k);
if (nStart < 0 || nStart + (int) LEDS_PER_KEY > (int) LED_COUNT)
bOnStrip = false;
}
#if LED_OFFSET == 0
Check ("every key maps onto the strip", bOnStrip);
#else
// A non-zero offset deliberately shifts an end key past the strip. The
// property that must hold is that those pixels are clipped, never
// wrapped round to the far end.
(void) bOnStrip;
bool bClipped = true;
for (unsigned k = 0; k < KEY_COUNT; k++)
{
int nStart = LEDs.GetKeyLED (k);
if (OnStrip (nStart))
{
continue;
}
// Count how many of this key's pixels are actually on the strip.
unsigned nExpect = 0;
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
{
int nLED = nStart + (int) i;
if (nLED >= 0 && nLED < (int) LED_COUNT) nExpect++;
}
AllOffAndClear ();
Inject (0x90, (uint8_t) (MIDI_NOTE_MIN + k), 127);
LEDs.Update ();
if (CountLit () != nExpect) bClipped = false;
}
Check ("an offset clips off-strip pixels rather than wrapping", bClipped);
AllOffAndClear ();
#endif
// --- keys are monotonic across the keyboard --------------------------
bool bMonotonic = true;
for (unsigned k = 1; k < KEY_COUNT; k++)
{
#if STRIP_REVERSED
if (LEDs.GetKeyLED (k) > LEDs.GetKeyLED (k - 1)) bMonotonic = false;
#else
if (LEDs.GetKeyLED (k) < LEDs.GetKeyLED (k - 1)) bMonotonic = false;
#endif
}
Check ("key positions advance monotonically", bMonotonic);
#if NOTE_MAP_GEOMETRIC && !STRIP_REVERSED && LED_OFFSET == 0
// --- geometric map tracks real key positions -------------------------
// White keys should sit one white-key pitch apart, ~3.38 LEDs, not 2.
double dPitch = (double) LED_COUNT / WHITE_KEY_COUNT;
double dWorst = 0.0;
int nPrevWhite = -1;
for (unsigned k = 0; k < KEY_COUNT; k++)
{
uint8_t note = (uint8_t) (MIDI_NOTE_MIN + k);
switch (note % 12)
{
case 0: case 2: case 4: case 5: case 7: case 9: case 11:
break;
default:
continue;
}
if (nPrevWhite >= 0)
{
double d = LEDs.GetKeyLED (k) - nPrevWhite;
double e = d - dPitch;
if (e < 0) e = -e;
if (e > dWorst) dWorst = e;
}
nPrevWhite = LEDs.GetKeyLED (k);
}
Check ("white keys sit one white-key pitch apart", dWorst <= 1.0);
#endif
// --- calibration patterns --------------------------------------------
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_ENDS);
LEDs.Update ();
Check ("pattern ENDS lights exactly the two end pixels",
CountLit () == 2 && !Dark (0) && !Dark (LED_COUNT - 1));
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_ALL);
LEDs.Update ();
Check ("pattern ALL lights the whole strip", CountLit () == LED_COUNT);
bool bAllWithinCeiling = true;
#if GLOBAL_BRIGHTNESS < 255
for (auto &p : Strip.m_Pixels)
for (int c = 0; c < 3; c++)
if (p[c] > GLOBAL_BRIGHTNESS) bAllWithinCeiling = false;
#endif
Check ("pattern ALL still respects the brightness ceiling", bAllWithinCeiling);
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_WALK);
Inject (0xB0, CALIB_CC_INDEX_HI, 0);
Inject (0xB0, CALIB_CC_INDEX_LO, 5);
LEDs.Update ();
Check ("pattern WALK lights only the selected pixel",
CountLit () == 1 && !Dark (5));
// a 14-bit index beyond the strip must not paint anything
Inject (0xB0, CALIB_CC_INDEX_HI, 127);
Inject (0xB0, CALIB_CC_INDEX_LO, 127);
LEDs.Update ();
Check ("pattern WALK ignores an out-of-range index", CountLit () == 0);
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_OCTAVES);
LEDs.Update ();
Check ("pattern OCTAVES lights something", CountLit () > 0);
// notes held while a pattern runs must not survive it
Inject (0x90, 60, 127);
LEDs.Update ();
Check ("a pattern overrides note display", CountLit () > 0);
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_OFF);
LEDs.Update ();
Check ("leaving calibration restores note display",
CountLit () > 0 && !Dark (LedFor (60)));
Inject (0x80, 60, 0);
LEDs.Update ();
printf ("\n%s\n", g_nFail ? "FAILURES" : "all tests passed");
return g_nFail != 0;
}