Files
PianoLED-Circle-Edition/tests/test_pianoleds.cpp
T
prosolis 59bd3b4cea Add boot self-test and idle indicator; target the Waveshare RP2350-Plus
The board arriving is a Waveshare RP2350-Plus (4MB, USB-C), which is now the
default BOARD. Its pico-sdk header defines neither PICO_DEFAULT_LED_PIN nor
PICO_DEFAULT_WS2812_PIN - it has no onboard indicator at all - so a bare board
gives no sign of life. The status-LED support added for boards that do have one
(zero, one, tiny, usb_a on GPIO16; eth on GPIO25) is kept and compiles out here.

That makes feedback on the strip itself the useful path, and it turns out to be
the better one anyway:

- Boot self-test sweeps one pixel from index 0 to the far end once at startup.
  It answers in a single glance whether the firmware runs, PIO drives the line,
  the strip is the length LED_COUNT claims, the far end holds voltage, and -
  because you see which end it starts from - whether STRIP_REVERSED is right.
  It runs before USB, so the first test needs nothing but 5V.
- Idle indicator holds one dim pixel lit while no host is connected, separating
  "powered and waiting" from "no power" and from "crashed".

Both are platform-independent, so the Circle build gets them too.

Verified: tests pass across seventeen configurations, now including the
self-test and idle paths on and off. Both platforms build clean with no
warnings from project sources.

Note on the previous commit's verification: a filtered build log hid a real
compile error in the Pico target (sleep_ms takes uint32_t, which is unsigned
long here, and did not match the portable void(*)(unsigned) delay callback).
The build script now gets an explicit success check rather than a grep.

Claude-Session: https://claude.ai/code/session_01TVCB25LBsmeteWvaSMz4Ne
2026-08-27 23:22:56 -07:00

373 lines
11 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
static unsigned g_nDelayCalls = 0;
static void CountingDelay (unsigned) { g_nDelayCalls++; }
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 ();
LEDs.SetHostConnected (true); // tests exercise the connected path
// --- 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 ();
// --- idle indicator ---------------------------------------------------
LEDs.AllOff ();
LEDs.SetHostConnected (false);
LEDs.Update ();
#if IDLE_INDICATOR
Check ("with no host, one dim pixel marks the strip alive",
CountLit () == 1 && !Dark (0));
#else
Check ("with no host and no indicator, the strip is dark", CountLit () == 0);
#endif
// notes cannot arrive without a host, and must not display if they do
Inject (0x90, 60, 127);
LEDs.Update ();
Check ("notes do not display while no host is connected",
CountLit () <= 1);
LEDs.SetHostConnected (true);
LEDs.Update ();
Check ("reconnecting restores note display", !Dark (LedFor (60)));
LEDs.AllOff ();
LEDs.Update ();
// --- self test --------------------------------------------------------
g_nDelayCalls = 0;
LEDs.RunSelfTest (CountingDelay);
#if BOOT_SELF_TEST
Check ("self test steps once per pixel", g_nDelayCalls == LED_COUNT);
Check ("self test leaves the strip dark", CountLit () == 0);
// and the renderer must not think the strip still holds what it drew
Inject (0x90, 60, 127);
LEDs.Update ();
Check ("display works after the self test", !Dark (LedFor (60)));
LEDs.AllOff ();
LEDs.Update ();
#else
Check ("self test is compiled out", g_nDelayCalls == 0);
#endif
printf ("\n%s\n", g_nFail ? "FAILURES" : "all tests passed");
return g_nFail != 0;
}