Files
PianoLED-Circle-Edition/pico/main.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

195 lines
4.5 KiB
C++

//
// main.cpp
//
// Piano LED Visualizer on RP2040 / RP2350.
//
// The board is a USB MIDI device: the PC is the host and owns the piano
// connection and everything else. From the PC's side this enumerates as an
// ordinary ALSA MIDI port. See PIANO-LED-CIRCLE-PLAN.md section 2.
//
#include "pianoleds.h"
#include "picostrip.h"
#include "pico/stdlib.h"
#include "tusb.h"
static CPicoLEDStrip s_Strip (LED_COUNT, WS2812_PIN);
static CPianoLEDs s_PianoLEDs (s_Strip);
// --------------------------------------------------------------------------
// Status indicator
// --------------------------------------------------------------------------
//
// Most Waveshare RP2350 boards have an onboard WS2812 rather than a plain LED,
// and the board header names the pin. Where it exists, it is the only feedback
// this headless board has: it answers "did the firmware boot" and "did the host
// enumerate us" before the strip is even wired.
#if STATUS_LED_ENABLED && defined(PICO_DEFAULT_WS2812_PIN)
#define HAVE_STATUS_LED 1
static CPicoLEDStrip s_Status (1, PICO_DEFAULT_WS2812_PIN);
enum TStatus
{
StatusFault, // strip did not initialise
StatusWaiting, // no USB host yet
StatusReady, // enumerated, running normally
StatusCalibrating // a calibration pattern is active
};
static TStatus s_LastStatus = StatusFault;
static bool s_bStatusValid = false;
static void ShowStatus (TStatus Status)
{
if ( s_bStatusValid
&& Status == s_LastStatus)
{
return; // only touch the LED when it changes
}
const uint8_t B = STATUS_LED_BRIGHTNESS;
switch (Status)
{
case StatusFault: s_Status.SetLED (0, B, 0, 0); break;
case StatusWaiting: s_Status.SetLED (0, B, B / 2, 0); break;
case StatusReady: s_Status.SetLED (0, 0, B, 0); break;
case StatusCalibrating: s_Status.SetLED (0, 0, 0, B); break;
}
s_Status.Update ();
s_LastStatus = Status;
s_bStatusValid = true;
}
#else
#define HAVE_STATUS_LED 0
static void ShowStatus (int) {}
enum { StatusFault, StatusWaiting, StatusReady, StatusCalibrating };
#endif
// Number of MIDI bytes carried by a USB MIDI event packet, indexed by its
// Code Index Number (USB MIDI 1.0, table 4-1). 0 means "not a message we
// forward".
static const uint8_t s_CINLength[16] =
{
0, 0, 2, 3, 3, 1, 2, 3,
3, 3, 3, 3, 2, 2, 3, 1
};
// --------------------------------------------------------------------------
// TinyUSB device callbacks
// --------------------------------------------------------------------------
extern "C" void tud_mount_cb (void)
{
// Fresh session: nothing should still be lit from the last one.
s_PianoLEDs.AllOff ();
}
extern "C" void tud_umount_cb (void)
{
// Host went away, possibly mid-chord. Do not leave keys lit.
s_PianoLEDs.AllOff ();
}
extern "C" void tud_suspend_cb (bool remote_wakeup_en)
{
(void) remote_wakeup_en;
s_PianoLEDs.AllOff ();
}
extern "C" void tud_resume_cb (void)
{
s_PianoLEDs.AllOff ();
}
// --------------------------------------------------------------------------
// sleep_ms takes uint32_t, which is unsigned long on this target; the portable
// header deliberately knows nothing about platform types.
static void DelayMs (unsigned nMilliSeconds)
{
sleep_ms (nMilliSeconds);
}
static void PollMIDI (void)
{
uint8_t Packet[4];
while (tud_midi_available ())
{
if (!tud_midi_packet_read (Packet))
{
break;
}
// Packet[0] is cable number (high nibble) and Code Index Number
// (low nibble); the message itself is in Packet[1..3].
unsigned nLength = s_CINLength[Packet[0] & 0x0F];
if (nLength == 0)
{
continue;
}
s_PianoLEDs.OnMIDIPacket (&Packet[1], nLength);
}
}
int main (void)
{
stdio_init_all ();
#if HAVE_STATUS_LED
s_Status.Initialize ();
#endif
if (!s_Strip.Initialize ())
{
// Sit here showing red. A dark board would look like a power
// problem; this says the firmware ran and the strip did not.
while (true)
{
ShowStatus (StatusFault);
tight_loop_contents ();
}
}
s_PianoLEDs.Initialize ();
// Before USB, so the sweep runs on power alone and needs no PC.
s_PianoLEDs.RunSelfTest (DelayMs);
tusb_init ();
while (true)
{
tud_task ();
PollMIDI ();
// Rendering blocks for the strip's frame time, so it runs here
// rather than in the USB callback. Returns immediately when
// nothing has changed.
s_PianoLEDs.Update ();
s_PianoLEDs.SetHostConnected (tud_mounted ());
if (!tud_mounted ())
{
ShowStatus (StatusWaiting);
}
else if (s_PianoLEDs.GetCalibrationPattern () != CALIB_PATTERN_OFF)
{
ShowStatus (StatusCalibrating);
}
else
{
ShowStatus (StatusReady);
}
}
return 0;
}