Skip to content

Add Spiker:bit EMG support to the DataFlow tile [CLUE-567] - #2924

Open
scytacki wants to merge 20 commits into
masterfrom
CLUE-567-microbit-support
Open

Add Spiker:bit EMG support to the DataFlow tile [CLUE-567]#2924
scytacki wants to merge 20 commits into
masterfrom
CLUE-567-microbit-support

Conversation

@scytacki

Copy link
Copy Markdown
Member

Adds the Backyard Brains Spiker:bit (a micro:bit expansion board for biosignal recording) as a third hardware option in the DataFlow tile, covering EMG mode in this first increment:

  • EMG signal in → the existing EMG sensor node.
  • Servo output → the existing Servo output node drives a servo on the Spiker:bit.

End-to-end goal: a student plugs in a Spiker:bit (with a micro:bit seated in it), chooses "Spiker:bit" in the connect flow, and the tile connects over WebUSB, detects whether our fixed program is running, flashes it if not, and then streams EMG data in and servo angles out — with no manual MakeCode step.

What's included

  • SpikerbitDevice (src/models/stores/spikerbit-device.ts) — a WebUSB module that connects, detects the running program/version, flashes our firmware, and streams EMG in / servo out.
  • Firmware of record — MakeCode source plus the compiled spikerbit-clue.hex under src/plugins/dataflow/firmware/, computing the EMG envelope in firmware (v3). Webpack + Jest are configured to import .hex files as source strings, with a consistency test guarding the compiled artifact.
  • DataFlow UI — Spiker:bit offered as a connect option in the tile, with connection state surfaced via isConnected() across the topbar and connect button.
  • SerialDevice — transport-aware output so servo angles route correctly for the Spiker:bit; a detectSpikerbitVersion helper; a reusable parseArduinoSerialData helper; and tolerance for trailing whitespace when parsing serial lines.

The Spiker:bit is a distinct third scenario from the existing "arduino" and radio-hub "microbit" device families — it is neither, and the data path (connect → stream EMG → drive servo) is designed to stand on its own so the auto-flash step can be dropped cleanly if needed.

Out of scope for this increment

EEG/ECG modes, the potentiometer/fsr/tmp channels, a reproducible in-repo firmware build, and any change to the existing Arduino or radio-hub micro:bit flows.

Notes for reviewers

  • The large insertion count is almost entirely the compiled spikerbit-clue.hex firmware artifact (~17k lines).
  • The design doc lives on the branch under docs/superpowers/specs/ and is not part of this PR's committed diff.

🤖 Generated with Claude Code

scytacki and others added 15 commits July 17, 2026 17:21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-567]

Split into spikerbit-device.ts (no library import, tested with a fake
IMicrobitUsbConnection) and spikerbit-connection.ts (the sole static
importer of the ESM-only @microbit/microbit-connection library) so
Jest never has to load the library. Added tsconfig paths entries for
the library's usb/universal-hex exports subpaths, since this repo's
classic TS moduleResolution doesn't read package "exports" maps
(same workaround already used for @concord-consortium/codap-formulas).
…-567]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The .ts is MakeCode source (paste target for makecode.microbit.org); the
firmware dir is excluded from tsconfig/eslint since it uses MakeCode-only
globals. The compiled universal .hex is still authored/committed separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The micro:bit firmware pads each serial line with spaces before the \r\n
(e.g. "emg:57            \r\n"), so both parseArduinoSerialData and
detectSpikerbitVersion require [ \t]* before the line ending. The Arduino's
unpadded lines still match exactly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Universal micro:bit hex (V1+V2) built from spikerbit-clue-v1.ts. Bench-verified:
EMG streams into the sensor node, the Servo output drives the servo, and the
version query is detected (no reflash on reconnect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…E-567]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v2 MakeCode source disables writeLine's 32-byte space padding and moves
the "?" version reply onto the forever-loop fiber so it can't interleave with
the emg stream. Re-export the compiled hex and bump kSpikerbitFirmwareVersion
to 2 to match.

Rename the firmware artifacts to spikerbit-clue.{ts,hex} (the -v1 suffix was
redundant with the versioned banner) and update the README and import.

Add a Jest consistency test that guards against the source VERSION,
kSpikerbitFirmwareVersion, and the committed hex drifting out of sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pxt-spikerbit extension's musclePowerSignal() subtracts a fixed 580 noise
floor and half-wave rectifies, but the board's resting baseline is ~511, so
moderate contractions never clear the floor and read as ~0 — only hard,
rail-to-rail flexes register. Recorded raw-vs-envelope data on a micro:bit V2
confirmed this.

v3 computes the envelope itself: track the DC baseline and full-wave rectify
around it (envelope = EMA(|raw - baseline|)). The EMA coefficients are derived
from the measured per-sample dt so the smoothing time constant is fixed
regardless of sample rate. We no longer call the extension at all — its
startMuscleRecording() only drove P8=0/P9=0 to select EMG mode, which we now do
ourselves, reading the raw signal on P1. Servo confirmed on P0. Bench-validated
on hardware: tracks moderate flexes the old envelope missed.

Bump kSpikerbitFirmwareVersion to 3 and re-export the hex. Add the two MakeCode
bench-test programs used to diagnose and validate this, and update the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cypress

cypress Bot commented Jul 22, 2026

Copy link
Copy Markdown

collaborative-learning    Run #19498

Run Properties:  status check passed Passed #19498  •  git commit 2f72f43308: Tighten Spiker:bit connection typing and record review follow-ups [CLUE-567]
Project collaborative-learning
Branch Review CLUE-567-microbit-support
Run status status check passed Passed #19498
Run duration 03m 23s
Commit git commit 2f72f43308: Tighten Spiker:bit connection typing and record review follow-ups [CLUE-567]
Committer Scott Cytacki
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 4
View all changes introduced in this branch ↗︎

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.25000% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.14%. Comparing base (96a5a0d) to head (2f72f43).
⚠️ Report is 153 commits behind head on master.

Files with missing lines Patch % Lines
...c/plugins/dataflow/components/dataflow-program.tsx 35.71% 9 Missing ⚠️
...w/components/ui/dataflow-serial-connect-button.tsx 42.85% 8 Missing ⚠️
src/models/stores/serial.ts 87.50% 3 Missing ⚠️
src/models/stores/spikerbit-connection.ts 66.66% 2 Missing ⚠️
src/models/stores/serial-protocol.ts 95.45% 1 Missing ⚠️
src/plugins/dataflow/nodes/rete-manager.tsx 66.66% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (96a5a0d) and HEAD (2f72f43). Click for more details.

HEAD has 36 uploads less than BASE
Flag BASE (96a5a0d) HEAD (2f72f43)
cypress-regression 15 0
cypress 21 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #2924       +/-   ##
===========================================
- Coverage   86.10%   69.14%   -16.96%     
===========================================
  Files         933      931        -2     
  Lines       53246    53320       +74     
  Branches    14076    14087       +11     
===========================================
- Hits        45847    36870     -8977     
- Misses       7383    16418     +9035     
- Partials       16       32       +16     
Flag Coverage Δ
cypress ?
cypress-regression ?
cypress-smoke 41.91% <14.52%> (-0.05%) ⬇️
jest 55.43% <74.21%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

scytacki and others added 3 commits July 22, 2026 17:38
Add a hardware-reference section to the firmware README from the Backyard Brains
schematic: the micro:bit pin usage (P0 servo, P1 EMG via JP2, P2 free, P8/P9
mode-select), the servo connector wiring, and the 2xAA → VIN → PAM2401 boost
power chain. Records why battery voltage can't be read off any rail (all
regulated, including the servo's +5V V+) and how to monitor it instead — a
divider from raw VIN into the free P2 analog pin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the spikerbit-clue-v1.{ts,hex} → spikerbit-clue.{ts,hex} rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
connectAndStream previously flashed only when the version query got no reply, so
a board running an older firmware version was left as-is. Now it also flashes
when the reported version is below kSpikerbitFirmwareVersion, so connecting a
board running old firmware auto-updates it.

Update the device tests: the fake connection now reports a configurable version,
the up-to-date cases report the current version, and a new test covers an
out-of-date board triggering a flash. Update the design spec (detection step and
the former "no version-based re-flash" limitation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a third DataFlow hardware option for Backyard Brains Spiker:bit (micro:bit expansion board) using a new WebUSB + auto-flash connection path, while reusing the existing Arduino-style emg:<value>\r\n serial protocol so the existing EMG Sensor + Servo nodes can work unchanged.

Changes:

  • Add a WebUSB-based Spiker:bit connection/flash/streaming module (SpikerbitDevice) and a thin wrapper that isolates the ESM-only @microbit/microbit-connection dependency.
  • Bundle and validate a “firmware of record” (spikerbit-clue.ts + compiled .hex) plus Jest tests to keep firmware artifacts consistent with the app’s expected version.
  • Update DataFlow UI/connect flow and serial store to support a second “transport” (Web Serial vs Spiker:bit WebUSB) while keeping the existing node/protocol layer.

Reviewed changes

Copilot reviewed 21 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
webpack.config.js Adds webpack handling to import .hex as source text for flashing.
tsconfig.json Adds path aliases for @microbit/microbit-connection/* ESM subpaths and excludes firmware sources from TS checks.
.eslintrc.js Excludes MakeCode firmware sources from ESLint.
package.json Adds @microbit/microbit-connection dependency and extends Jest file mocks to include .hex.
package-lock.json Locks the new micro:bit connection dependency tree.
src/plugins/dataflow/nodes/rete-manager.tsx Switches “connected” checks from hasPort() to transport-agnostic isConnected().
src/plugins/dataflow/components/ui/dataflow-serial-connect-button.tsx Replaces the single “refresh serial” button with a device-choice menu (Arduino / micro:bit hub / Spiker:bit).
src/plugins/dataflow/components/ui/dataflow-program-topbar.tsx Plumbs the new onConnectDevice() handler into the topbar.
src/plugins/dataflow/components/ui/dataflow-program-topbar.scss Styles the new device-choice dropdown menu.
src/plugins/dataflow/components/dataflow-program.tsx Adds Spiker:bit connect flow via dynamic import and flashes the bundled .hex.
src/models/stores/serial.ts Extracts Arduino parsing to a helper and adds Spiker:bit transport routing (writeLine() via callback).
src/models/stores/serial-protocol.ts Adds shared helpers for parsing Arduino-style serial data and detecting Spiker:bit version banners.
src/models/stores/serial-protocol.test.ts Unit tests for the extracted parsing/version-detection helpers.
src/models/stores/spikerbit-device.ts New WebUSB Spiker:bit device module: connect → detect version → flash → stream → route servo writes.
src/models/stores/spikerbit-device.test.ts Unit tests for Spiker:bit detect/flash/routing/disconnect behavior using a fake connection.
src/models/stores/spikerbit-connection.ts Single static-import entry point for the ESM-only micro:bit library (intended for dynamic import by UI).
src/models/stores/spikerbit-firmware-consistency.test.ts Jest consistency checks to keep MakeCode source, compiled hex, and app version constant in sync.
src/models/stores/serial-transport.test.ts Unit tests for SerialDevice’s new transport routing behavior.
src/plugins/dataflow/firmware/README.md Documents the firmware artifacts, protocol, and manual rebuild steps.
src/plugins/dataflow/firmware/spikerbit-clue.ts MakeCode “source of record” for the Spiker:bit firmware (v3).
src/plugins/dataflow/firmware/spikerbit-emg-test.ts Bench-only diagnostic firmware (not flashed by CLUE).
src/plugins/dataflow/firmware/spikerbit-envelope-test.ts Bench-only diagnostic firmware for sample rate + envelope validation (not flashed by CLUE).
src/plugins/dataflow/firmware/spikerbit-clue.hex Compiled universal hex artifact used for auto-flashing (large).
src/hex.d.ts Adds a TS module declaration for importing .hex files.
docs/superpowers/specs/2026-07-17-spikerbit-emg-dataflow-design.md Adds a design/spec document for the Spiker:bit DataFlow integration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +62
const running = await this.queryVersion();
if (running == null) {
// USB connection stays Connected through a flash (DAPLink interface chip is
// untouched) and serial auto-reinitialises, so no explicit reconnect here.
await this.connection.flash(flashDataSource, { partial: true, progress });
await this.queryVersion();
}
Comment on lines +234 to +236
public isConnected(){
return this.hasPort() || this.spikerbitConnected === true;
}
// routes to it instead of the Web Serial writer. This single-store reuse is a
// deliberate MVP shortcut; see docs/superpowers/specs/2026-07-17-spikerbit-emg-dataflow-design.md §10.
spikerbitWrite: ((line: string) => void) | undefined;
private spikerbitConnected: boolean;
Comment on lines +47 to +50
const chooseDevice = (deviceType: "serial" | "spikerbit") => {
setMenuOpen(false);
onConnectDevice(deviceType);
};
]

@microbit/microbit-connection ships a CommonJS build (resolved via its package
exports "require" condition), so a plain static import works in both Jest 30 and
webpack — the ESM/dynamic-import rationale was incorrect. Import it statically
instead of via dynamic import(); this moves ~51 KB solely into the already-lazy
Dataflow webpack chunk (+2.7%) with nothing added to initial load, and keeps
spikerbit-connection.ts as a thin typed wrapper.

Also from the maintainability review:
- rename SerialDevice.hasPort() to hasWebSerialPort() so callers don't confuse it
  with the any-transport isConnected() (the Spiker:bit uses WebUSB, no port).
- document that the firmware-consistency test only checks the embedded version
  string, not that the hex was compiled from the source (test header + README).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…UE-567]

- Replace the `as unknown as IMicrobitUsbConnection` cast in createSpikerbitConnection
  with a plain typed return; the library's MicrobitUSBConnection is structurally
  assignable to our interface, so the compiler now catches API drift instead of the
  cast silently erasing it. Correct the interface's comment (the library isn't ESM-only).
- Initialize SerialDevice.spikerbitConnected in the constructor and drop the now-
  redundant `=== true` guard in isConnected().
- Record two known limitations in the design spec's future-work section: the half-wired
  connect menu (duplicate options, no click-outside dismissal) and the missing listener
  cleanup / in-flight connect guard in the Spiker:bit connect path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants