1 Hardware
archeious edited this page 2026-04-17 19:30:38 -06:00

Hardware

Four analog needles, five annunciator lamps, brushed-aluminium bezel, mounted under the monitor. ESP32 polls the daemon's /usage endpoint at ~1 Hz and updates the cluster.

Monitor-mounted cluster

Cluster layout

Four round gauges in a single bezel, reading left to right:

# Label Metric Scale
1 5H PLAN / Left fuel % of 5h plan window used 0 - 100%
2 TOKENS/MIN / Tach Short-window burn rate 0 - redline
3 THINKING/OUTPUT / Temp Thinking vs output tokens low - high, colour-coded zones
4 CACHE HIT / Boost Cache read as fraction of input 0 - 100%

Annunciator row below the gauges, left to right:

Lamp Colour Condition
MODEL RGB Red for Opus, amber for Sonnet, green for Haiku; reflects the most recent assistant message
HOT Red Tach above redline; optionally flashes
WARN Amber Fuel above 80% or cache-hit-rate below a floor
STALL Blue No telemetry for the configured silence window
IDLE Green Daemon reachable, no current activity (pulses softly)

The 7-day window is not on the cluster. It moves too slowly to warrant a needle and lives on the deep-stats dashboard instead.

Movement

Switec X27.168 automotive stepper motor.

  • 315-degree sweep
  • 600 steps (~2 degrees per step with 3-degree/step gearing)
  • Direct drive, no microstepping required
  • ~$8 per unit from Adafruit / AliExpress
  • Used in car dashboards, so bezels and faces are off-the-shelf

Sibling parts in the same family (X25, VID28, VID29, BKA30D-R5) all work with the same driver library. The X27.168 has the longest sweep and the most tutorial coverage, which is why it wins.

Driver library

clearwater/SwitecX25 Arduino C++ library. Despite the name it covers X27.168 without modification. Drives four GPIO pins per motor. No external driver IC strictly required for short wiring runs; add ULN2003A darlington arrays if you want cleaner current handling or longer wire runs.

No maintained MicroPython port exists. Firmware is therefore Arduino C++, not MicroPython. Not the original plan, but the right trade given how mature the Arduino ecosystem is for this motor.

Board

ESP32 DevKit V1 (generic).

  • WiFi built in
  • Enough GPIO for 3+ steppers (12 pins), 5 annunciator LEDs, one reset button
  • Arduino framework via PlatformIO
  • ~$8 per board

Alternatives:

  • Raspberry Pi Pico W with CircuitPython — fewer tutorials for the stepper, but friendlier to iterate
  • ESP32-S3 if you want USB-CDC for serial debugging without an extra USB-to-UART chip

Picking the classic ESP32 DevKit keeps part count low.

Wiring

Concrete GPIO assignments for a four-gauge cluster. Adjust to suit the specific board variant.

ESP32 DevKit V1
  +---------------- 5V  -> stepper common (via regulator if needed)
  +---------------- GND -> common ground

  Gauge 1 (5h PLAN)
    GPIO 13, 14, 27, 26  -> X27.168 #1 coil pins

  Gauge 2 (Tach)
    GPIO 25, 33, 32, 35  -> X27.168 #2 coil pins

  Gauge 3 (Thinking ratio)
    GPIO 34, 39, 36, 22  -> X27.168 #3 coil pins

  Gauge 4 (Cache hit)
    GPIO 23, 18, 19, 5   -> X27.168 #4 coil pins

  Annunciator LEDs (220R series resistors)
    GPIO 16 (R), 17 (G), 21 (B)  -> MODEL (common-cathode RGB)
    GPIO 4                       -> HOT   (red, PWM for flash)
    GPIO 2                       -> WARN  (amber)
    GPIO 15                      -> STALL (blue)
    GPIO 12                      -> IDLE  (green, PWM for pulse)

  Reset button
    GPIO 0 (boot button dual-use) or GPIO 35 with pull-up

Separate 5V rail for the steppers if you see brownouts when all four move at once. The onboard regulator is marginal under combined motor current.

Firmware structure

firmware/
  platformio.ini
  src/
    main.cpp            setup() + loop()
    wifi.cpp            connect + reconnect
    gauge.cpp           SwitecX25 wrapper; map 0-1000 to 0-600 steps
    annunciator.cpp     LED state machine
    poll.cpp            HTTP GET /usage every 1s
    config.h            daemon URL, thresholds

Poll loop:

  1. Every 1000 ms, HTTP GET http://<daemon>:8080/usage
  2. Parse JSON with ArduinoJson
  3. For each gauge, set target step from scaled value
  4. Update LED states from boolean flags and last_model
  5. gauge.update() runs per-loop-tick; stepper moves toward target non-blocking

Firmware does not interpret: no PromQL, no ring buffers, no rolling windows. That is all daemon work. Firmware maps pre-computed floats to stepper positions and LED states.

Enclosure

V1 target:

  • Brushed-aluminium bezel (3D-printed PLA + metallic spray, upgrade later)
  • Cream faces with a hairline burgundy redline zone (optional house palette match with quartermaster)
  • Hexagonal screws around each gauge bezel (visible in the render)
  • Smoked acrylic covering the annunciator row so LEDs only appear when lit
  • Matte black mounting bracket clipped to the monitor bezel
  • Roughly 200 mm wide by 95 mm tall

Faces and bezels can be printed on a home 3D printer at V1 fidelity; if the project earns a V2, commission a machinist for a proper aluminium bezel.

Bill of materials (V1)

Item Qty ~Cost
Switec X27.168 stepper 4 $32
ESP32 DevKit V1 1 $8
ULN2003A driver array 1-2 $2
RGB LED (common cathode) 1 $1
5 mm LEDs (red, amber, blue, green, red flash) 4 $2
220R resistors 10 $1
5 V 2 A supply 1 $10
PLA filament + spray paint n/a $10
Hook-up wire, headers, JST connectors $10
Smoked acrylic scrap 1 $5

~$80-90 for V1 materials.

Calibration

After the cluster is physically assembled, run the daemon for a week and compare:

  • Left fuel steady-state against ccusage blocks or Claude's /usage output; tune CLAUDE_GAUGE_5H_CEILING
  • Tach peaks against "Claude was really cooking" sessions; tune CLAUDE_GAUGE_TACH_REDLINE
  • Cache hit rate against a session you know was cache-warm vs a cold session; confirm the needle is responsive

Recalibrate when Anthropic changes plan limits or when your daily workload shifts materially.