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
373 lines
11 KiB
C++
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;
|
|
}
|