Tejas Patel
Tejas Patel
write to
tjs13.9@gmail.com
theme

All work

Mini Cyberdeck

A pocket terminal for answering Claude Code away from the desk: an ESP32-S3, a two-inch screen and eight keys under the thumb.

A working prototype that answers Claude Code with a thumb. On build day, review caught about 50 defects before they reached it.

my part
The brief, the hardware and the testing on the device. The firmware was written by Claude Code.
when
31 August to 24 September 2026, commits on six of those days
context
Personal
with
Claude Code wrote the firmware (140 of the repo's 141 commits carry its name) and ran the design panels and reviews
status
Prototype
Three screens from the firmware's own test harness, overlapped on a cream background: a clock screen with the date and '2 sessions need', the eight-tile launcher (Terminal, Monitor, Voice, Files, Level, Torch, Settings, Today) under a '1 needs you' chip, and a status screen with a y/n question waiting.
Three of its screens, from the firmware's own test harness. Screens, not a photograph: the final case isn’t printed yet.

Mini Cyberdeck, in 3D

The launcher: eight tiles in the key pad's own four-by-two layout, with counts on TERMINAL and VOICE.

outcomes

  1. 1.2 s

    from the firmware starting to the launcher, down from 5.2 s on 12 September, the last reading before build day. From reset: 2.0 s, down from 6.0 smeasured · sourceThe device's boot logs. 24 September, the flash-and-watch log captured in the build session: '(1991) phase2: [t+1185ms] going home', 1,185 ms on the firmware's own clock and 1,991 ms after reset. 12 September, the last boot log before build day: '(5962) phase2: [t+5234ms] going home', 5,234 ms and 5,962 ms on the same two clocks. Boots on 3 September read 5.1 to 5.3 s on the firmware's clock and 5.8 to 6.1 s from reset.

  2. ~50

    defects caught by review on the evening of 24 September, before they reached the devicereported · sourceThe Build Day report written at the end of the 24 September session: 'This evening alone they found about 50 defects before they reached you.' The report itemises 8, 9, 8 and 8 findings for four of the M10 and M11 reviews (33), and the FILES review at 17:36 reported 15 real findings. Those add to 48; the report's 'about 50' is its own round figure, and it gives no counts for its other reviews.

  3. 65

    images drawn on a laptop by the firmware’s own code: 55 app screens and 10 toolkit sample sheetsmeasured · sourceThe host harness tools/uk_host_test at firmware build b60d541 (24 September 2026): 47 render() and 8 save() calls in screens.c, which draw the app screens, and 10 images from render.c, which draws toolkit sample sheets (icons, sprites, a type specimen, demo lists, a text viewer at two scales, two standby faces). Built with -fsanitize=address,undefined. Sample data, with the Wi-Fi name replaced. The reply sheet is not among them: it draws straight to the panel, so the harness cannot render it.

The brief

On the evening of 31 August I gave Claude Code a brief: a battery handheld that joins my tailnet and opens a shell on a remote host, which became my Raspberry Pi 5, built in phases that stop at every gate, because I have to flash the board and look at it and it can’t. Forty minutes later I told it to build all the phases now. Just after ten that night it was showing a live tmux session. Twenty minutes later, once scrolling worked, I asked how it could be a better Claude Code buddy, and a few minutes after that said I wanted to avoid connecting a keyboard.

Claude Code stops when it needs a person: a permission prompt, a yes or no, a choice of three. Away from the laptop, that question waits until somebody sees it. So the device has one job: tell me, and let me answer with a thumb.

I should say who did what. I wrote the brief, wired the hardware and tested the builds on the device. Claude Code wrote the firmware: 140 of the repo’s 141 commits carry its name. Its design panels and reviews proposed much of the interaction detail, and I kept what worked in my hand and sent back what didn’t, from a touch calibration that was “too harsh” to a power-off that woke straight back up.

Eight keys on one wire

decision 1

The key pad is the launcher

The clear plastic sketch model turned the way the handheld is held: a window cut across the top for the screen, and eight holes below it, four across and two down, for the keys.

2-inch screensourceThe device guide's spec table: a 2-inch IPS panel, 320 × 240.

8 keys, one wiresourceFirmware commit 7b2c19e, 2 September 2026: an 8-button resistor ladder on GPIO7, 4 wide by 2 tall.

2-inch screensourceThe device guide's spec table: a 2-inch IPS panel, 320 × 240.

8 keys, one wiresourceFirmware commit 7b2c19e, 2 September 2026: an 8-button resistor ladder on GPIO7, 4 wide by 2 tall.

The sketch model I cut, turned the way it's held: screen up, keys under the thumbs.

By 2 September I had added eight keys under the screen, four across and two down. For a rapid prototype, I cut holes for them and a window for the screen into the plastic box that came with a two-inch display, and photographed it that day. All eight share one analogue pin, each at its own voltage, and I built the ladder from the resistors I had on hand: a 1k where a 10k should go, then two 5k in series when that read wrong. The firmware learns what each key reads, so the exact resistor values and their tolerance don’t matter, as long as every key reads differently. It switches off any key that reads too close to another and keeps the rest, and it leaves the whole pad off if fewer than three can be told apart. The first time I ran it, it refused to arm because the ladder I’d built read wrong. The launcher takes the pad’s shape, eight tiles in four by two, so each key opens the tile in its place.

0.6ssourcemain/buttons.c:61-62 at b60d541: LONG_PRESS_MS 600, LONG_MAX_MS 1500.between a press and a hold

Nothing happens while a key is down. Let go before 0.6 seconds for the short action; hold, and the screen names the long action as you cross into it; past 1.5 seconds it says CANCELLED and letting go does nothing. Holding Enter used to send struck out: continue to the agent. Held a shade too long, it typed the word into Claude Code instead of confirming, so “continue” moved to the reply sheet and holding Enter now opens the keyboard, which sends nothing until you do.

To hand one to someone else, it has to learn its keys on their unit. Setup runs once, before the launcher: calibrate the touch, join Wi-Fi, add the handheld’s key on the Pi, then press each key as it lights on a drawing of the pad.

First run on a new unit4 screens, 320 × 240 screen
  1. Touch calibration: tap the cross, with dots showing progress through five targets.

    1Calibrate the touch

    Five crosses set in from the corners, which the digitiser doesn't quite reach.

  2. Wi-Fi: the handheld reaches your Pi over Wi-Fi, then your tailnet. Connected, with Change and Continue.

    2Wi-Fi

    Join a network, or skip it and finish later in Settings.

  3. Your Pi: add this handheld's key to authorized_keys on the Pi, show the key as a QR code for a phone, and the host.

    3Your Pi

    The handheld's key as a QR code, to add on the Pi from a phone. No Pi password touches the device.

  4. Learning the keys: key 3 of 8, lit on a drawing of the pad, press it and hold it down.

    4Learn the keys

    Each key lights on the pad drawn on screen: press it and hold.

Every drawn screen on this page except the reply sheet is the firmware's own screen code, drawn on a laptop with sample data. The Wi-Fi name is a stand-in.

Answering from a pocket

decision 2

Answer with the question still on screen

The reply sheet across the top half of the screen, with Claude Code's question still visible below it: make this edit to voice_service.c, yes, yes and don't ask again, or no.

top half

the question, still on screen

top half

the question, still on screen

A laptop preview of the reply sheet over a permission prompt, copying the firmware's draw calls, with the prompt text typed in. The sheet draws straight to the panel, so the screen harness can't render it.

The device reads Claude Code’s state from the terminal itself. The bell it rings when it stops for a person means NEEDS YOU; braille spinner glyphs, or output in the last 2.5 seconds, mean WORKING. Put down, the standby face says which in large type, and a NEEDS YOU lights the screen even untouched. Press Reply, or double-tap the glass, and the sheet covers the top half only, so the question keeps painting underneath. The first reply menu covered the whole screen and hid the prompt it existed to answer; the design panel I asked for on day three caught it.

Answering the agent4 screens, 320 × 240 screen
  1. The standby face at 14:32: one session needs you, the agent's last answer under it, battery and online status at the foot.

    1It needs you

    Put down, the standby face says so in large type, lit even untouched.

  2. The reply sheet across the top half of the screen, with Claude Code's question still visible below it: make this edit to voice_service.c, yes, yes and don't ask again, or no.

    2Reply

    The sheet takes the top half. Claude Code's question keeps painting below it.

  3. MENU over the live terminal: quick replies 1, 2, 3, y, n, continue, stop, /compact and run the tests, then Sessions, Keyboard, Find, Git and Keys.

    3Or MENU, for the rest

    Quick replies, your own among them, then Sessions, Keyboard, Find, Git and Keys.

  4. The standby face again, now saying Working, claude.

    4Back to work

    Put down again, the face says Working.

Sleep follows the hand. While you move it in your hand the screen stays lit; hold it still for a couple of seconds and it counts as put down, dropping to the standby face after 45 seconds by default. Face down, it switches off at once, and with Raise to wake on, which is the default, it lights as you pick it up.

The same screens, wired together:

The launcher, and what it opensTap a tile to open it, and Home to come back. Record a note in VOICE, or open Sessions from the terminal.
The launcher: eight tiles in the key pad's own four-by-two layout, with counts on TERMINAL and VOICE.
AppsDeeper

Voice notes

decision 3

A voice note can be a request to act on

Hermes' answer on the handheld in large orange type, outcome first: the reminder is set for Thursday at 9am and the dentist moved to Tuesday, then the grocery list. Sample data.

outcome first

no question back

outcome first

no question back

The answer screen, drawn on a laptop by the firmware's own screen code, with a sample answer.

On 12 September I asked for the recorder to feed Hermes, the agent on the Pi. A note recorded in VOICE goes over the tailnet, is transcribed by Whisper through OpenRouter, and Hermes answers on the device. It worked, and that night I asked why Hermes did so little with the notes: its prompt told it to file each one and reply in a single line. Now Hermes works out whether a note is something to keep, a question or a request, uses its tools to do what the note asks, and answers for a 320 by 240 screen: the outcome first, and never a question back. On 24 September the recorder and the notes list became one VOICE app at my ask, and you decide what a take is for after you’ve made it.

A note to Hermes4 screens, 320 × 240 screen
  1. VOICE ready to record: a waveform, 0:00, a red record button, and the last take marked Answered.

    1Record

    One button. Holding VOICE's key on the launcher starts recording at once.

  2. Recording at 1:23 with 20 seconds left, the waveform live and the status bar showing REC.

    2Recording

    A running timer shows, with a countdown once you are near the limit, and REC stays in the status bar.

  3. After stopping: a 42-second take, with Ask Hermes, Save audio and Discard.

    3After the take

    Ask Hermes, save the audio, or discard it.

  4. Hermes' answer in large type: the reminder is set for Thursday at 9am, the dentist moved to Tuesday, the grocery list updated.

    4The answer

    Outcome first, in large type.

Updates

decision 4

A failed update says what actually failed

Couldn't update: the Pi has no update staged, and this version is still installed, unchanged.

the reason

the reason

A failure card drawn by the firmware's own card code, with the reason typed in for the render. On the device the reason is the step that failed, such as 'handheld-term.bin not found on host'.

The first update over the air, on 2 September, failed with a screen telling me to put the file in the home directory on the Pi. The file was there. The real cause was memory: the update opened a second SSH connection beside the live session, and its first read needed a 16 KB block when the largest free block of internal RAM was 15 KB. So the failure screen names the step that failed. An update now streams into a spare slot, is checked before anything is erased, and is marked good only once it reaches the Pi; if it never does, the next restart puts the old version back.

An update3 screens, 320 × 240 screen
  1. Software Update: an update is ready, with the version on this handheld and the one staged on the Pi.

    1Ready

    The version on the handheld beside the one staged on the Pi. The build ids shown are sample values.

  2. Updating: installing at 42 percent, and a warning not to switch off, because it restarts on its own when done.

    2Installing

    Don't switch off: it restarts on its own when done.

  3. Couldn't update: the Pi has no update staged, and this version is still installed, unchanged.

    3Or, if it can't

    It says why, for example that the file isn't on the Pi, and the running version stays.

One pin, one job

decision 5

The camera goes

April 2025: a hand holding the cyberdeck's two-inch Waveshare ESP32-S3 board, in a different project, its screen showing a live camera preview labelled LIVE - Touch to capture.

I’d used the same board as a camera in April 2025, in a different project: an image-recognition device for experimenting with code and LLM calls. On 3 September the cyberdeck got a camera too. That night the key pad died whenever the camera had been used. The firmware handed pin 7 back and forth between the two at five places in the code, and by the next afternoon Claude Code had put the fault down to my wiring and stopped. Then my meter read 20 ohms between pin 7 and ground while the keys were dead, which is what a chip holding the pin low looks like, and unplugging the camera’s ribbon brought the key pad back, though the stored levels had to be learned again. So I dropped the camera. Separately, the analogue microphone, which shared the key pad’s converter, had been swapped in code earlier that afternoon for a digital one on three pins of its own, and I wired it that night.

14pins freedsourceFirmware commit 03784aa, 4 September 2026: 'fifteen pins - 2 4 6 8 9 10 11 12 13 14 15 16 17 21'. The list holds 14 pins; the commit's 'fifteen' also counts GPIO7, which stays with the key pad.

Build day, 24 September

On 24 September I fitted a 2,500 mAh cell in place of the 300 mAh one, asked for an audit of the whole codebase, and then asked for the product to be “complete”: lists that scroll and one voice app. Claude Code turned two audits and two design passes into fourteen milestones, M0 to M13, and I approved the plan that afternoon.

The plan gave every milestone the same gate: a build with zero warnings, an adversarial review of its diff, and a commit, with the checkpoints marked for it also staged on the Pi for an update over the air. In practice the Build Day report counts 13 reviews for the 14 milestones, M3 to M7 were reviewed together, and M3 to M9 and M12 landed as one commit. The staging script does refuse a build with warnings, and I flashed the builds to try in my hand.

That evening the reviews caught about 50 defects before they reached the device. Opening the Wi-Fi picker took the device offline for good. A notification tapped during setup left Home disabled until a hard reset. The standby face could cover the menu while its buttons, Switch off among them, stayed live.

Two bugs did reach my hands: opening a folder on the card rebooted the device, because the card and the screen drove one bus at once, and a misread length prefix kept the tailnet down. Both were found from the device’s logs and fixed within the hour.

sources

  1. 11.2 s from the firmware starting to the launcher, down from 5.2 s on 12 September, the last reading before build day. From reset: 2.0 s, down from 6.0 s. The device's boot logs. 24 September, the flash-and-watch log captured in the build session: '(1991) phase2: [t+1185ms] going home', 1,185 ms on the firmware's own clock and 1,991 ms after reset. 12 September, the last boot log before build day: '(5962) phase2: [t+5234ms] going home', 5,234 ms and 5,962 ms on the same two clocks. Boots on 3 September read 5.1 to 5.3 s on the firmware's clock and 5.8 to 6.1 s from reset.
  2. 2~50 defects caught by review on the evening of 24 September, before they reached the device. The Build Day report written at the end of the 24 September session: 'This evening alone they found about 50 defects before they reached you.' The report itemises 8, 9, 8 and 8 findings for four of the M10 and M11 reviews (33), and the FILES review at 17:36 reported 15 real findings. Those add to 48; the report's 'about 50' is its own round figure, and it gives no counts for its other reviews.
  3. 365 images drawn on a laptop by the firmware’s own code: 55 app screens and 10 toolkit sample sheets. The host harness tools/uk_host_test at firmware build b60d541 (24 September 2026): 47 render() and 8 save() calls in screens.c, which draw the app screens, and 10 images from render.c, which draws toolkit sample sheets (icons, sprites, a type specimen, demo lists, a text viewer at two scales, two standby faces). Built with -fsanitize=address,undefined. Sample data, with the Wi-Fi name replaced. The reply sheet is not among them: it draws straight to the panel, so the harness cannot render it.

related

  • Warp DriveA desk toy for children aged 6 to 12: warp with the knob, return to zero, meet one of 18 things in space. I listed it for sale on my shop.
  • MirrorFactory OSA shop-floor board I built for a framing workshop's iPads. The floor kept its TV, and that taught me more than the board did.
Next: Sehat SathiA wearable ECG prototype for rural Kutch, held on by magnets meant to double as its contacts, lit through a motif from local embroidery.4 of 6