Repository navigation
CLUE-728: Dataflow speaks the CC Device Protocol to the Spiker:bit - #3055
Conversation
The wire formats were spread across serial.md, arduino.md and hub.md, and nothing described them as a whole. The new CC Device Protocol is designed against this record of what the current protocols do and don't do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pure line handling that the session and firmware detection build on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ash it Firmware answering ? with a hello line must be told apart from the legacy banner, and a protocol Dataflow doesn't speak must be reflashed rather than trusted on its firmware number alone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Spiker:bit now uses the ccDevice protocol: SerialDevice opens a session when it becomes active, routes inbound data to it, writes servo as servo:<angle>, and Disconnect ends the session before closing the transport. A device that lists its channels satisfies only those, so the Spiker:bit no longer claims the Arduino's fsr/tmp/a1 channels. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ted MakeCode runtime The MakeCode simulator can't receive serial input and no emulator runs a V2 hex, so the firmware's protocol logic had no automated tests. MakeCodeSim transpiles spikerbit-clue.ts and runs it against fake MakeCode globals on a simulated clock, so tests can drive serial input, buttons and the analog pin and check what the firmware writes, shows and plays. It is proven against the current v3 firmware before the protocol rewrite. The firmware header now states the loop rule the simulator depends on (pause only as a loop's last statement), and the hex is rebuilt because it embeds the source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the firmware sim MakeCode's forever loop sleeps 20 ms after every pass, not only after a body that doesn't pause, so the sim's forever passes were 20 ms too short. And the board's serial receive buffer holds 20 characters unless the program raises it with serial.setRxBufferSize; the sim buffered without limit, so firmware that never raised it would pass in Jest and truncate protocol commands on the board. The sim now drops characters that arrive while the buffer is full, as codal-core does, and fakes setRxBufferSize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…clearing The old firmware compares the whole line to "?", so a checksummed one got no banner and the checks then ran against firmware that treats any line as a servo angle. Clearing the receive buffer mid-line left a fragment that a check could take for a reply. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Spiker:bit firmware now answers ? with a hello line instead of the legacy banner, stays silent until a host opens a session or asks it to stream, and checks and adds a checksum on every line. Sessions keep the board streaming only while Dataflow keeps pinging, so a closed tab no longer leaves it filling the USB buffer with stale readings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r the board to finish Copying the hex with copyFile left a "._" file on the drive, which made DAPLink's transfer fail, and the wait needed to catch the drive and port disappearing between polls. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The LED display now shows how far a connection got (waiting, found, connected, streaming), so a stuck connection can be diagnosed on the board. identify shows an icon and plays a sound on the built-in speaker, with the P0 audio output off so the servo doesn't twitch; a V1 has no speaker and only shows the icon, keeping the hex universal. A long press of A starts and stops free-run outside a session, for plotters, and does nothing during one. The firmware sim now raises the button events CODAL does and panics like a V1 on speaker calls. serial-protocols.md documents the CC Device Protocol, and serial.md and the firmware README no longer describe the Spiker:bit as speaking keyValue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File calls on the DAPLink drive can block in the kernel while the board reprograms, hanging the whole process. The copy is now a child process that is abandoned if stuck, and readiness is judged by the board answering ? rather than the drive or port appearing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Firmware numbers are per device, so a board on the same protocol with a higher number must not be accepted as a Spiker:bit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every committed firmware answers identify, so the --expect-identify flag only let a regression pass. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A close write that never settles left Disconnect hanging and kept Connect and Reconnect blocked by busyChoice. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An angle of 90 maps to itself, so those tests could not catch a missing scale. Use 0, which maps to 30. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The protocol doc said it fills a gap that includes capability discovery, which is only reserved. One test mixed the protocol-tag rule with the channel-list rule. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n MakeCode The firmware is silent until it's spoken to, so trying it on a board needs a terminal or a long press of button A. serialterminal.com sends whole lines and runs in any Chrome or Edge, and MakeCode's plotter button only appears once readings arrive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CC Device Protocol Records the protocol design (framing, sessions, acknowledgments, the compatibility rules and what is reserved), the firmware behavior, and the Dataflow side, as agreed while designing this story. serial-protocols.md points here for the reasoning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## CLUE-728-spikerbit-status #3055 +/- ##
=============================================================
+ Coverage 87.65% 87.68% +0.02%
=============================================================
Files 1084 1085 +1
Lines 62318 62501 +183
Branches 16669 16714 +45
=============================================================
+ Hits 54625 54804 +179
- Misses 7672 7676 +4
Partials 21 21
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
collaborative-learning
|
||||||||||||||||||||||||||||
| Project |
collaborative-learning
|
| Branch Review |
CLUE-728-spikerbit-status
|
| Run status |
|
| Run duration | 03m 54s |
| Commit |
|
| Committer | Scott Cytacki |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
4
|
| View all changes introduced in this branch ↗︎ | |
src/plugins/dataflow-tool only exists to keep the ccemgfsr.ino URL that printed materials and the brain and vibe teacher guides link to. The serial docs this branch changes, and the micro:bit and diagram files they refer to, now live with the plugin in src/plugins/dataflow/docs. The Arduino folder stays where it is. The firmware header names the new path, so the hex is rebuilt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tale or repeated endSession's close condition, queryFirmware's backlog story (now in the docs), the flash list (shouldFlash decides), shouldFlash's Dataflow wording, the channel-list rule (channelSatisfiedBy holds it), the backlog range and a few test comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
queryFirmware's reset-and-query, the another-device flash row, CcDeviceSession and where lastHeartbeatAt lives, the settled builder's calls, the hidden-tab outcome, and #3055's tests and docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…LUE-728-spikerbit-firmware-status
Device lines now need a checksum, so the tests send them as the board does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ware 4 does The fake answered with the v1–v3 banner, so every Spiker:bit connect flashed and never opened a session. It now answers !! with an empty line and its ok, ? with the hello line, and open with its ok; the disconnect test checks that nothing was flashed and that the session was opened and closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…device lines need a checksum Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougmartin
left a comment
There was a problem hiding this comment.
Looks good 👍
Reviewed at a25dc69. All the related suites pass locally. A v4 board is identified without a flash, a v3 banner or a missing reply still flashes, firmware 5 connects without one, and the Arduino and radio-hub paths behave as before. Copilot's three fixes in 394c316 each fail a test when reverted. I checked user-guide.md: its edits are outside the generated sections, and all ten example checksums in it and in cc-device-protocol.md are correct. device-connection.test.ts's fake now answers as firmware 4 does, and its test checks no flash and a session opened and closed.
A few non-blocking notes (take or leave). The last two are PR-body updates I'd ask for before merge:
src/models/stores/serial-cc-device.test.ts(lines 107-111), withsrc/models/stores/serial-transport.test.ts(line 32): the body says servo scaling is tested "for both protocols, tested at n=0", but the Arduino tests use n=90, the scaling's fixed point. Changingserial.tsline 190 tothis.writeLine(Math.round(n).toString())leaves all 1019 tests insrc/models/storesandsrc/plugins/dataflowpassing. CallingwriteToOutForServo(0, "Servo")for the Arduino and expecting["30"]would guard it.src/models/stores/cc-device-session.ts(lines 5-9, 39-40), withsrc/models/stores/cc-device-session.test.ts(lines 38-54, 97-105): no caller or test passespingIntervalMsoropenRetryMs. The tests check only one side of each threshold, soopenRetryMs ?? 1600andpingIntervalMs ?? 950both pass every suite. Meanwhileserial.md(lines 214, 216) anduser-guide.md(lines 258-259) state 2 s and 1 s. Suggest replacing the two options with exportedkOpenRetryMs = 2000andkPingIntervalMs = 1000, and asserting both sides: oneopenat 1999 ms and two at 2000 ms, no ping at 999 ms after theokand one at 1000 ms.randomSessionIdis used by the tests and stays.src/plugins/dataflow/docs/serial.md(lines 119-120, 401-421): "auto-disconnect on unplug" works only because one of the session's writes fails. CLUE never callsinitialize(), so the library'snavigator.usblistener is off, and a failed serial read only firesbackgrounderror. The path is: aping(every 1 s), anopenretry or a servo write fails, the library'swithEnrichedErrorscallsdisconnect(true)and setsDisconnected, andMicrobitUsbTransport's status listener (spikerbit-device.tslines 67-69) clears the device. So aftererr othersessionstops the pings, an unplug goes unnoticed unless a program is writing to the servo.serial.mdlines 119-120 still say "it doesn't watch for the board being unplugged", and "Connection status tracking" doesn't mention the Spiker:bit. Worth a sentence in both. A test can only imitate the library's half with a fake whoseserialWriterejects and emitsDisconnected, so docs matter more here than a test.src/models/stores/spikerbit-device.ts(lines 72-77, 203): thewritecomment says "A failure is logged, not thrown", but the library'sserialWriteisn'tasync. ItsassertConnected(connection.jslines 516-517) throws synchronously when the device is gone, before any promise exists. Nothing reaches that today. If something did, though, thequeryFirmwareretry at line 203 would throw out of thesetTimeoutcallback, the poll would never reschedule, the promise would never resolve, andbusyChoicewould stay set. The synchronoustry/catchinCcDeviceSession.send(cc-device-session.tslines 170-177) is the only guard on the session's writes, and removing it passes every suite. MakingwriteandwriteResetAndQueryasyncturns every failure into a rejection, makes the comment true, and changes nothing for callers, sinceIDeviceTransport.writealready allows a promise.src/models/stores/spikerbit-device.test.ts(lines 140-151): the legacy-banner test passes without legacy detection, because a missing reply flashes too. With the legacy branch atserial-protocol.tsline 69 disabled, onlyserial-protocol.test.tsline 119 fails. Asserting with fake timers thatflashis called well beforekFirmwareQueryTimeoutMswould make it test the banner. Low stakes: the only difference is flashing at once rather than after 1.5 s.src/models/stores/serial-protocol.ts(lines 9-12, 23-24),src/models/stores/serial-protocol.test.ts(lines 72-73) andsrc/plugins/dataflow/model/utilities/channel.ts(lines 40-42): comments still describe the Spiker:bit as akeyValuedevice.parseKeyValueData's header justifies[ \t]*by Spiker:bit v1/v2 padding, and its lines 23-24 say "a Spiker:bit never emits fsr/a1". The test says "A Spiker:bit satisfies fsr by protocol".channel.tssays fsr/a1 on a Spiker:bit time out tomissingand thatparseKeyValueDatarefreshes it. None of that holds now that the Spiker:bit has a channel list andCcDeviceSession. Make them Arduino-only or drop the Spiker:bit examples.docs/superpowers/specs/2026-10-20-clue-728-spikerbit-firmware-protocol-design.md(lines 98-100, 502, 506): "a line without one is accepted" and "Check the*hhchecksum when present" contradict the code (cc-device-protocol.tslines 46-47) andcc-device-protocol.md(lines 63-64), which drop a non-empty device line that has no checksum. Line 506's "answering the latestopen" doesn't match the code either, which accepts theokto any pendingopen(fine, since every retry carries the same id). This text dates from #3056, but this PR rewrites that section.src/models/stores/spikerbit-device.ts(lines 35-39, 112-113, 125-128): comment trims. Lines 35-39 could be one line (e.g. "Above CcDeviceSession's 1..kMaxSeq, so a late reply to a connect-time!!can't match a session command."). Lines 112-113 repeat what lines 167-169 say. Lines 125-128 narrate whatsetActiveDevicedoes.src/models/stores/spikerbit-sim/spikerbit-end-to-end.test.ts(lines 150-155): "moves the servo from a Live Output" callsserialDevice.writeToOutForServodirectly. Rename it (e.g. "moves the servo for a servo output"), or drive it throughLiveOutputNode.sendDataToSerialDevice.- PR body, "For later": it covers hidden-tab throttling, the first heartbeat, the session state not being observable, and the UI needing its own timer. Three things from the #3056 discussion are missing: using
document.visibilityStateto tell a hidden tab from a stopped board; that the library never emitsPaused, because CLUE doesn't callinitialize(); and that unplug detection depends on the session's writes (theserial.mdnote above). - PR body, hardware and bundle sections: the hardware list has only a V2 Spiker:bit. It names no V1, no commit, and which firmware "auto-flash from other firmware" started from, and records no Arduino or radio-hub retest, although
device-capabilities.ts,live-output-node.tsandDeviceConnection.disconnectchanged for every device. Separately, "Where the device code lives" says all of this device code ships in the main bundle. By the import graph,spikerbit-device.ts,spikerbit-connection.ts, the library and the hex are imported only fromsrc/plugins/dataflow/model/device-connection.ts, so they stay in the Dataflow chunk. What newly reaches the main bundle iscc-device-session.tsandcc-device-protocol.ts, viastores.ts,serial.tsandserial-protocol.ts. I traced this from the imports and didn't run a webpack build.
Questions
- Chromebook: is the body's "connecting works sometimes but not always" from a retest on this branch's head with the
!!connect, as you planned after #3056? If so, is it the same as on #3040 (works once, then stops) or different, and does it flash when it fails? - Your 3-of-3 Node runs: had the board been unplugged since the old code's reflashes? The library fills the interface chip's buffers only inside
flash()(cdcSaturated), and they stay full until an unplug, so if it wasn't unplugged, those runs don't cover Kirk's case from #2929. Does the first Connect in the browser, on a freshly plugged-in board already running v4, skip the flash? - Besides the V2, was this branch tried on a V1 Spiker:bit (connect, auto-flash, EMG), an Arduino, and the radio hub? And which firmware was "auto-flash from other firmware" run from: the v3 banner, a MakeCode program (no reply, so the 1.5 s timeout), or something else?
…h checks The review of #3055 asked for them at its head; they run here because this branch contains that code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the value At 90 the scaling leaves the value unchanged, so writing the unscaled value passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and test both sides of each Nothing passed the two options, and the tests checked only one side of each interval, so other values passed while the docs state 1 s and 2 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is logged, not thrown The library's serialWrite throws before returning a promise once the board is gone. A throw from the connect-time retry would have escaped its timer and left the connect attempt waiting forever. Also tests the session's own guard on a throwing write, which nothing exercised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not after the timeout A board that doesn't answer is flashed too, so the test passed without legacy detection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ents It speaks the CC Device Protocol now, so the keyValue parser, its test and the EMG channel's comment are about the Arduino. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One shortened, one that repeated the firmware handler's comment, and one that narrated what setActiveDevice does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It calls writeToOutForServo directly; the Live Output node's own test covers the node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only by a failed write, since CLUE doesn't call the library's initialize(); after err othersession stops the pings, an unplug goes unnoticed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host drops a non-empty device line without a checksum, and the session accepts an ok to any pending open, since every re-sent open carries the same id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, Doug. I took all of your notes. Here's where each went.
Your questions:
|
Notes from running the hand checks and the #3055 retests on a micro:bit V2 and V1 on a Spiker:bit, and an Arduino, at the branch preview. Adds the Web Serial terminal check, which shows not responding and recovery; holding the reset button turned out to need Reconnect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLUE-728, part 2 of 3: Dataflow speaks the CC Device Protocol to the Spiker:bit. It builds on #3056, which added the protocol and firmware v4 and is merged into the feature branch
CLUE-728-spikerbit-status. The story's UI half (the "connection stopped" display and the "Is it alive?" button) comes in a later PR into the same feature branch.The protocol and the reasoning behind it are in the spec ("Dataflow protocol code") and
cc-device-protocol.md.serial.mdis updated to describe the new flow, including a new section on Dataflow's side of the protocol session, and its diagrams are re-exported.What changes
ccDeviceprotocol type for the Spiker:bit. The Arduino and the radio hub stay onkeyValueandradioHub. Device capabilities can now list the channels a device provides, so the Spiker:bit satisfies onlyemg:fsr,tmpanda1show as missing right away instead of after the 7 s freshness timeout.CcDeviceSession(cc-device-session.ts), the host side of a session:open, then pings every second.err nosessionand stops onerr othersession.servo:<angle>.lastHeartbeatAtfor the UI half.SerialDevicecreates one when accDevicedevice becomes active and stops it on clear, unplug or reconnect.!!(spikerbit-device.ts). Right after connecting, Dataflow writes!! #nand a bare?in one write, so the board is reset to idle and then identified. CLUE-728: CC Device Protocol and Spiker:bit firmware v4 #3056 explains the backlog in the interface chip that makes this necessary: with one?, a board left streaming answered with ahelloline glued to a cut-off reading, and Dataflow reflashed a current board.!!and?once more. The overall limit stays 1.5 s, and the re-query after a flash uses the same sequence.#nnumbers start at 1000, outside the session's 1–999, so a late reply to a connect-time!!can't be taken for a reply to the session's own commands.serial-protocol.ts,spikerbit-device.ts). It flashes when:CLUE-SPIKERBIT v<N>banner;hellonames another device or a protocol version it doesn't speak;closefirst, bounded by a 500 ms timeout so a stuck write can't wedge Connect, Reconnect or Disconnect.servo:<angle>with the same scaling. The gripper output stays Arduino-only.Where the device code lives
This PR adds
cc-device-session.tstosrc/models/storesand extendsserial.tsandspikerbit-device.tsthere. These files sit insrc/models/storesbecause the existing serial and Spiker:bit device code already lives there (serial.ts,spikerbit-device.ts,serial-protocol.ts, the transports), and the app-wide store creates the oneSerialDeviceat startup. That store already imports Dataflow code statically (serial.tsimports Dataflow'snode.tsanddevice-capabilities.ts), against the rule that tile plugins load only through dynamic imports. Of the code this PR adds or changes,cc-device-session.tsandcc-device-protocol.tsnewly reach the main bundle, throughstores.ts,serial.tsandserial-protocol.ts.spikerbit-device.ts,spikerbit-connection.ts, themicrobit-connectionlibrary and the hex are imported only from Dataflow'sdevice-connection.ts, so they stay in the Dataflow chunk. (This is from the imports; no bundle analysis was run.) Moving just the new files intosrc/plugins/dataflow/wouldn't help, becauseserial.tswould still import them.CLUE-754 covers fixing it: a core serial service that any tile can use, with Dataflow's own device code (the CC Device Protocol, the Spiker:bit, the firmware simulator) moved into
src/plugins/dataflow/and registered when the plugin loads. CLUE-755 covers the other static imports of tile plugins from core, and a lint rule against them.What the tests cover
nosessionand stop onothersession;#nwrapping.closewrite never finishes.helloline after a cut-off fragment and the!!reply's empty line;SpikerbitDevice,SerialDeviceandCcDeviceSessionconnected to the firmware running in the simulator from CLUE-728: CC Device Protocol and Spiker:bit firmware v4 #3056: no flash, EMG reaching a channel, the servo moving, the session staying alive, and Disconnect leaving the board idle. Its fake connection models the interface chip's buffer (512 bytes, oldest kept, 62-byte reads): a board left in free-run, and one whose host vanished mid-session, both connect without a flash, and both fail against the old one-?connect.Run on a real Spiker:bit (V2, with a servo) in a Mac browser, on an earlier commit of this PR from before the
!!connect, and passing:Connecting after 15 s of unread free-run was run on a V2 from Node over WebUSB, with Dataflow's own
SpikerbitDeviceandmicrobit-connection. The code from before the!!connect reflashed the board in 2 of 2 runs, and the new sequence identified it in 3 of 3, in about 30 ms. Those runs didn't flash through the library, so they don't cover connecting again after a browser flash, when the library has filled the interface chip's buffers.Retesting at this PR's head is done in the UI-half PR, which builds on this one:
The radio hub isn't retested.
On a Chromebook, connecting works sometimes but not always. The cause isn't known yet: CLUE-727's logging of each connection step, including when a flash starts and finishes, should show where it fails, and making it reliable will be done there.
For later (UI half)
lastHeartbeatAtis first set by the first ping's reply, about a second after connecting.err othersession; all three look like a stalelastHeartbeatAt.lastHeartbeatAtis never reset, and MobX won't re-run anything as time passes, so the UI needs its own timer to notice it going stale.document.visibilityStatetells a hidden, throttled tab apart from a board that stopped answering.Pausedstatus: the library reportsPausedonly from listeners that itsinitialize()sets up, and CLUE doesn't call it.err othersessionstops the pings, an unplug goes unnoticed (serial.md, "Connection status tracking").No Firebase functions, rules or indexes change.
🤖 Generated with Claude Code