Two round-screen desk badges driven by a phone tap, so the question of which office someone is in today gets answered from across the room instead of in a group chat.

Solo build2026Prototype, rollout blocked

01The problem

I split my week between two offices, and the question that followed me around was the dumbest one possible: is he in today, and which desk. Colleagues at the office I was not at had no way to know short of messaging me, which is an interruption for a one-word answer several times a week.

Commercial punch-clock hardware solves the HR half of this and none of the human half. It produces attendance records, and it gives the person walking past an empty desk nothing at all, because the answer lives inside a portal nobody opens. The existing behaviour was a message in a group chat, or a guess. The quieter second problem was personal: no honest record of when the working day actually started, and therefore no honest signal for when it could reasonably end.

02What I built

Two desk objects, one per office. Each is a 1.28-inch round display driven by a small microcontroller over SPI. Tapping a phone on an NFC sticker fires a shortcut that posts a location to a script endpoint, which appends a row to a spreadsheet; both badges poll that same endpoint every five seconds and re-render, so a tap at one badge changes the face on the other within a few seconds. That cross-badge behaviour was proven with both boards on a bench network — the office installation is still blocked by the radio constraint in section 07. Each badge runs its own animated character whose behaviour tracks the working state rather than decorating it: asleep before the day starts, working during it, celebrating once it is done.

  • One sticker does both jobs: consecutive taps at the same office more than thirty seconds apart flip the state, so there is no menu and no app to open.
  • Four visual states — pre-work, working, can-leave, off-work — each with its own palette, character animation and caption strip.
  • An eight-and-a-half-hour rule in the backend: once that much has passed since the first tap of the day, the badge moves itself into the celebration state.
  • Arrival triggers tagged separately, so they update location without ever counting as a clock-in or clock-out.
  • Accidental double-tap protection: same-office taps inside thirty seconds are treated as one event.
  • A single firmware codebase that compiles to two different products, with a build-time flag that strips the other office's character assets out of the binary entirely.
Schematic of the four badge states A drawn representation of the round display in each of its four states. Every face carries a day strip at the top, a character band in the middle and a caption strip below; the second face is marked as the current state. Nothing here is a photograph of the hardware. PUNCH-CLOCK FOUR SCREEN STATES PRE-WORK WORKING CAN LEAVE OFF-WORK

Drawn, not captured — no photograph of the hardware appears here. Each disc is one round panel: a day strip at the top, the character band in the middle, a caption strip below, and a palette that changes with the state. The marks show which of the four states is current. All content is illustrative.

03As a product

Who buys it
Small multi-site teams — studios, agencies, clinics, workshops — where five to thirty people rotate between locations and presence matters more than payroll-grade attendance. The same chassis is also a status display for anything a small business already tracks in a spreadsheet.
Value
Presence you read at a glance from across the room rather than look up. The tap is the entire user interface, the backend is a spreadsheet the office already knows how to read, and the running cost is nothing.
Positioning
Enterprise attendance systems are priced and designed for compliance, charge per seat, and give a passer-by nothing. Chat-app status is free but depends on everyone remembering to set it, which nobody does. This sits between the two: the physical tap makes the update automatic and the physical display makes the answer public without anyone having to ask.
Status
Prototype and internal only. Both boards were built and run end to end; permanent office installation is blocked on a radio constraint, not on code. Never sold, never priced, no second user.

04How it works

Stack
An ESP32-C3 module with a 240 by 240 round IPS panel over four-wire SPI, built through the Arduino toolchain; LovyanGFX for display and JPEG decode; a Google Apps Script web app as the serverless endpoint and a Google Sheet as the datastore; phone shortcuts and NFC stickers as the trigger. Character assets are generated as video, transcoded, and baked into flash as byte arrays by two small Python tools in the repository.
Shape
Polling, not push. Every device asks the same endpoint the same question every five seconds, and the endpoint derives the answer from the day's rows each time rather than storing state. Nothing here is a throughput story; the constraints that mattered were four megabytes of flash, a fragmenting heap and a 2.4 GHz-only radio.
Tap to both screens, four levels, one fan-out A vertical flow chart in four levels. A phone tap on an NFC sticker fires a silent shortcut that posts one row — a shared token and an office code — to a script endpoint, which appends a single timestamped row to a spreadsheet. The endpoint then replays the whole day of rows into one computed state on every request, storing nothing itself. That single state fans out to two lanes at the bottom: the badge that was tapped and the badge in the other office, both of which pull the answer every five seconds rather than being pushed to. Every band is hollow and equal in width, because nothing in this flow has ever been counted; the widths describe the structure only and claim no quantity. TAP NFC sticker, silent shortcut ONE POST: TOKEN PLUS OFFICE CODE APPEND One timed row, token checked ONE ROW IN, ALL ROWS OUT REPLAY A day of rows, one state NOTHING STORED, RECOMPUTED EACH TIME POLL Both badges, every 5 s TAPPED OTHER

Width uniform throughout — nothing that moves through this flow has ever been instrumented or counted, so every band is drawn at equal width and no band claims a quantity. The split at the bottom is a fact about structure, not about volume.

One write, two readers, and the fan-out is the point: the tap happens at one office and the answer appears at both. A Wi-Fi arrival can move where the badge thinks you are but can never clock you in or out, and two taps inside thirty seconds count as one. The flow was exercised with both boards on a bench network, not in the offices.

The decision I spent longest on

Abandoning the full-screen sprite. The natural way to animate a 240 by 240 display is to compose each frame into an off-screen buffer and push it, which avoids tearing entirely. On this chip that buffer is 115 KB in one contiguous block, and the allocation fails intermittently even with 256 KB of heap free, because the heap fragments.

Rather than fight the allocator I restructured the renderer into bands: the character image decodes straight to the panel in the middle strip, and the strips above and below are drawn once and left alone until their text actually changes. That removed the memory risk, cut the per-frame work by more than half, and killed the flicker a naive full redraw would have put on the text.

05Retrieval architecture

There is no retrieval layer in this project because there is no model in it. A tap writes a row and two screens read it back. Saying that plainly is more useful than stretching the vocabulary until it fits, because what stands in retrieval’s place is a real architecture with real trade-offs of its own.

That architecture is state sync. The endpoint keeps nothing: it recomputes the answer from the day’s rows on every request, and the badges ask rather than being told. Three of the six layers below are empty, and the emptiness is most of the reason the thing is reliable — no cache to go stale, no subscription to keep alive, and no state on a device that could disagree with the sheet.

ModelNo model anywhere. Nothing on the badge, in the script endpoint or in the phone shortcut infers anything; every value on the screen is arithmetic over timestamps.

Corpus
One spreadsheet, and it is the system rather than a reference for it. Each row is a timestamp, an office code and the trigger that produced it; a working day is a few rows. There is no second copy anywhere — the badge holds no history, the phone holds none, and the endpoint holds none between requests — so the sheet is the sole source of truth by construction rather than by policy, and anyone in the office can open it and read exactly what the badge read.
Ingestion
A tap, over HTTPS, one row at a time. The phone touches an NFC sticker, a silent shortcut posts a shared token and an office code, and the script appends one timestamped row. Three things are refused or reclassified at the door: a token that does not match, an office code outside the whitelist of six, and a second tap at the same office inside thirty seconds, which is folded into the first rather than recorded twice. Arrival-triggered rows are accepted but tagged separately, so they can move where the badge thinks you are without ever counting as a clock-in or a clock-out.
Index
None. The store is a few rows per working day and it is read from the top on every single request. At this size a date-keyed read would save nothing anybody could measure while adding a second thing that has to stay correct, and a spreadsheet was chosen over a database in the original spec precisely so the store would stay legible to a person. It is also the first thing that would have to change if this ran for thirty people instead of one, and knowing exactly which line that is seemed worth more than pre-empting it.
Query
None. Nothing is asked. Both badges issue the same authenticated request every five seconds and carry no parameters at all: there is only ever one question, the endpoint already knows what it is, and there is nothing to interpret, disambiguate or route. The tap at the other end is not a query either — it states a fact about where somebody is and never asks the system for anything back. A device that has been unplugged recovers by asking the same question again, which is the whole benefit of having only one.
Selection
The replay is the mechanism. On every request the script reads the whole day, finds the first tap, counts consecutive same-office taps more than thirty seconds apart to derive the clock-in and clock-out toggle, ignores arrival-sourced rows for that purpose, and applies the eight-and-a-half-hour rule that moves the badge into its celebration state on its own. What comes back is not a list, so there is nothing to shape or order: a day of rows collapses into one computed state plus the time of the first tap. With no candidate set and no score, there is nothing that could be reranked, and the same answer is handed to both badges rather than assembled separately for each.
Grounding
None. There is no model anywhere in this system, so nothing can invent an answer and there is nothing to fence in. The screen shows arithmetic over timestamps its own user wrote, and the only thing that can be wrong is a tap — which is corrected by tapping again, not by an approval step. A gate here would be theatre: the tap is the input rather than a proposal awaiting review, and putting a confirmation in front of it would slow down the single interaction the entire product is made of.

Present — this layer exists and runs.Absent by decision — the layer is not there, and the sentence beside it is the reason. Every one of the six is answered on every system in this chapter, so the rows can be read across pages.

06Numbers

Measure Figure Basis
Devices built and running 2 Verified
Tap to both screens updating 5 s Verified
Animation frames baked into firmware 72 Verified
Firmware and backend source ~1,164 lines Verified
Flash used by the larger build, of a 3 MB partition 2.95 MB Verified
Backend running cost HK$0 Verified
Build time, concept to working pair ~2 days Projected

Verified — read off the firmware constants, the frame headers, a line count over the sketch and the two asset tools, and the build output recorded at flash time. Projected — the spec is dated one day and the first end-to-end commit the next, so the build spans two calendar days of sessions. That span is the only evidence; hours actually worked were never logged.

07Timeline

  1. 2026-05 Spec written and handed over: two boards, NFC trigger, script backend, and a short list of decisions explicitly marked as not up for re-litigation.
  2. 2026-05 Firmware, backend and the phone shortcuts built and taken end to end; character assets generated, transcoded and baked into flash; both boards flashed and running.
  3. 2026-05 Committed to a private repository, 97 files, secrets excluded with a template committed in their place.
  4. 2026-05 Office rollout attempted and blocked. The office network is 5 GHz-only and the whole chip family is 2.4 GHz silicon; diagnosed the same day.
  5. 2026-08 Still on the bench. The travel router that would bridge a 2.4 GHz network off the office one has not been bought, and two cosmetic items remain open.

08Looking back

What broke

The rollout, and not for a reason any amount of code could fix. The office network is 5 GHz-only, confirmed by reading the channel off a connected Mac, and the entire chip family is 2.4 GHz silicon. The clean fix is a travel router in repeater mode bridging a 2.4 GHz network off the office one, which is a purchase I have not made, which is why two working badges are sitting on a bench rather than on two desks. Everything in this case study that involves both boards talking to each other was proven on that bench network.

Smaller ones cost real time. Sketch headers named after common words were silently picked up by the platform's own headers, because the filesystem is case-insensitive by default. The display library shipped a font containing only digits, so the first status line rendered as a row of blanks, and the fix was to split fonts by job. The script platform fought back twice: enabling its API takes up to fifteen minutes to propagate and needs a fresh login to pick up the scope, and a POST to the endpoint returns an error status while the handler runs correctly anyway, so the status code is useless and the sheet has to be read back to verify. One build even locked the serial device while the screen kept running, so the board looked healthy and was unreachable.

What it changed

It proved the same personal-automation thesis holds in hardware. The stack is deliberately unglamorous — a spreadsheet, a free script endpoint, a phone tap — and the engineering effort went where a user actually feels it: how fast the screen reacts, whether the animation stutters, whether the caption is legible from across a room, and whether the bezel of one case crops the artwork, which it does, which is why safe content on that board sits inside a tighter radius. The piece most worth reusing is the build flag: one codebase, one flash command, two objects with distinct personalities and no dead weight in either binary.

Where it stands

Two badges that work, on a bench, waiting on a router I have not bought. The cat board still reuses its working frame for the celebration state, and the motion sensor is not populated on these boards, so knock-to-ask is written and disabled.

Back to Top