Skip to main content

Circuit Playground

Adafruit's round board, with an accelerometer, light, sound and temperature sensors, eight touch pads, ten NeoPixels, two buttons and a slide switch. George's firmware turns it into a MIDI controller with one sensor on duty at a time, depending on the mode. The firmware is from a 2019 Web MIDI workshop and hasn't changed since.

Source of truth

The firmware is public: github.com/georgemandis/circuit-playground-midi-multi-tool. This page comes from reading that .ino. Where they disagree, the code wins.

Pick a mode first​

To change or view the current mode your Circuit Playground is in, toggle the slide switch so that one or more NeoPixel LEDs light up and stay on:

Circuit Playground slide switch

With the slide switch toggled you can change modes by pressing the left and right buttons on the board. The number of lit pixels is the mode number, one pixel for mode 1 up to all ten for mode 10, and a pitch plays on each press:

Circuit Playground buttons

Toggle the switch back and the mode starts.

It's one mode at a time, so a sketch that wants both light and touch needs two boards. The buttons and the switch only pick the mode. They never send MIDI, so if the Explorer shows nothing when you press a button, that's expected.

The ten modes​

Everything the board sends is on channel 2: status 0x91 for notes, 0xB1 for control changes.

Modes that send MIDI​

ModeWhat it does
1Cap touch. Each of the eight pads is a note: note on with velocity 127 when touched, note off on release. Note number is 1 + the pad's pin (table below). The pixels flash a random colour on each touch, and the buttons nudge the touch sensitivity.
2Light. CC 1 with the light level scaled to 0..127, once a second.
3Sound. CC 1 with the sound level scaled to 0..127, once a second.
4Temperature. CC 1 with degrees Celsius as the value, once a second.
5Random notes. A note on with a random note and random velocity, every 50 ms or so. The buttons speed it up and slow it down.
6Accelerometer. Three note ons in a row every 200 ms, so five times a second: x, y, then z, each as note = m/s² + 20, velocity 127. 20 is zero g, 30 is about one g. Flat on a table reads roughly 20, 20, 30.
7Tap. A tap on the board flashes all ten pixels a random colour and prints to the serial port. No MIDI. The firmware README says it sends a message; the code that would is commented out. See below.

Modes that receive MIDI​

ModeWhat it does
8Speaker. Plays any incoming note on over the built-in speaker, at the note's pitch, for 50 ms.
10Colour mixer. A note on on channel 1 sets red, channel 2 sets green, channel 3 sets blue, each to note + velocity (0 to 254), on all ten pixels at once.

Neither​

ModeWhat it does
9Random tones on the speaker and random pixel flashes. No MIDI in or out. For funsies.

No mode does both.

Touch pads, helper index to firmware pin to note:

touch(i)01234567
pin3201126910
note43121371011

About mode 7​

The firmware README says mode 7 "converts the accelerometer tap detection to a MIDI message". The helpers' README says it sends nothing. The code: setup() arms the tap detector and attaches an interrupt. In mode 7 the interrupt prints the tap to serial, sets a flag and flashes all ten pixels. The part of mode 7 that would send noteOn(2, 1, 127) sits under "Accelerometer detect tap: TBD" and is commented out. So the README overstates it. A tap is felt and shown on the board and no MIDI leaves it. A hardware check is still pending. For a buzzer or a tap, use a touch pad in mode 1.

Start here​

Open starters/java/CircuitPlaygroundStarter/CircuitPlaygroundStarter.pde.

CircuitPlaygroundStarter.pde
// Circuit Playground starter: the eight touch pads light a ring, tilt rolls a ball,
// the light sensor sets the sky. The firmware sends ONE sensor at a time: flip the slide
// switch and use the two buttons to pick a mode (see HARDWARE-CHECKLIST.md).
// No board: keys 1-8 are the pads, the mouse tilts. Edit the lines marked "change this".
CircuitPlayground cpx;

void setup() {
size(800, 600);
cpx = new CircuitPlayground(this); // change this: new CircuitPlayground(this, "name") if it isn't found
cpx.connect(); // prints the MIDI devices it found; fine with no device
}

void draw() {
float tiltX = cpx.accel.x, tiltY = cpx.accel.y; // -1..1, one g = 1 (accelerometer mode)
if (!cpx.connected()) { // mouse stand-in
tiltX = mouseX * 2.0 / width - 1;
tiltY = mouseY * 2.0 / height - 1;
}
background(lerpColor(color(10, 10, 40), color(255, 230, 120), cpx.light)); // change this: light is 0..1
translate(width / 2, height / 2);
for (int i = 0; i < 8; i++) { // the eight capacitive pads, as a ring
float a = TWO_PI * i / 8;
fill(cpx.touch(i) ? color(255, 80, 120) : color(70, 70, 90)); // change this: a colour per pad?
circle(cos(a) * 220, sin(a) * 220, cpx.touch(i) ? 90 : 50);
}
fill(255);
circle(tiltX * 200, tiltY * 200, 60 + cpx.sound * 100); // change this: the ball; sound is 0..1
}

The helper​

new CircuitPlayground(this)Connects by USB-name substring. The board's USB name depends on how it was flashed. The Explorer prints it.
cpx.touch(i), cpx.touchJustPressed(i), cpx.anyTouch()The eight pads, index 0 to 7 as in the table above. Mode 1.
cpx.light, cpx.sound (0 to 1), cpx.temperature (°C), cpx.sensor (raw CC 1)All three follow the same CC 1. Which one means anything depends on the mode the board is in.
cpx.accel.x, .y, .z-1 to 1, one g is 1. Mode 6.
cpx.mode"touch", "accel", "sensor", "notes" or empty: the kind of traffic last recognised.
cpx.pixels(r, g, b), cpx.clear()All ten pixels one colour, for a board in mode 10.
void touchPressed(int pad), void touchReleased(int pad), void accelChanged()Define any in your sketch and the helper calls it. The generic noteOn and controlChange work too.

From the helpers' README​

This differs from the original API contract. The firmware at github.com/georgemandis/circuit-playground-midi-multi-tool (main, 2026-10-02) sends one sensor at a time, chosen by its mode, and never sends the buttons or the slide switch (they pick the mode). So there is no buttonA, buttonB or slideSwitch, and light, sound and temperature all come from the same CC 1. Only the mode the board is in tells them apart. The helper exposes what arrives, plus mode, the kind of traffic it last recognised.

CircuitPlayground cpx = new CircuitPlayground(this); // or (this, "name")
cpx.touch(i); cpx.touchJustPressed(i); cpx.touchJustReleased(i); cpx.anyTouch(); // i = 0..7 (table below)
cpx.accel.x; cpx.accel.y; cpx.accel.z; // -1..1, one g = 1, clamped. accel.rawX etc. are the notes
cpx.light; cpx.sound; // 0..1 (CC 1 / 127; both follow CC 1)
cpx.temperature; // whole degrees C (the raw CC 1 value)
cpx.sensor; // raw CC 1 value 0..127, -1 if never
cpx.mode; // "touch", "accel", "sensor", "notes" or "" (nothing yet)
cpx.pixels(r, g, b); cpx.pixel(i, r, g, b); cpx.clear(); // all ten NeoPixels, one colour (mode 10)
cpx.padForPin(pin); cpx.padForNote(note); cpx.noteForPad(i); cpx.accelFromNote(note);

Callbacks: touchPressed(int pad), touchReleased(int pad), accelChanged(). Stand-in: keys 1..8 are the touch pads.

What the firmware sends. All output is on status 0x91, channel 2 (the sketch calls noteOn(1, ...) with a 0-based channel). Mode numbers are the firmware README's. The NeoPixel ring shows mode − 1 pixels while the slide switch is on; left and right buttons change it.

ModeSendsHelper fields
1 cap touchNote On vel 127 on touch, Note Off on release; note = 1 + pintouch(i), callbacks
2 lightCC 1 = light / 1023 · 127, once a secondlight, sensor
3 soundCC 1 = sound / 1023 · 127, once a secondsound, sensor
4 temperatureCC 1 = °C as an integer, once a secondtemperature, sensor
5 random notesNote On, random note and velocity, every ~50 ms (buttons change the rate)mode == "notes"; use noteOn() or AnyMidi
6 accelerometerthree Note Ons in a burst every 200 ms: X, Y, Z as round(m/s² + 20), vel 127accel
7 tapnothing over MIDI (pixels flash locally)-
8 speakerplays any incoming Note Onpixels() will also beep it
9 random tonesnothing over MIDI-
10 colour mixerlistens: Note On on channel 1 / 2 / 3 sets red / green / blue = note + velocity (0..254), all pixelspixels(), clear()

Touch pads, index → firmware pin → note:

touch(i)01234567
pin3201126910
note43121371011

Touch and accelerometer are both Note Ons on the same channel. Three Note Ons within 60 ms count as one accelerometer reading (touch never does that); any Note Off means touch. In accelerometer mode a note that collides with a pad note (board on its side gives about note 10) is not reported as a touch. pixel(i, ...) sets all ten pixels because the firmware has no per-pixel message.

Python: CircuitPlayground(this, name="circuit playground"), cpx.touch(i), cpx.touch_just_pressed(i), cpx.accel.x, cpx.light, cpx.sound, cpx.temperature, cpx.sensor, cpx.mode, cpx.pixels(r, g, b), cpx.clear(); callbacks touch_pressed(pad), touch_released(pad), accel_changed().

Try first​

  • Mode 2: a sketch whose background follows the room light. Cover the sensor with your hand.
  • Mode 3: clap to spawn a shape. The value is loudness.
  • Mode 6: a bouncy ball that rolls the way you tilt the board.
  • Mode 1: eight touch pads, eight notes. Wire a fruit to one with a clip lead and play it.
  • Mode 10: send notes at it from the Explorer's send panel and mix a colour on the pixels.

More on the ideas page.

Projects for this board​

The projects from the 2019 workshop, which used these boards with Web MIDI. They are being rebuilt in ports/ and land here as they finish.

Colour mixer, mode 10​

In mode 10 a sketch mixes colours on the onboard NeoPixels by sending note on messages to channels 1, 2 and 3 for red, green and blue. The device listens for status bytes 144, 145 and 146, note ons for MIDI channels 1 to 3.

Because we're limited to two 7-bit bytes instead of a single 8-bit value there's a curious "bug" with this approach. Can you identify it? Can you fix it?

CircuitPlaygroundColorMixer.pde
// Colour Mixer (George's 2019 workshop project, firmware mode 10). Three sliders mix a colour on screen and
// the board's ten NeoPixels follow in real time. Needs MidiCore.pde + CircuitPlayground.pde for connecting;
// the colour messages themselves are sent raw below so you can see the bytes.
//
// Mode 10: a Note On on channel 1 sets red, channel 2 green, channel 3 blue, and the value is note + velocity.
// Why the sum? From 2019: "Because we're limited to two 7-bit messages instead of a single 8-bit message
// there's a curious 'bug' with this approach... Can you identify it? Can you fix it?"
// A MIDI data byte is 0..127, so one byte only reaches half brightness. The fix splits the 0..255 value across
// note and velocity: 200 = note 127 + velocity 73. Even fixed, 127 + 127 = 254, so pure white is one step out
// of reach: the other half of the bug. Press B to flip between the naive send and the fix; W is white.
// Mouse: drag the three sliders. Keys: B bug/fix, W white, K black. Works without a board (nothing to light).
CircuitPlayground cpx;
int[] rgb = { 200, 80, 40 };
String[] NAMES = { "red", "green", "blue" };
boolean fixed = true;
int dragging = -1;

void setup() {
size(600, 420);
textSize(16);
cpx = new CircuitPlayground(this);
cpx.connect();
send();
}

void send() { // one Note On per colour: channel 1 red, 2 green, 3 blue
for (int i = 0; i < 3; i++) {
int v = rgb[i];
if (fixed) { int note = min(127, v); cpx.core.noteOn(i + 1, note, v - note); } // split: note + velocity = v (max 254)
else cpx.core.noteOn(i + 1, min(127, v), 0); // naive: one 7-bit byte, tops out at 127
}
}

int boardValue(int v) { return fixed ? min(254, v) : min(127, v); } // what the board actually shows

void draw() {
background(30);
noStroke();
fill(rgb[0], rgb[1], rgb[2]);
rect(20, 20, 270, 100, 12);
fill(boardValue(rgb[0]), boardValue(rgb[1]), boardValue(rgb[2]));
rect(310, 20, 270, 100, 12);
fill(220);
text("on screen: rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")", 20, 140);
text("on the board: rgb(" + boardValue(rgb[0]) + ", " + boardValue(rgb[1]) + ", " + boardValue(rgb[2]) + ") " + (fixed ? "fix: note + velocity" : "BUG: one 7-bit byte"), 310, 140);
for (int i = 0; i < 3; i++) {
float y = 190 + i * 70;
fill(60); rect(60, y - 4, 510, 8, 4);
fill(i == 0 ? color(255, 80, 80) : i == 1 ? color(80, 255, 80) : color(80, 120, 255));
circle(60 + rgb[i] * 2, y, 28);
fill(220);
text(NAMES[i] + " " + rgb[i], 60, y - 16);
int note = min(127, rgb[i]), vel = fixed ? rgb[i] - note : 0;
text("[" + (0x90 + i) + ", " + note + ", " + vel + "]", 420, y - 16); // the bytes that just went out
}
text("drag sliders B bug/fix W white K black" + (cpx.connected() ? "" : " (no board connected)"), 20, height - 16);
}

void mousePressed() { for (int i = 0; i < 3; i++) if (abs(mouseY - (190 + i * 70)) < 20) dragging = i; mouseDragged(); }
void mouseDragged() { if (dragging >= 0) { rgb[dragging] = constrain((mouseX - 60) / 2, 0, 255); send(); } }
void mouseReleased() { dragging = -1; }
void keyPressed() {
if (key == 'b') fixed = !fixed;
else if (key == 'w') rgb = new int[] { 255, 255, 255 };
else if (key == 'k') rgb = new int[] { 0, 0, 0 };
send();
}

Simple synthesizer, mode 8​

The sketch is the keyboard, the board's speaker is the voice. Every note on the sketch sends, the board plays.

CircuitPlaygroundSimpleSynth.pde
// Simple Synth (2019 workshop project, firmware mode 8). An on-screen keyboard that plays the board's speaker:
// in mode 8 the board plays any Note On it receives for 50 ms. Needs MidiCore.pde + CircuitPlayground.pde.
// Click the keys, or type: a s d f g h j k l are the white keys from C, w e t y u the black ones. Z / X shift
// octaves. Because each beep is only 50 ms, a held key is re-sent every 60 ms so it sounds continuous.
// Without a board the keys still light up; there is just nothing to hear.
CircuitPlayground cpx;
final String WHITE = "asdfghjkl", BLACK = "wetyu";
final int[] WHITE_OFF = { 0, 2, 4, 5, 7, 9, 11, 12, 14 }; // semitones above C for the nine white keys
final int[] BLACK_OFF = { 1, 3, 6, 8, 10 }; // and the five black keys
final int[] BLACK_POS = { 0, 1, 3, 4, 5 }; // which white key each black key sits after
int base = 60, held = -1, lastSent = 0; // base: middle C

void setup() {
size(540, 300);
textSize(14);
cpx = new CircuitPlayground(this);
cpx.connect();
}

void play(int note) { held = note; lastSent = 0; }

void draw() {
if (held >= 0 && millis() - lastSent >= 60) { cpx.core.noteOn(1, held, 100); lastSent = millis(); } // re-trigger the 50 ms beep
background(30);
for (int i = 0; i < 9; i++) { // white keys
fill(held == base + WHITE_OFF[i] ? color(255, 200, 60) : 240);
stroke(30); rect(i * 60, 60, 60, 220);
fill(80); text(WHITE.charAt(i) + "", i * 60 + 25, 265);
}
for (int i = 0; i < 5; i++) { // black keys
fill(held == base + BLACK_OFF[i] ? color(255, 200, 60) : 20);
rect(BLACK_POS[i] * 60 + 40, 60, 40, 130);
fill(200); text(BLACK.charAt(i) + "", BLACK_POS[i] * 60 + 54, 180);
}
noStroke(); fill(220);
text("octave " + (base / 12 - 1) + " Z / X octave down / up " + (cpx.connected() ? "playing on the board" : "no board: keys light but stay silent") + (held >= 0 ? " note " + held : ""), 10, 30);
}

int noteAt(float x, float y) { // which key is under the mouse
for (int i = 0; i < 5; i++) if (y < 190 && x >= BLACK_POS[i] * 60 + 40 && x < BLACK_POS[i] * 60 + 80) return base + BLACK_OFF[i];
return y >= 60 ? base + WHITE_OFF[constrain((int) x / 60, 0, 8)] : -1;
}
void mousePressed() { play(noteAt(mouseX, mouseY)); }
void mouseDragged() { held = noteAt(mouseX, mouseY); }
void mouseReleased() { held = -1; }

void keyPressed() {
int w = WHITE.indexOf(key), b = BLACK.indexOf(key);
if (w >= 0) play(base + WHITE_OFF[w]);
else if (b >= 0) play(base + BLACK_OFF[b]);
else if (key == 'z') base = max(24, base - 12);
else if (key == 'x') base = min(96, base + 12);
}
void keyReleased() { held = -1; }

Quiz buzzer, mode 1​

Boards as buzzers: who buzzed first? In 2019 this used mode 7, the tap. That sends nothing over MIDI, so the buzzer is a touch pad in mode 1. Slap the pad.

CircuitPlaygroundQuizBuzzer.pde
// Quiz Buzzer (2019 workshop project). Several Circuit Playgrounds, one per team; the first to send a message
// wins, the others are locked out until R resets. Uses firmware mode 1 (touch a pad to buzz): the 2019 notes say
// mode 7 "tap", but in the firmware the tap send is commented out ("Accelerometer detect tap: TBD"), so mode 7
// sends nothing. Any note on channel 2 counts, so the pads do not have to be wired to anything.
// No helper here: the helper opens one device, and this needs every board, so it opens every MIDI input whose
// name contains "circuit playground" itself and numbers them as teams. No boards: keys 1-4 buzz.
import javax.sound.midi.*;

ArrayList<String> boards = new ArrayList<String>();
final color[] COLORS = { #ff5d73, #ffd166, #06d6a0, #4cc9f0, #c77dff, #ff9f1c };
volatile int winner = -1; // set from the MIDI thread, read in draw()
int wonAt = 0;

void setup() {
size(800, 500);
textAlign(CENTER, CENTER);
for (MidiDevice.Info info : MidiSystem.getMidiDeviceInfo()) try {
MidiDevice dev = MidiSystem.getMidiDevice(info);
if (dev.getMaxTransmitters() == 0 || dev instanceof Sequencer) continue; // inputs only
println("input: " + info.getName());
if (!info.getName().toLowerCase().contains("circuit playground")) continue; // change this for other devices
final int team = boards.size(); // this board's team number
dev.open();
dev.getTransmitter().setReceiver(new Receiver() {
public void send(MidiMessage m, long t) {
byte[] b = m.getMessage();
if (b.length == 3 && (b[0] & 0xF0) == 0x90 && (b[2] & 0xFF) > 0) buzz(team); // any Note On from this board
}
public void close() { }
});
boards.add(info.getName());
} catch (MidiUnavailableException e) { println("could not open " + info.getName()); }
println(boards.size() + " board(s) connected");
}

synchronized void buzz(int team) { // first one in wins; synchronized settles ties
if (winner >= 0) return;
winner = team;
wonAt = millis();
}

void draw() {
int n = max(boards.size(), 4);
background(winner < 0 ? color(20, 24, 32) : COLORS[winner % COLORS.length]);
for (int i = 0; i < n; i++) { // one box per team
float w = (width - 40) / (float) n, x = 20 + i * w;
fill(winner == i ? 255 : COLORS[i % COLORS.length], winner < 0 || winner == i ? 255 : 90);
rect(x + 8, 320, w - 16, 140, 16);
fill(winner == i ? 0 : 255); textSize(28);
text("team " + (i + 1), x + w / 2, 390);
textSize(12); text(i < boards.size() ? "board " + (i + 1) : "key " + (i + 1), x + w / 2, 430);
}
fill(255); textSize(winner < 0 ? 48 : 96);
text(winner < 0 ? "ready... buzz in!" : "team " + (winner + 1) + "!", width / 2, 160);
textSize(16);
text(winner < 0 ? boards.size() + " board(s) connected keys 1-4 buzz too" : "locked out R to reset (" + nf((millis() - wonAt) / 1000.0, 0, 1) + " s)", width / 2, 260);
}

void keyPressed() {
if (key == 'r') winner = -1;
if (key >= '1' && key <= '4') buzz(key - '1');
}

Morse code translator, mode 8 or mode 1​

Two directions. Type a phrase and the board beeps it as Morse in mode 8. Or tap it on a touch pad in mode 1 and the sketch decodes the dots and dashes. A Morse table: this gist.

CircuitPlaygroundMorseCode.pde
// Morse Code (2019 workshop project, firmware mode 8). Type a phrase and press Enter: the board beeps it in Morse
// through its speaker, while the screen shows the phrase, the code, and a blinking indicator (small white dot,
// long green dash) like the 2019 page. In mode 8 the board plays any Note On for 50 ms, so a dot is one Note On
// and a dash is Note Ons every 50 ms for three units. Needs MidiCore.pde + CircuitPlayground.pde.
// Keys: letters, digits and space type the phrase; Enter sends; Backspace edits; Escape is Processing's quit.
// Without a board the indicator still blinks, silently.
CircuitPlayground cpx;
final String ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
final String[] CODE = { ".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.",
"--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..", "-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----." };
final int UNIT = 70; // ms per Morse unit: dot 1, dash 3, gap 1, letter gap 3, word gap 7
String typed = "sos", phrase = "", morse = "";
ArrayList<int[]> steps = new ArrayList<int[]>(); // { start ms, length in units, kind } kind 0 gap, 1 dot, 2 dash, 3 letter end
int startAt = -1, lastBeep = 0, current = -1;

void setup() {
size(800, 400);
textAlign(CENTER, CENTER);
cpx = new CircuitPlayground(this);
cpx.connect();
}

void start() { // translate the phrase into timed steps
phrase = typed.toLowerCase().trim();
morse = "";
steps.clear();
int t = 0;
for (int i = 0; i < phrase.length(); i++) {
char c = phrase.charAt(i);
if (c == ' ') { t += 7; morse += " "; continue; }
int k = ALPHABET.indexOf(c);
if (k < 0) continue;
for (char s : CODE[k].toCharArray()) {
steps.add(new int[] { t * UNIT, s == '.' ? 1 : 3, s == '.' ? 1 : 2 });
t += (s == '.' ? 1 : 3) + 1;
}
morse += CODE[k] + " ";
t += 2;
}
steps.add(new int[] { t * UNIT, 1, 0 }); // a final silent step so the display knows it ended
startAt = millis();
}

void draw() {
int kind = 0;
if (startAt >= 0) {
int now = millis() - startAt;
current = -1;
for (int i = 0; i < steps.size(); i++) if (now >= steps.get(i)[0] && now < steps.get(i)[0] + steps.get(i)[1] * UNIT) { current = i; kind = steps.get(i)[2]; }
if (kind > 0 && millis() - lastBeep >= 50) { cpx.core.noteOn(1, 72, 100); lastBeep = millis(); } // keep the 50 ms beep going
if (now > steps.get(steps.size() - 1)[0] + UNIT) startAt = -1;
}
background(20, 24, 32);
fill(kind == 1 ? color(255) : kind == 2 ? color(60, 255, 120) : color(70, 30, 70)); // the indicator
circle(width / 2, 110, kind == 1 ? 40 : kind == 2 ? 130 : 70);
fill(240); textSize(28);
text(startAt >= 0 ? phrase : typed + "_", width / 2, 230);
fill(150); textSize(22);
text(startAt >= 0 ? morse : "type a phrase, press Enter", width / 2, 280);
textSize(14);
text((cpx.connected() ? "beeping on the board (mode 8)" : "no board: silent blink") + " unit " + UNIT + " ms", width / 2, height - 24);
}

void keyPressed() {
if (key == ENTER || key == RETURN) start();
else if (key == BACKSPACE) typed = typed.length() > 0 ? typed.substring(0, typed.length() - 1) : "";
else if (key != CODED && (ALPHABET.indexOf(Character.toLowerCase(key)) >= 0 || key == ' ')) typed += key;
}

Fruit piano, mode 1​

Mode 1, alligator clips and fruit (or anything conductive). Each pad is a note. The sketch plays it or draws it.

CircuitPlaygroundFruitPiano.pde
// Fruit Piano (2019 workshop project, firmware mode 1), and its Tactile Flashcards variant. Alligator clips run
// from the board's eight touch pads to fruit (or anything conductive); touching a fruit plays a tone on the
// computer and splashes a colour. Press F and every pad becomes a word instead: the flashcard mode, with L
// cycling the language. Needs MidiCore.pde + CircuitPlayground.pde. No board: keys 1-8 are the pads.
// Sound comes from Java's built-in synthesizer, so there is nothing to install.
import javax.sound.midi.*;

CircuitPlayground cpx;
Receiver synth;
final int[] NOTES = { 60, 62, 64, 65, 67, 69, 71, 72 }; // C major scale, one note per pad
final String[][] WORDS = { { "Apple", "Pomme", "Manzana" }, { "Lime", "Citron vert", "Lima" }, { "Lemon", "Citron", "Limón" },
{ "Orange", "Orange", "Naranja" }, { "Pear", "Poire", "Pera" }, { "Banana", "Banane", "Plátano" }, { "Grape", "Raisin", "Uva" }, { "Tomato", "Tomate", "Tomate" } };
final String[] LANGS = { "English", "Français", "Español" };
float[] splash = new float[8];
int lang = 0, lastPad = -1;
boolean flashcards = false;

void setup() {
size(800, 500);
colorMode(HSB, 360, 100, 100, 100);
textAlign(CENTER, CENTER);
cpx = new CircuitPlayground(this);
cpx.connect();
try { Synthesizer s = MidiSystem.getSynthesizer(); s.open(); synth = s.getReceiver(); }
catch (MidiUnavailableException e) { println("no built-in synth: " + e.getMessage()); }
}

void touchPressed(int pad) { // the helper calls these (keys 1-8 too, with no board)
splash[pad] = 1;
lastPad = pad;
tone(NOTES[pad], true);
}
void touchReleased(int pad) { tone(NOTES[pad], false); }

void tone(int note, boolean on) {
if (synth == null) return;
try { synth.send(new ShortMessage(on ? ShortMessage.NOTE_ON : ShortMessage.NOTE_OFF, 0, note, 100), -1); } catch (InvalidMidiDataException e) { }
}

void draw() {
background(0, 0, 12);
for (int i = 0; i < 8; i++) { // one circle per pad, pads 0..7 going round the board
float x = 100 + i * 85, hue = i * 45;
boolean on = cpx.touch(i);
if (splash[i] > 0) { fill(hue, 70, 100, splash[i] * 60); circle(x, 250, 80 + (1 - splash[i]) * 300); splash[i] -= 0.02; }
fill(hue, 80, on ? 100 : 45);
circle(x, 250, on ? 110 : 70);
fill(0, 0, 100); textSize(16);
text(flashcards ? WORDS[i][lang] : "pad " + (i + 1), x, 250);
}
if (flashcards && lastPad >= 0) { textSize(72); fill(lastPad * 45, 70, 100); text(WORDS[lastPad][lang], width / 2, 90); }
fill(0, 0, 70); textSize(14);
text((flashcards ? "flashcards: " + LANGS[lang] + " L next language" : "fruit piano") + " F toggle " + (cpx.connected() ? "touch the fruit" : "no board: keys 1-8"), width / 2, height - 24);
}

void keyPressed() {
if (key == 'f') flashcards = !flashcards;
if (key == 'l') lang = (lang + 1) % LANGS.length;
}

Thermometer, mode 4​

Mode 4 sends degrees Celsius once a second. A very local weather app. An hour of readings is a graph.

CircuitPlaygroundThermometer.pde
// Thermometer (2019 workshop project, firmware mode 4). The board sends its temperature in whole degrees
// Celsius once a second as CC 1 on channel 2; the helper exposes it as cpx.temperature. A big live reading
// and a 60-second history graph. Needs MidiCore.pde + CircuitPlayground.pde.
// Keys: F toggles Celsius / Fahrenheit. No board: up / down arrows move a fake temperature so the graph works.
CircuitPlayground cpx;
float[] history = new float[60]; // one reading per second, newest last
int samples = 0, lastSample = 0;
float fake = 22;
boolean fahrenheit = false;

void setup() {
size(800, 500);
textAlign(CENTER, CENTER);
cpx = new CircuitPlayground(this);
cpx.connect();
}

float reading() { return cpx.connected() ? cpx.temperature : fake; }
float shown(float c) { return fahrenheit ? c * 9 / 5 + 32 : c; }
boolean waiting() { return cpx.connected() && cpx.sensor < 0; } // connected, but no CC 1 yet (wrong mode?)

void draw() {
if (millis() - lastSample >= 1000 && !waiting()) { // once a second, push a sample
lastSample = millis();
for (int i = 1; i < 60; i++) history[i - 1] = history[i];
history[59] = reading();
samples = min(60, samples + 1);
}
background(20, 24, 32);
fill(240); textSize(120);
text(waiting() ? "..." : nf(shown(reading()), 0, 1) + (fahrenheit ? " °F" : " °C"), width / 2, 120);
textSize(14); fill(150);
text(waiting() ? "board connected, waiting for a reading: is it in mode 4?" : cpx.connected() ? "live from the board, one reading a second" : "no board: up / down arrows fake a temperature", width / 2, 210);
float lo = 1e9, hi = -1e9; // graph of the last minute, auto-scaled
for (int i = 60 - samples; i < 60; i++) { lo = min(lo, history[i]); hi = max(hi, history[i]); }
if (hi - lo < 2) { lo -= 1; hi += 1; }
stroke(60); line(60, 260, 60, 460); line(60, 460, 760, 460);
noFill(); stroke(255, 160, 60); strokeWeight(3);
beginShape();
for (int i = 60 - samples; i < 60; i++) vertex(map(i, 0, 59, 60, 760), map(history[i], lo, hi, 450, 270));
endShape();
strokeWeight(1); noStroke(); fill(150); textAlign(RIGHT, CENTER);
if (samples > 0) { text(nf(shown(hi), 0, 1), 55, 270); text(nf(shown(lo), 0, 1), 55, 450); }
textAlign(CENTER, CENTER);
text("last 60 seconds F: " + (fahrenheit ? "show Celsius" : "show Fahrenheit"), width / 2, 480);
}

void keyPressed() {
if (key == 'f') fahrenheit = !fahrenheit;
if (key == CODED && keyCode == UP) fake += 0.5;
if (key == CODED && keyCode == DOWN) fake -= 0.5;
}

Tactile flashcards, mode 1​

Same wiring as the fruit piano, different payoff. Instead of a note, touching an object shows you its name, and a key cycles the language. It lives inside the fruit piano sketch above: run it, press F, and every pad becomes a word. L switches language. The word list is at the top of the sketch, so swap in whatever is on the table.

Without the helper​

Channel 2 notes and CC 1 decoded inline, including the accelerometer burst. Under sixty lines, same stand-ins. Read the three bytes, no helpers first.