thegian7 ~/catalog/…/hackdeck$

Hardware · ESP32-P4

HackDeck

An I²C and GPIO deck built on the M5Stack Tab5, for interrogating SAO badges and other self-powered I²C targets. It streams structured NDJSON to a Mac over USB-C, with or without an SD card — and it refuses to do the things that destroy targets.

M1 — badge-capable, verified on hardware, screen dark

What it is

A bench tool for the badge table, not a product.

You sit down at a badge table in poor light with someone else's hardware in front of you. You want to know what's on its I²C bus, read its identification descriptor, and watch a wake line — without turning their badge into a souvenir.

HackDeck does that from a serial shell, and writes every observation as one self-framing JSON record per line so the capture survives a dropped byte and can be replayed later. The headless path is the one that ships; the screen is a nicety it doesn't depend on.

Scanner

Sweeps the external bus and reports hits with device hints, distinguishing "no devices" from "no bus at all."

Explorer

Reads registers off a chosen address. 8-bit indices only — 16-bit is not a free upgrade, see below.

GPIO monitor

Watches a line — such as the DC34 badge's open-drain wake pin — which Grove Port.A has no conductor for.

The refusals are the feature

Most of the engineering here is in what it declines to do.

The badge rail is 3.0 V. Grove Port.A supplies 5 V. That mismatch is the single thing most likely to destroy a target, so the harness carries no 5 V conductor at all.

Target facts are confirmed, not guessed: the 3.0 V rail, the devices at 0x3C and 0x19, and GPIO4-as-open-drain-wake all come from DEF CON's published SAO spec sheet, archived in the repo.

Why the harness lives on J9

The decision most likely to trip up someone reading an older draft.

Grove Port.A is HY2.0-4P: GND, 5 V, G53, G54. That is ground plus exactly two usable IOs and no pin for the badge's wake line — which is the entire reason the GPIO monitor exists. Port.A can run Scanner and Explorer, or GPIO work, but not both at once.

So the default build puts external I²C on the J9 "2X5PIN EXT" header on the back, at 0.1" pitch. No boot strap, no reset line, no console UART and no internal I²C anywhere near it — fewer neighbours means fewer ways to destroy something in bad light.

SignalTab5 pinJ9 pin
External SDAGPIO499
External SCLGPIO5010
Wake line / GPIO toolGPIO18
SpareGPIO07
GND—1, 3, 4

Wire format

One newline-terminated JSON object per record, self-framing.

A dropped byte costs at most one record. The host capture tool ignores any line not starting with {, so framework log output in a dev build cannot corrupt a capture.

// a real line off the board — the first confirmed target, 2026-07-30
{"k":"scan_hit","boot":1,"seq":9,"ms":120581,
 "tool":"i2c_scan","res":"ok","addr":"0x28",
 "hint":"MFRC522/WS1850S RFID / BNO055 IMU?",
 "host_ts":"2026-07-30T16:24:37.324826+00:00"}

Known limits

Stated plainly, because a bench tool that overstates itself is worse than none.

← the catalog·thegian7