September 35 / 2026 · Day 35

Agent Dial

An honest fork of the OpenHarness dial for the Stopwatch: see every agent session, answer, approve, stop, and speak tasks, whatever harness runs them.

Arduino ESP32 3.3.10 + M5Unified 0.2.19 + M5GFX 0.2.26 + LVGL 9.5.0 + ArduinoJson 7.4.2 + Node hub (USB serial) + harness adapters (pi, Claude Code, CLI)Contract, host bridge, and 31-scenario device replay verified; live pi and Claude Code approval driven on the attached board; physical hand review pending
On this page
Illustration of the M5Stack Stopwatch development kit

Used in this project

M5Stack Stopwatch

Stopwatch Dev Kit (ESP32-S3) · C152

1.75″ round AMOLED · 466 × 466 · ESP32-S3

The build notes: what we tried, what broke, and what stayed. Get the working instructions →

Agent Dial: the story

Forking a product, not a picture

The OpenHarness dial is a round USB companion for coding agents. Its repository holds firmware, a daemon, schematics, and a handful of design documents.

The documents describe Wi-Fi pairing, an on-screen STOP, a spoken answer, and a colored state ring around the rim. Then you read the firmware, and none of that is there. Voice is a tap on a mic button, stop is a physical key and a confirm, and the rim is just black.

So the fork follows the code. Wherever this lesson says “upstream,” it means what upstream’s firmware does at commit 4689799, not what its docs hope for.

The byte that names the product

Upstream’s frames start with A5 48. The comment beside that 48 explains that a sibling product with the same board once got flashed by the wrong app, because nothing on the wire told them apart.

The Stopwatch is also an Espressif USB device, and upstream’s host offers firmware to anything that greets it as harness. An honest fork keeps the framing, the CRC, and the message names, and changes that one byte to 45. Upstream’s host then never reads our frames, and never offers our board its firmware.

What a harness will let you do

Nothing reaches into a harness from outside. pi extensions and Claude Code hooks run inside the harness and call out.

pi’s extension API can abort a turn and send a user message, so a pi tile gets Stop and Voice. Claude Code’s hooks can decide a permission request, and its documentation shows nothing that stops a turn, answers AskUserQuestion, or injects a prompt. So a Claude Code tile gets approvals and nothing it can’t keep.

The dial learned to hide what a session can’t do rather than pretend.

“The graphics are a little whack”

The first candidate drew everything with M5GFX primitives and bitmap fonts. It passed every contract check, and it looked wrong on the desk.

Upstream renders with LVGL and Geist, and ships both as LVGL sources. Its full font set would not fit the Stopwatch’s app partition beside Wi-Fi, so the fork regenerates the same faces from upstream’s own TTFs, with upstream’s converter options, over a narrower range. The screens now use upstream’s styles, colors, and icons, one pixel right to center on the wider panel.

A clock that ran backwards

The device replay then failed in a way the host model couldn’t. The loop sampled now, input handlers stamped millis() a moment later, and now - stamp wrapped to four billion.

The dial fell asleep instantly. A recording stopped the moment it began. Left alone, the same arithmetic would have declared a live hub silent.

Every stamped timer now measures a signed difference against millis().

What we learned

A fork is honest when it follows the code, credits it, and lists every place it leaves.

Harness neutrality comes from capabilities: each adapter claims only what its harness exposes, and the dial draws only what a session can do.

A contract can pass while the screen looks wrong, so the device’s framebuffer is its own evidence. So is the replay, which found a clock bug no host model could.