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
195 lines
4.5 KiB
C++
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;
|
|
}
|