Engineering Journal

← all posts

The frame that grows as it climbs

FirmwareHardware & PCBSoftware & tooling
Mouse engineers at five levels of a copper relay tower add one glowing data
block at each stop, building a longer sealed message as it climbs.

Our LED poles are getting their sense of touch back, and this week the work was pure paper: designing the byte-level language that eighteen tiny chips will use to report a hand on the pole. No firmware was flashed and nothing here has run on hardware yet. What exists as of tonight is a specification, a reference implementation, twenty-two byte-exact example messages, and a test that corrupts every byte of four of them, one at a time, showing that a single bad byte can destroy a frame but cannot quietly move a board's data to the wrong place. Every number in this post is arithmetic, not measurement.


What we are actually building

A PoleFX pole is a dance pole wrapped in LEDs. Up the inside run thin strips of circuit boards, soldered end to end like segments of a tape measure. Each board carries a small microcontroller, a chip with less memory than the HTML of this page, that senses capacitance through the pole wall. Touch the pole and the nearest sensors feel your hand the same way a phone screen feels your finger.

This is not the pole's first sense of touch. The 2024 system worked: the pole streamed its raw readings over the network to a PC running TouchDesigner, which filtered them, decided what counted as a touch, rendered a reactive texture, and streamed that texture back to the pole. Real shows ran on it. But it came with two strings attached. The pole could only feel anything with a whole computer and a network riding along, and it sensed along a single strip, one side of the tube, so you had to touch the correct side of the pole.

The 2026 version cuts both strings. Detection moves onto the pole's own controller, so touch works anywhere the pole works, no laptop in the rig. Coverage grows to three strips spaced around the tube, eighteen boards in all, so there is no wrong side, and a hand wrapped around the pole becomes something the system can actually recognize. That is what unlocks the effects we want to build next. It also changes the plumbing: those eighteen boards now have to report inward, chip to chip to controller, fast enough to drive light, with no PC in the loop to clean up after them. That path is a language problem. Somebody has to decide what the bytes mean. This week I decided, and wrote it down before touching any firmware, because wire formats are the kind of thing that is cheap to specify and brutal to retrofit.

Two schematic chains of six sensor boards under a controller. Left, v5: the controller sends an ask down to the nearest board, the packet grows moving away from it, and a long red return wire carries the finished packet from the far end all the way back. Right, v6: the far board speaks first and the frame grows hop by hop toward the controller, 9 to 33 bytes, with no return wire.
The 2024 chain was a loop: the controller asked, the packet grew as it moved away, and the finished answer came home over one wire spanning the whole pole. v6 inverts the flow so data only ever moves toward the controller, and the long fragile wire stops existing.

The bug that shaped the whole design

The old protocol, from 2024, had a flaw we found by reading the code, not by watching it fail. Boards in a chain need to know their own position: am I board 0, board 3, board 7? The old design answered that with division. Each board counted the bytes that had already passed through it and divided by the size of one board's report. Bytes so far, divided by bytes per board, equals my position. Elegant, self-configuring, no addresses to assign.

Except the size of one board's report came from a byte in the request, and if that byte arrived as zero, through a bug or a single bit flipped by electrical noise, the division was by zero. On this particular chip, divide by zero does not crash. It quietly returns zero. So every board on the chain would conclude, simultaneously and with full confidence, that it was board 0. The controller would see a chain of impostors all reporting from the same position, and nothing in the protocol could detect it.

One flipped bit, and the pole forgets which end is up. That is the failure the new grammar had to make unrepresentable.

The constraints, honestly listed

Designing for these boards means designing inside a box:

What a hand actually looks like in data

Here is the observation that makes the whole thing small: a pole is almost always not being touched. And when it is touched, a hand lights up a handful of adjacent sensors, not fifty. Sending every sensor's full reading every cycle would be shipping a mostly blank spreadsheet 51 times a second through chips with no room to hold it.

So a board's report is built around saying nothing efficiently. A board with nothing to say contributes exactly two bytes: one bit per sensor of "touched or not," and a count of zero extras. A quiet six-board strip produces a complete frame of 19 bytes.

When something does happen, the board sends short records only for the sensors that matter, and the definition of "matters" was dictated by the detection software that consumes this data. That detector, prototyped earlier this week, wants more than the loud sensors. It confirms a real touch by checking whether a candidate's neighbors also stirred, faintly, below the touch threshold; that faint shoulder is how it tells a fingertip from electrical noise. So the reporting floor sits low, near the noise, and any reported sensor automatically brings its immediate neighbors along even when they are below the floor. Clip the shoulders off in transit and the detector upstream goes blind to exactly the evidence it runs on.

There is a cap: five records per board, with an overflow flag. Wrap your whole palm around one board and it reports the five strongest readings and admits the coverage is partial, rather than blowing the frame size out for everyone. The cap is not a guess; it falls out of the latency arithmetic, because the worst legal frame has to be budgeted, and it still carries a full grip.

Counting instead of dividing

The new frame is an envelope that grows as it climbs. The far board starts it: a 5-byte header, its own 2-plus bytes, a 2-byte checksum. Each board on the way toward the controller opens the envelope, adds its own block on the end, updates the count and length on the front, reseals the checksum, and passes it along. Your position in the chain is simply which block is yours: first block, first board. Nobody divides anything. There is no division operator anywhere in the protocol, which retires the board-zero bug as a class rather than patching the instance. Not every way a board can end up mislabeled, though. Count, declared length and block sizes are checked against each other, so a board that appends a block without owning up to it is rejected; a board that appends one and counts it shifts everyone behind it and the frame is valid. What changed is that this now takes a firmware bug rather than a single flipped bit.

Corruption gets the same structural treatment. Every frame carries a 16-bit checksum, and the format is strict: reserved bits must be zero, record lists must be sorted, the declared length must match the content exactly, and any violation rejects the whole frame. Nothing partial is ever accepted, because a fresh frame is at most one heartbeat away and a wrong frame can misplace a touch. The reference implementation's meanest test takes the four well-formed example frames, flips every byte of each, one at a time, three different ways, and asserts that all 726 mutations are rejected; a companion test cuts one frame off at each of its 33 lengths and rejects those too. Both pass.

It is worth being exact about what that buys, because it is less than it sounds. A single corrupted byte is an eight-bit burst, and CRC-16/CCITT catches every burst up to sixteen bits, so the result does generalize past the four frames tested: one bad byte either dies at the checksum or dies at a structural rule. Multi-byte corruption is a different animal. Sixteen checksum bits mean roughly one badly mangled frame in 65,536 still checks out, and a frame that checks out is a frame the controller believes. The grammar makes a mis-addressed board hard to produce, not impossible. What makes it survivable is the rest of the design: the next frame is 19.53 ms behind, and the sequence number counts what went missing.

Two more decisions worth naming. Every frame carries a sequence number that counts acquisitions rather than transmissions, so a skipped frame is a visible gap instead of a silent one; the system is allowed to fall behind under a worst-case load, and has to say so when it does. It is one byte with no epoch beside it, which means it wraps every five seconds at today's cadence and starts over if the far board reboots. It counts short gaps honestly and cannot describe long ones. And the header leads with the length, so a relaying board can update the envelope and start forwarding before its own contribution even exists. That trick, cut-through relaying, is not required today. The arithmetic says the current heartbeat does not need it. But the 5 ms ambition does: at that rate the sums say plainly that store-and-forward at today's serial speed cannot fit even an idle frame, and the fix is faster links plus cut-through, both of which the grammar already supports. Writing that down now, before hardware, is the cheapest insurance in the project.

Worth the sophistication?

A fair question, because the growing packet is not the new idea. The 2024 chain already grew as it traveled: each board appended a fixed sixteen bytes, full 16-bit readings for all eight of its sensors, every poll, touched or not. That simplicity had real virtues. Fixed sizes mean fixed offsets, no selection logic, and a parser a few lines long.

What it cost was the wire. A six-board readout came back at roughly 360 bytes and 31 milliseconds of wire time around the loop, with nobody touching the pole, and at the firmware's 30 Hz poll rate that was 96 percent of the wire's capacity. At the firmware's own eight-board maximum the arithmetic goes past 100 percent: the code asks for data faster than the loop can carry it. The simple format was already at its ceiling while the pole sat idle.

The v6 frame does the same six-board job in about 7 milliseconds idle and 14 with a grip, a quarter of the wire time, and spends part of the savings on things the old packet simply did not have: the checksum, the sequence number, the strict validation. Sparseness is what pays for safety. So the difference is not that the new frame grows and the old one did not; both grow with board count. The difference is what each board adds, two bytes when it has nothing to say instead of sixteen, and what the frame can survive on the way.

The sophistication has a real price, and it is worth naming: variable-size frames are harder to parse than fixed ones, which is precisely why the strict validation and the golden vectors exist. And accumulation is still accumulation. Every hop retransmits everything before it, so total wire work still curves upward with board count; the compact encoding shrinks that curve's constant dramatically, and cut-through is the stored lever that flattens it if we ever need to.

Room left to improve, written down rather than discovered later: every number here is arithmetic and the first improvement is a logic analyzer on real hops; cut-through is specified but unimplemented, including the awkward part, where a board that has already forwarded most of a frame learns at the trailer that the frame was bad and has to deliberately poison its own checksum on the way out; the serial links have never been qualified above 115200 on these crystal-less boards; the neighbor rule at board seams only works in one direction; and the push-versus-poll question on the last hop is deliberately unsettled until hardware can break the tie. If bytes ever get genuinely tight, delta records could shrink below eight bits, but that complexity has to earn its way in with measurements first.

What exists tonight

A specification with byte-exact layouts and worked examples for six-board and eight-board strips. A pure-Python reference encoder and decoder, small enough to port to a 4 KB chip without floating point. Twenty-two golden vectors, seven good and fifteen deliberately malformed: frozen example messages that the board firmware and the controller firmware will both be tested against, so neither side gets to reinterpret the format in its own tests. And budget tables for every scenario from idle to palm-wrap, all derived, all waiting to be confirmed or embarrassed by a logic analyzer.

The next byte on this wire will be sent by real hardware, and the first thing it transmits will be checked against files that already exist.