Files
PianoLED-Circle-Edition/PIANO-LED-CIRCLE-PLAN.md
T
prosolis 138efc28ca BOM: confirm the plain Zero and Zero W as verified fallbacks
The BOM claimed "Zero W also works" as an aside. Checked it: Circle's
CHANGELOG gives gadget support for "Zero (2) (W)", the parentheses including
the plain Zero, and the README lists Raspberry Pi Zero as Tested with no
caveat. build.sh already emits kernel.img for RASPPI=1, and the MIDI gadget's
string descriptors are present in that image, so no build change is needed
for either board.

Keeping the Zero 2 W as the specified part on availability grounds. Records
that a plain Zero would permanently close the WiFi option in section 3a,
since it has no radio silicon at all.

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

15 KiB

Piano LED Visualizer on Circle (bare metal)

Kickoff document. Written 2026-08-27.

Everything below is split into VERIFIED (checked against a primary source this session, with the source named) and ASSUMED (plausible, load-bearing, not yet confirmed). Do not promote an ASSUMED item to fact without checking it.


1. Goal

An LED strip above the keys of a Casio Privia PX-S7000 that lights the key being played, and can light the next key to play when driven by learning software.

Explicit non-goal: this is not a port of onlaj/Piano-LED-Visualizer. That project is ~95% web UI, WiFi hotspot, song library, LCD menu, and MIDI file playback. In this architecture the PC owns all of that. What remains is a few hundred lines of greenfield firmware.

2. Architecture (settled)

Casio PX-S7000  --USB-B--> PC (Debian 13, EliteOne AIO)  --USB--> Pi Zero (Circle) --> WS2812B strip
                            |                                                              ^
                            +-- Neothesia / sheet reader                                   |
                                                                                    5V PSU +

The PC is the hub. It is the only thing that talks to the piano. ALSA's sequencer fans the piano's input port out to both the on-screen software and the Pi.

The Pi presents itself to the PC as a USB MIDI device. From the PC's side it is just another ALSA MIDI output port; aconnect the piano's input to it and the job is done.

Why this shape

  • No network stack on the Pi at all. This was the deciding factor. An appliance Linux box with an unauthenticated web UI that nobody ever patches is a real home network risk, not a theoretical one. Circle with a USB gadget has no shell, no sshd, no web server, no package manager, and no network interface. The category is deleted rather than mitigated.
  • No 2.4GHz contention. Pi Zero and Zero 2 W are 2.4GHz-only, which is the same band as the WU-BT10's BLE. Using USB avoids reasoning about it.
  • Deterministic latency, no scheduler, no WiFi power-save.
  • Still builds in five years. A pinned Circle tree does. A Python app with pip dependencies does not.
  • Power-cycle safe. No writable filesystem to corrupt when the thing gets killed at the wall.

Consequence to be aware of

VERIFIED: "Circle does not support OTG protocols, so the USB controller always works in host or gadget mode and the connected peer must work in the opposite mode." (Circle CHANGELOG)

So the Pi is gadget-only in this build. It cannot also host a USB MIDI device, which means it can never take MIDI directly from the piano. All MIDI arrives from the PC. That is exactly the architecture above, so this costs nothing here, but it does close off "plug the piano into the Pi" as a fallback.

3. Verified facts

Circle USB MIDI gadget

  • Introduced in Release 45.3 (2023-10-06): "This release comes with initial USB gadget (aka device, peripheral) mode support, which is used to implement an USB MIDI (v1.0) gadget."
  • Class CUSBMIDIGadget, header include/circle/usb/gadget/usbmidigadget.h.
  • Usage: instantiate CUSBMIDIGadget instead of CUSBHCIDevice, then call Initialize() and UpdatePlugAndPlay() as in host mode.
  • Supported models: "Raspberry Pi models (3)A(+), Zero (2) (W) and 4B". The Zero and Zero 2 W are explicitly supported. The MiniDexed docs mention only Pi 3/4; that was incomplete, not a restriction.
  • Source: https://github.com/rsta2/circle/blob/master/CHANGELOG.md and https://circle-rpi.readthedocs.io/en/47.0/subsystems/usb.html

Circle WS28XX driver (addon/WS28XX/)

  • ws28xxstripe.cpp/h - single-channel stripe driver
  • ws2812oversmi.cpp/h - multi-channel driver over the Secondary Memory Interface
  • sample/ - example programs, including sample/multichan/
  • Only the single-channel driver is needed here. One strip, one data line.

Casio WU-BT10

  • Bluetooth 5.0. Profiles: A2DP (audio) and GATT (MIDI over BLE). Codec SBC.
  • USB bus powered, 0.25W, no separate supply.
  • This is the project's Bluetooth MIDI path, and it terminates at the PC, not the Pi. Piano -> WU-BT10 -> PC over BLE GATT, then PC -> Pi over USB as usual. See section 3a for why it cannot terminate at the Pi.
  • A2DP is also available for streaming audio from the PC to the piano's own speakers, if wanted.
  • Source: https://support.casio.com/en/support/answer.php?cid=008006001002&qid=104463&num=1

Reference build (https://yanniznik.com/building-a-led-piano-visualizer/)

  • WS2812B, 144 LEDs/m, ~172 LEDs, 1.5-2m strip
  • 5V 6A supply, 22-18 AWG wire, 5.5x2.5mm DC socket
  • Aluminum LED profile with diffuser, double-sided tape
  • ~$75-100 total. Includes a $30 iConnectivity mio MIDI interface we do not need, because the PX-S7000 has USB-B.

3a. Bluetooth and WiFi on Circle

Checked against the pinned Circle tree (Step51) on 2026-08-27, after the question was raised of supporting Bluetooth and WiFi on the Pi alongside USB.

Bluetooth: not possible on Circle. Two independent blockers.

VERIFIED: Circle's CHANGELOG states "As announced the Bluetooth support has been removed for legal reasons." What remains, lib/usb/usbbluetooth.cpp, is a bare HCI transport for a USB dongle with no stack above it. There is no BLE and no GATT anywhere in the tree, and the removed stack was classic Bluetooth in any case. MIDI over BLE is GATT, so even the deleted code would not have served.

VERIFIED: that transport would need a dongle in USB host mode, and Circle is host-or-gadget and never both (see section 2). The Pi is a gadget here.

The consequence is not a loss. The WU-BT10 already does GATT MIDI to the PC, and the PC is the hub, so Bluetooth MIDI is available today with no firmware change at all. It simply terminates one hop earlier than the question assumed.

WiFi: possible, but it is a project, not a flag.

VERIFIED: addon/wlan is a Plan 9 derived BCM4343 driver (under its own Plan 9 license), usable with Circle's TCP/IP stack and a bundled WPA supplicant.

Four costs, all VERIFIED unless marked:

  1. Circle's README lists the Raspberry Pi Zero 2 W as "WLAN unknown for new revision". The Zero W is Tested. That is the opposite way round from the BOM's preference in section 5.
  2. Circle ships mDNS advertisement for _apple-midi._udp but not the RTP-MIDI protocol itself. The session layer would have to be written, or ported from MiniDexed, which implements exactly this on Circle.
  3. WLAN loads firmware blobs from RPi-Distro/firmware-nonfree off the SD card at boot, which reintroduces filesystem reads and weakens the power-cycle-safe property that section 2 was built around.
  4. ASSUMED: that WLAN coexists with USB gadget mode. WLAN is SDIO and so it probably does, but both addon/wlan samples pair it with USB host, so this is unproven and would need a spike before anything is committed to.

And it reintroduces exactly the LAN attack surface that section 2 names as the deciding factor for this architecture.

Decision (2026-08-27): USB first. Bluetooth is taken on the PC side via the WU-BT10, at no cost. WiFi is deferred, not rejected; if it is revisited, cost 4 above is the first thing to settle, because it may close the option outright.

4. Assumptions to verify before spending money

  1. PX-S7000 is class-compliant USB MIDI. Almost certainly true for a 2022 instrument, but unconfirmed. Test: plug into the PC, run aconnect -l, look for it. This is step one of Phase 0 and it gates everything.
  2. Exact GPIO/pin for CWS28XXStripe. Circle's single-channel driver is believed to clock the waveform out over SPI, which would put data on the MOSI pin, but the pin and the peripheral were not confirmed. Read addon/WS28XX/sample/ before wiring anything.
  3. Mounting geometry. See section 8. Unsolved and unbudgeted.
  4. PLV web UI authentication. Only matters if Phase 0 uses that project rather than a throwaway script. Assume it is unauthenticated until shown otherwise.

5. Bill of materials

Item Note
Raspberry Pi Zero 2 W VERIFIED: the plain Zero and Zero W work too. build.sh already emits kernel.img for them, and the gadget's string descriptors are present in it, so either is a drop-in if Zero 2 W supply is a problem. Chosen anyway as the current and most available part; the quad core is headroom this workload does not need. Note a plain Zero would permanently close the WiFi option in section 3a. Headers not needed if the LED line is soldered.
microSD card Circle boots from FAT32. Tiny card is fine.
WS2812B strip, 144 LEDs/m, 2m Need 176 LEDs = 1.222m. Buy 2m, cut to length.
5V PSU, 6A See the power note in section 7 before assuming this is enough.
74AHCT125 level shifter See section 7. Do not skip this on the assumption 3.3V will work.
Aluminum LED profile + diffuser, ~1.3m The article is right that bare strip looks bad.
DC barrel socket 5.5x2.5mm, quick connect
22-18 AWG wire Including a second run for power injection at the far end.
USB cable, PC to Pi Zero Micro-USB, and it must be a data cable. Plug into the Zero's USB port, not PWR.

Roughly $50-70 with the mio omitted.

6. Note-to-LED mapping

88 keys span MIDI notes 21 (A0) through 108 (C8).

At 2 LEDs per key: 176 LEDs. At 144 LEDs/m that is 1.222m, which is why 144/m is the density everyone uses. It lines up with a standard 88-key keybed almost exactly.

led_index = (note - 21) * 2

Light led_index and led_index + 1. Clamp to [0, 175] and drop anything outside 21-108 rather than trusting the input.

If the strip is mounted with pixel 0 at the treble end, invert: led_index = (108 - note) * 2. Decide this after the strip is physically mounted, and make it a compile-time constant so it is one flag to flip.

7. Electrical notes

Power budget, and why 6A is a bet. 176 LEDs at full white draw ~60mA each = 10.56A theoretical maximum. A 6A supply does not cover that. It does not need to: playing a ten-finger chord lights 20 LEDs, about 1.2A, so 6A is enormous headroom for real use.

But this makes a global brightness clamp a firmware requirement, not a nicety. A bug that sets every pixel to white on a 6A supply browns out the rail. Cap global brightness in the firmware, and cap the number of simultaneously lit pixels if you ever add an idle animation.

Level shifting. WS2812B wants logic high at 0.7 x VDD, which is 3.5V on a 5V rail. The Pi's GPIO is 3.3V. This is marginal by design. It frequently works and then intermittently doesn't, which is the worst failure mode to debug. Use a 74AHCT125 on the data line. This is the single most common cause of "the strip flickers and I don't know why."

Common ground. The Pi's ground and the LED supply's ground must be tied together. Non-optional.

Power injection. Feed 5V at both ends of the strip. Over 1.2m at 144/m the far end otherwise drifts dim and yellow.

Refresh floor. WS2812B is 1.25us per bit, 24 bits per LED, so 30us per LED. 176 LEDs = 5.3ms per full frame, plus a ~50us reset. Max ~190 Hz. Irrelevant for this application but it is the hard latency floor, and it dominates the USB MIDI transit time.

8. The actual hard part

The PX-S7000 is a slab. It has no shelf above the keybed the way an upright piano does. The aluminum profile has to sit above the keys, parallel to them, at a fixed offset, with nothing obvious to attach to.

This is the part of the project that will consume the most time, and it is not a software problem. Solve it, or at least prototype it in cardboard, before ordering electronics. Take measurements off the actual instrument.

9. Phases

Phase 0 - bench validation on Linux (disposable, one afternoon)

The purpose is to separate "my wiring is wrong" from "my firmware is wrong." On the first power-up of a strip you soldered yourself, that is the only question that matters, and bare metal gives you no debugging surface to answer it.

Throw all of this away afterwards. It never gets mounted, never gets a hostname, never sits on the LAN permanently.

  1. Piano to PC over USB-B. Confirm it appears in aconnect -l.
  2. Pi Zero on Raspberry Pi OS, rpi_ws281x, Python REPL. Configure it for SPI, so the data line is GPIO10 (physical pin 19). The stock rpi_ws281x examples drive the strip from GPIO18 over PWM, but the Circle firmware uses SPI on GPIO10. Bench on the default pin and the wiring you prove is not the wiring you keep.
  3. Confirm: pixel 0 is at the end you think it is; (note-21)*2 lights the right key; the far end holds voltage under load; the colors look right through the diffuser, which is not the same as how they look bare.
  4. Decide brightness, colors, and whether velocity modulates anything. These are product decisions, and answering them here means the firmware gets written once.

Exit criteria: you know the hardware is good and you know exactly what you want the firmware to do.

Phase 1 - Circle firmware [DONE 2026-08-27, unverified on hardware]

Written and building; see firmware/ and README.md. Both kernel images build clean against Circle Step51. Steps 2 and 3 were resolved by reading the Circle sources rather than by trial: the WS28XX driver clocks the strip over SPI, putting data on MOSI = GPIO10 = physical pin 19, and the gadget lifecycle follows sample/29-miniorgan, which already carries a USB_GADGET_MODE path.

Nothing here has touched a real LED or enumerated on a real PC. The riskiest untested part is USB enumeration, and it is blocked by section 10 along with all of Phase 0.

  1. Clone Circle, pin the release, build a stock sample for the Zero to confirm the toolchain and the boot path.
  2. Build and run addon/WS28XX/sample/. Read it first for the pin assignment.
  3. Build and run a USB MIDI gadget sample. Confirm the PC enumerates it and it shows up in aconnect -l on the PC side.
  4. Join the two. CUSBMIDIGadget in, CWS28XXStripe out, the mapping from section 6 in between.
  5. Global brightness clamp per section 7.
  6. Compile-time flag for strip orientation.

The whole thing is a few hundred lines. The risk is in steps 2 and 3, not step 4.

Phase 2 - mechanical

Per section 8. Can proceed in parallel with Phase 1, and should, because it has the longest lead time and the most unknowns.

Phase 3 - learning integration

Lighting the next note to play, driven by software on the PC. Deliberately deferred. It is a protocol design question and it depends on what the PC ends up running.

10. Sequencing warning

At time of writing there is no PC. The EliteOne 800 G5 negotiation is unresolved. Phase 0 step 1 cannot start without a machine.

Phase 2 does not depend on the PC and can start immediately with a tape measure.