index / work / bubba / nfc writer
The NFC writer
The bench tool that gives every Bubba totem its identity. It writes the tag, reads it straight back off the chip, compares it byte for byte against what was sent, and only then records the unit.
Internal, single-purpose, not shipped to anyone. It has provisioned twenty-two tags for the test cohort, so everything below has run end to end on tags that are now in other people's hands. This page exists because it is the point where the manufacturing and the software of a physical product actually meet.
| role | sole designer and engineer |
|---|---|
| platform | macOS desktop, Python, Tkinter |
| surface | 745 lines, 2 files1 |
| reader | ACR122U over PC/SC |
| tags | NTAG / Ultralight |
| registry | Google Sheet via a service account |
| provisioned | 22 tags, test cohort |

Where it sits
the bridge between the object and the system
Every Bubba totem carries a URL of its own, burned into the tag at production time: tapbubba.com/totem/BUBBA-0001, and the next one, and the one after that. The mapping between the object in someone's hand and the record in the system has to stay trustworthy for as long as the object exists, which is longer than any of the software will last in its current form.
This tool creates that mapping, and it is the only place a totem's identity is ever assigned. The identity is deliberately ahead of its uses. Tap a totem today and the URL reaches the same page whichever unit you tapped, telling you to open the app; the serial that distinguishes it is in the registry, and on the chip, waiting for a web side that reads it. Putting it there at production time costs nothing. Retrofitting it onto tags already sitting on other people's desks would cost everything.
What it writes
three NDEF records, at the card protocol
| rec | type | value on tag |
|---|---|---|
| 1 | URI | https://tapbubba.com/totem/BUBBA-0001 |
| 2 | Text | programmed:20260602 |
| 3 | Text | version:1 |
Rather than depending on a high-level NFC library, the tool speaks PC/SC directly to the reader and issues raw APDUs against tag pages. Page 03 carries the capability container, E1 10 12 00, where E1 is the NDEF magic byte, written if absent. Page 04 carries the TLV header and message length, then the encoded records. From there the payload is written four bytes at a time, terminated with FE and padded to a page boundary.
That was a considered trade. Dropping to the card protocol removed a fragile dependency layer and made tag-format failures diagnosable instead of opaque. A tag that comes back wrong now reports which page failed and what the card answered, rather than raising somewhere inside a library.
Serials come from the registry, never from the machine
one counter, and it is not local
The next serial is not held in a local counter. On each launch it is derived by scanning the shared registry for the highest existing BUBBA-#### and incrementing. Two operators, on two machines, with two readers, cannot silently drift into duplicate identities.
The registry is a Google Sheet reached through a service account, which was chosen rather than settled for. At this stage the fleet record needs to be readable and editable by people who are not engineers, exportable, and easy to audit, and a sheet does all of that on day one. The tool treats it as an append-only log, so it can be swapped for a real datastore later without touching the tag-writing core.
The registry row holds more than the tag does. Batch and unit name are recorded against the serial but never written to the chip, along with the tag's own hardware serial, which is the one identifier on the object that this tool did not choose. Anything that might need to change later stays off the tag, because the tag is the one part of the system that cannot be redeployed.
Four failure modes designed out
not documented around
A finished totem cannot be silently reassigned. Before any write, the existing NDEF is decoded. A tag already carrying a tapbubba.com URL is rejected, and the tool reports which unit it already is, so a live totem in someone's hand is never orphaned by a stray tap on the reader.
The tag is proven correct, not assumed. Every write is read straight back off the tag and compared byte for byte against what was sent. The result appears in the window as verified or failed before the operator moves on, so a bad unit is caught at the bench rather than in a box.
No phantom rows. The record is written only after the tag write succeeds. A failed or aborted write leaves nothing behind, so the registry can never claim a unit that does not physically exist. The registry is the fleet, and a fleet with imaginary members in it is worse than no fleet record at all.
A production run does not stop because the wi-fi did. If the registry append fails or times out, the row is persisted to a local queue and the operator is warned in the window. The queue flushes on the next successful connection, and a partial flush re-queues what did not land rather than dropping it.

Built for a run, not for a demo
the bench, not the desk
Writing is a continuous loop, paced by the tag being physically lifted off the reader rather than by a timer, so the operator sets the speed and never clicks between units. All NFC and network work runs off the UI thread, so the window stays responsive mid-write and a run can be stopped cleanly at any point.
The interface is Tkinter from the standard library, themed to the Bubba palette. Nothing to install, nothing to keep updated, nothing to break on a bench machine that gets touched twice a year. The core module runs standalone as a command line tool and the same verified write path runs headless, so the application layer adds ergonomics and status but never behaviour. That is why the four guarantees above hold whichever way it is run.
Honest limits
known gaps, stated plainly
- Single operator, single reader, macOS only. It assumes one USB reader and a hand-provisioned service account. Built to do one job reliably rather than to generalise.
- Cross-station safety is derivation, not locking. Serials are derived from the shared registry at launch rather than claimed under a lock, so two operators starting simultaneously mid-run would need to coordinate.
- Nothing protects the sheet from being edited by hand. The write-then-record ordering stops the registry claiming a unit that does not exist, but the same choice that makes the registry legible to non-engineers makes it editable by them. A deliberate trade, and not one I have mitigated.
- It has provisioned a cohort, not a batch. Twenty-two tags for testers, written to cards rather than to finished objects. The failure modes above are designed for a run of fifty and have not met one.
Sources
- Counted from the repository at one commit on 4 September 2026: 745 lines across 2 files. The tool only, not the registry sheet, the packaged application or the README.