Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions software/p5js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# StringField p5.js Gesture Debugger

This sketch is a loud, opinionated teaching demo: *data in → motion out*. It
turns StringField gesture JSON into giant state labels, velocity vectors, and a
noisy “projector” layer so the room can see the signal breathe.

If you’ve got hardware, this is your oscilloscope-with-attitude. If you don’t,
smash the keyboard shortcuts and still get the full show.

## Browser + hardware requirements

WebSerial is the gatekeeper. That means **Chrome, Edge, or any Chromium-based
browser**. Firefox and Safari still don’t speak WebSerial (as of today), so the
button will politely sulk.

You also need:

- A secure context: `localhost` or `https://` (no `file://` — browsers hate
that).
- A device that emits JSON lines over serial at **115200 baud**.

## Connect to the device (exact steps)

1. **Serve the sketch** from a local web server.
- Example: from the repo root run `python3 -m http.server 8000`, then visit
`http://localhost:8000/software/p5js/`.
2. **Open the sketch** in Chrome/Edge.
3. **Click “Connect Serial.”**
4. **Choose your device** in the browser’s port picker.
5. **Watch the status readout** flip to `serial: connected` and the visuals come
alive.

To disconnect, click the same button again (or yank the cable — the sketch will
announce the drama).

## No hardware? Still party.

Keyboard shortcuts are *intentionally* kept alive for classrooms and demos:

- `P` → pluck
- `S` → scrape
- `B` → bow
- `R` → release
- `H` → harmonic
- `M` → mute
- `T` → tremolo
- `V` → vibrato

These trigger the same rendering pipeline as real serial messages, so your
teaching flow doesn’t depend on cables behaving.
159 changes: 158 additions & 1 deletion software/p5js/sketch.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ let gestureState = 'idle';
let velocity = 0;
let stateHistory = [];
let noiseLayer;
let serialPort;
let serialReader;
let serialBuffer = '';
let serialStatus = 'serial: idle';
let serialStatusSpan;
let serialButton;
let serialHud;

// -- Lifecycle -----------------------------------------------------------------
// setup() runs once when the sketch boots. Treat it like lab warm-up: lay out
Expand All @@ -16,7 +23,10 @@ function setup() {
textFont('monospace');
noiseLayer = createGraphics(width, height);
noiseLayer.clear();
// TODO: hook up navigator.serial or WebMIDI; call ingestSerialLine(line) per message.
// WebSerial hookup: minimal overlay + reader loop. The UI stays visible so
// you can teach the "click → pick port → data flows" story in real time.
setupSerialHud();
setupSerialListeners();
}

// draw() fires 60 times per second. Every call repaints the entire scene so the
Expand Down Expand Up @@ -125,6 +135,153 @@ function registerState(state, vel) {
}
}

// ---------------------------------------------------------------------------
// WebSerial helpers. These are intentionally chatty so the sketch doubles as a
// teaching tool about how browser serial works. Punk-rock transparency > magic.

function setupSerialHud() {
serialHud = createDiv();
serialHud.style('position', 'absolute');
serialHud.style('top', '16px');
serialHud.style('right', '16px');
serialHud.style('padding', '12px 16px');
serialHud.style('background', 'rgba(5, 5, 20, 0.72)');
serialHud.style('border', '1px solid rgba(255, 255, 255, 0.2)');
serialHud.style('border-radius', '10px');
serialHud.style('color', '#f5f1ff');
serialHud.style('font-family', 'monospace');
serialHud.style('font-size', '14px');
serialHud.style('backdrop-filter', 'blur(4px)');
serialHud.style('z-index', '10');

serialButton = createButton('Connect Serial');
serialButton.parent(serialHud);
serialButton.style('margin-right', '12px');
serialButton.style('padding', '6px 12px');
serialButton.style('border', '1px solid rgba(255, 255, 255, 0.4)');
serialButton.style('background', 'rgba(255, 90, 90, 0.2)');
serialButton.style('color', '#fff');
serialButton.style('cursor', 'pointer');
serialButton.mousePressed(toggleSerialConnection);

serialStatusSpan = createSpan(serialStatus);
serialStatusSpan.parent(serialHud);

if (!('serial' in navigator)) {
serialStatus = 'serial: unsupported (use Chrome/Edge)';
updateSerialStatus();
serialButton.attribute('disabled', true);
serialButton.style('opacity', '0.4');
serialButton.style('cursor', 'not-allowed');
}
}

function setupSerialListeners() {
if (!('serial' in navigator)) return;
navigator.serial.addEventListener('disconnect', (event) => {
if (event.target === serialPort) {
disconnectSerial('serial: cable yanked');
}
});
}

function updateSerialStatus() {
if (serialStatusSpan) {
serialStatusSpan.html(serialStatus);
}
}

async function toggleSerialConnection() {
if (serialPort) {
await disconnectSerial('serial: disconnected');
return;
}
await connectSerial();
}

async function connectSerial() {
if (!('serial' in navigator)) return;
try {
serialStatus = 'serial: requesting port...';
updateSerialStatus();
serialPort = await navigator.serial.requestPort();
await serialPort.open({ baudRate: 115200 });
serialStatus = 'serial: connected';
updateSerialStatus();
serialButton.html('Disconnect Serial');
startSerialReadLoop();
} catch (err) {
serialStatus = 'serial: connection cancelled';
updateSerialStatus();
serialPort = null;
}
}

async function disconnectSerial(reason) {
serialStatus = reason || 'serial: disconnected';
updateSerialStatus();
if (serialReader) {
try {
await serialReader.cancel();
} catch (err) {
// ignore: cancelling a closed reader is fine
}
serialReader.releaseLock();
serialReader = null;
}
if (serialPort) {
try {
await serialPort.close();
} catch (err) {
// ignore: unplugged or already closed
}
serialPort = null;
}
serialBuffer = '';
if (serialButton) {
serialButton.html('Connect Serial');
}
}

async function startSerialReadLoop() {
if (!serialPort?.readable) {
serialStatus = 'serial: no readable stream';
updateSerialStatus();
return;
}
const decoder = new TextDecoderStream();
const readableClosed = serialPort.readable.pipeTo(decoder.writable);
serialReader = decoder.readable.getReader();

try {
while (true) {
const { value, done } = await serialReader.read();
if (done) break;
serialBuffer += value;
const lines = serialBuffer.split('\n');
serialBuffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.length > 0) {
ingestSerialLine(trimmed);
}
}
}
} catch (err) {
serialStatus = 'serial: read error';
updateSerialStatus();
} finally {
try {
await readableClosed;
} catch (err) {
// ignore pipe closure errors; we already handled status updates
}
if (serialPort) {
disconnectSerial('serial: stream closed');
}
}
}

// Called whenever WebSerial/WebMIDI hands us a full line of JSON. We parse it,
// shrug off errors (bad packets shouldn't crash the workshop), and hand the
// result to the handler below.
Expand Down