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
160 lines
3.8 KiB
C++
160 lines
3.8 KiB
C++
//
|
|
// kernel.cpp
|
|
//
|
|
// Piano LED Visualizer for Circle.
|
|
//
|
|
// The Pi is a USB MIDI *gadget*: the PC is the host, and this firmware appears
|
|
// on the PC as an ordinary ALSA MIDI output port. Circle has no OTG support, so
|
|
// the USB controller is gadget-only in this build and the piano can never be
|
|
// plugged in here directly - all MIDI arrives from the PC. See section 2 of
|
|
// PIANO-LED-CIRCLE-PLAN.md.
|
|
//
|
|
#include "kernel.h"
|
|
#include <circle/usb/gadget/usbmidigadget.h>
|
|
#include <circle/devicenameservice.h>
|
|
#include <assert.h>
|
|
|
|
static const char FromKernel[] = "kernel";
|
|
|
|
static void SelfTestDelay (unsigned nMilliSeconds)
|
|
{
|
|
CTimer::Get ()->MsDelay (nMilliSeconds);
|
|
}
|
|
|
|
void CKernel::MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength,
|
|
unsigned nDevice, void *pParam)
|
|
{
|
|
CKernel *pThis = static_cast<CKernel *> (pParam);
|
|
assert (pThis != 0);
|
|
|
|
pThis->m_PianoLEDs.OnMIDIPacket (pPacket, nLength);
|
|
}
|
|
|
|
CKernel::CKernel (void)
|
|
: m_Timer (&m_Interrupt),
|
|
m_Logger (m_Options.GetLogLevel (), &m_Timer),
|
|
m_pUSB (new CUSBMIDIGadget (&m_Interrupt)),
|
|
m_pMIDIDevice (0),
|
|
m_PianoLEDs (m_LEDStrip)
|
|
{
|
|
m_ActLED.Blink (5); // show we are alive
|
|
}
|
|
|
|
CKernel::~CKernel (void)
|
|
{
|
|
}
|
|
|
|
boolean CKernel::Initialize (void)
|
|
{
|
|
boolean bOK = TRUE;
|
|
|
|
if (bOK)
|
|
{
|
|
bOK = m_Serial.Initialize (115200);
|
|
}
|
|
|
|
if (bOK)
|
|
{
|
|
// Headless appliance: there is no screen, so the log goes to the
|
|
// serial port and nowhere else.
|
|
bOK = m_Logger.Initialize (&m_Serial);
|
|
}
|
|
|
|
if (bOK)
|
|
{
|
|
bOK = m_Interrupt.Initialize ();
|
|
}
|
|
|
|
if (bOK)
|
|
{
|
|
bOK = m_Timer.Initialize ();
|
|
}
|
|
|
|
if (bOK)
|
|
{
|
|
// Bring the strip up before USB, so the LEDs are known-dark by the
|
|
// time the host can start sending us notes.
|
|
bOK = m_PianoLEDs.Initialize ();
|
|
}
|
|
|
|
if (bOK)
|
|
{
|
|
assert (m_pUSB != 0);
|
|
bOK = m_pUSB->Initialize ();
|
|
}
|
|
|
|
return bOK;
|
|
}
|
|
|
|
void CKernel::UpdateMIDIDevice (void)
|
|
{
|
|
assert (m_pUSB != 0);
|
|
|
|
if (!m_pUSB->UpdatePlugAndPlay ())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The gadget deletes its CUSBMIDIDevice when the host suspends the bus
|
|
// and builds a new one on the next enumeration, so the pointer we hold
|
|
// is only valid until the next status change. Re-fetch it every time.
|
|
CUSBMIDIDevice *pMIDIDevice =
|
|
(CUSBMIDIDevice *) m_DeviceNameService.GetDevice ("umidi1", FALSE);
|
|
|
|
if (pMIDIDevice == m_pMIDIDevice)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_pMIDIDevice = pMIDIDevice;
|
|
|
|
if (m_pMIDIDevice != 0)
|
|
{
|
|
// The gadget builds a new device object on each enumeration, so
|
|
// clear anything held at the moment the host went away.
|
|
m_PianoLEDs.AllOff ();
|
|
m_PianoLEDs.SetHostConnected (true);
|
|
|
|
m_pMIDIDevice->RegisterPacketHandler (MIDIPacketHandler, this);
|
|
|
|
m_Logger.Write (FromKernel, LogNotice, "USB MIDI gadget connected");
|
|
}
|
|
else
|
|
{
|
|
// Host went away mid-chord. Do not leave keys lit.
|
|
m_PianoLEDs.AllOff ();
|
|
m_PianoLEDs.SetHostConnected (false);
|
|
|
|
m_Logger.Write (FromKernel, LogNotice, "USB MIDI gadget disconnected");
|
|
}
|
|
}
|
|
|
|
TShutdownMode CKernel::Run (void)
|
|
{
|
|
m_Logger.Write (FromKernel, LogNotice, "Compile time: " __DATE__ " " __TIME__);
|
|
m_Logger.Write (FromKernel, LogNotice,
|
|
"%u LEDs, %u keys, notes %u-%u, %s orientation",
|
|
(unsigned) LED_COUNT, (unsigned) KEY_COUNT,
|
|
(unsigned) MIDI_NOTE_MIN, (unsigned) MIDI_NOTE_MAX,
|
|
STRIP_REVERSED ? "reversed" : "normal");
|
|
m_Logger.Write (FromKernel, LogNotice,
|
|
"Brightness ceiling %u/255, at most %u keys lit at once",
|
|
(unsigned) GLOBAL_BRIGHTNESS, (unsigned) MAX_LIT_KEYS);
|
|
m_Logger.Write (FromKernel, LogNotice, "Waiting for USB host");
|
|
|
|
// Runs on power alone, before any host is present.
|
|
m_PianoLEDs.RunSelfTest (SelfTestDelay);
|
|
|
|
for (;;)
|
|
{
|
|
UpdateMIDIDevice ();
|
|
|
|
// Rendering blocks for ~5.3ms of SPI traffic, which is why it runs
|
|
// here and not in the MIDI packet handler's IRQ context. Update()
|
|
// returns immediately when nothing has changed.
|
|
m_PianoLEDs.Update ();
|
|
}
|
|
|
|
return ShutdownHalt;
|
|
}
|