Connecting to p5.js via Serial
Bridge the Touch Board's touch events to a p5.js browser sketch using the Web Serial API for reactive visual experiences.
The idea
Your Touch Board detects physical touch. p5.js draws reactive visuals in the browser. Connect them and you have a physical controller driving a browser canvas — touch a painted wall mural and watch particles explode on a projected screen.
Architecture
Touch Board (Arduino/USB) ──serial──► Chrome (Web Serial API) ──JS──► p5.js canvas
The Touch Board sends touch events as plain text over USB serial. Chrome’s Web Serial API reads that text. p5.js updates its visuals based on the received data.
No server, no Node.js, no drivers needed — just Chrome and the Touch Board.
Part 1: Touch Board firmware
Upload this sketch — it sends touch and release events as simple text lines:
#include <MPR121.h>
#include <Wire.h>
void setup() {
Serial.begin(57600);
while (!Serial);
if (!MPR121.begin(0x5C)) {
Serial.println("ERR:MPR121");
while (1);
}
MPR121.setInterruptPin(4);
Serial.println("READY");
}
void loop() {
if (MPR121.touchStatusChanged()) {
MPR121.updateTouchData();
for (int i = 0; i < 12; i++) {
if (MPR121.isNewTouch(i)) {
Serial.print("T:");
Serial.println(i);
}
if (MPR121.isNewRelease(i)) {
Serial.print("R:");
Serial.println(i);
}
}
}
}
Output format: T:3\n = touch electrode 3, R:3\n = release electrode 3.
Part 2: p5.js sketch
// Note: Web Serial requires Chrome/Edge and HTTPS (or localhost)
let port, reader;
let touchStates = new Array(12).fill(false);
let particles = [];
// ── UI ──────────────────────────────────────────
function setup() {
createCanvas(windowWidth, windowHeight);
colorMode(HSB, 360, 100, 100, 100);
let btn = createButton('Connect Touch Board');
btn.style('position', 'fixed');
btn.style('top', '10px');
btn.style('left', '10px');
btn.style('z-index', '100');
btn.style('padding', '8px 16px');
btn.mousePressed(connectSerial);
}
// ── Serial ──────────────────────────────────────
async function connectSerial() {
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: 57600 });
let decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
reader = decoder.readable.getReader();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
let lines = buffer.split('\n');
buffer = lines.pop();
for (let line of lines) parseLine(line.trim());
}
} catch (err) {
console.error('Serial error:', err);
}
}
function parseLine(line) {
if (line.startsWith('T:')) {
let n = parseInt(line.slice(2));
if (!isNaN(n) && n >= 0 && n < 12) {
touchStates[n] = true;
onTouch(n);
}
} else if (line.startsWith('R:')) {
let n = parseInt(line.slice(2));
if (!isNaN(n) && n >= 0 && n < 12) {
touchStates[n] = false;
}
}
}
// ── Visuals ─────────────────────────────────────
function onTouch(electrode) {
let x = map(electrode, 0, 11, width * 0.1, width * 0.9);
let hue = map(electrode, 0, 11, 0, 320);
for (let i = 0; i < 40; i++) {
particles.push({
x,
y: height / 2,
vx: random(-4, 4),
vy: random(-8, -1),
life: 1.0,
hue
});
}
}
function draw() {
background(240, 20, 8, 25);
// Draw touch indicators
for (let i = 0; i < 12; i++) {
let x = map(i, 0, 11, width * 0.1, width * 0.9);
let hue = map(i, 0, 11, 0, 320);
fill(hue, touchStates[i] ? 90 : 30, touchStates[i] ? 100 : 40);
noStroke();
circle(x, height * 0.8, touchStates[i] ? 40 : 20);
}
// Update and draw particles
noStroke();
for (let p of particles) {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life -= 0.02;
fill(p.hue, 80, 95, p.life * 80);
circle(p.x, p.y, p.life * 14);
}
particles = particles.filter(p => p.life > 0);
}
Running the sketch
- Upload the firmware to the Touch Board
- Open the p5.js sketch in Chrome (localhost or GitHub Pages — not
file://) - Click Connect Touch Board, select the Touch Board’s port
- Touch electrodes — watch particles appear
Adding proximity-based visuals
Read the proximity/capacitance value continuously (not just on touch events) by sending raw data:
// Add to the Arduino loop, every 50ms:
static unsigned long lastSend = 0;
if (millis() - lastSend > 50) {
MPR121.updateAll();
Serial.print("D:");
for (int i = 0; i < 12; i++) {
int delta = MPR121.getBaselineData(i) - MPR121.getFilteredData(i);
Serial.print(constrain(delta, 0, 255));
if (i < 11) Serial.print(",");
}
Serial.println();
lastSend = millis();
}
Parse in p5.js:
} else if (line.startsWith('D:')) {
let vals = line.slice(2).split(',').map(Number);
proximityData = vals; // array of 12 values, 0 = no touch, 255 = firm touch
}
Key takeaways
- Web Serial API in Chrome connects the browser directly to the Touch Board over USB
- Touch Board sends
T:N/R:Nlines; p5.js parses them and updates visual state onTouch(electrode)is the hook for triggering visual events- The
connectSerial()async loop keeps reading indefinitely once connected - Send raw delta values at intervals for proximity/continuous control rather than just touch events
- Works on localhost or HTTPS — not on
file://URLs (browser security restriction)