Post
Presto Cockpit turns a Pimoroni Presto into a home-lab dashboard
A Pimoroni Presto touchscreen as a desk-side home-lab dashboard: ten swipeable screens, two optional backends for a GPU box and an agent gateway, and rear LEDs that match the screen. The parts, the wiring, the setup, and what broke.
6 min read
A home lab produces a lot of state and shows almost none of it. The GPU box has a temperature, the inference server has a tokens-per-second figure, the agents have sessions open, and all of it lives in terminals and browser tabs. Presto Cockpit is my answer: a Pimoroni Presto touchscreen running MicroPython, a compact Python for microcontrollers, on the desk, showing one thing at a time.
It has ten swipeable screens, and seven rear RGB LEDs glow the accent color of whichever screen is showing. Two small optional backends feed the lab screens over WiFi, polled every 1 to 2 seconds. Run one, both, or neither: Crypto, Environment, Clock, News and Random need no backend.
The hardware: one Presto and two plug-in boards
The Presto is the only requirement: a 4 inch 480x480 capacitive touchscreen on an RP2350B, with a 7-zone RGB backlight, 2.4 GHz WiFi, microSD, a piezo and USB-C, about 60 GBP standalone or about 90 GBP as a Starter Kit with stand and cables. Its Qw/ST connector, Pimoroni's name for the Qwiic and STEMMA QT flavor of the two-wire I2C bus, means sensors plug in with a cable and no soldering.
The Multi-Sensor Stick (PIM745, about 12 GBP) adds a BME280 for temperature, humidity and pressure at I2C address 0x76, an LTR559 light sensor at 0x23 and an LSM6DS3 inertial unit (IMU); it feeds the Environment screen, LED auto-dim, shake input and the Random screen's entropy. The Qw/ST Pad (about 10 GBP) is an 8-button I2C gamepad, expected at address 0x21. Touch works without it, but the pad earns its place when touch sulks. Chain Presto to stick to pad.
Ten screens, one swipe apart
Ten screens are in the default rotation, plus two kept in the tree but out of SCREENS_ORDER: an altitude and artificial-horizon screen, and a lights screen. Both slowed the refresh and were dropped; a dashboard that stutters is worse than one that shows less. Navigate by swipe or the pad's L and R; U and D move within a screen, A selects or rolls.
| Screen | What it shows | Feed |
|---|---|---|
| DGX | GPU temperature, utilization, power, CPU, RAM, containers, tokens per second per model | dgx-vitals |
| OpenClaw | Per-agent generation rate, sessions, context, on-call state, 7d/30d/1y token ledger | openclaw-shim |
| Resources | CPU and RAM donuts, top processes | openclaw-shim |
| Kuma | A dot per monitor, response times, N/M up or X DOWN header | Uptime Kuma, API key |
| News | Hacker News front page, filtered | none |
| Crypto | Bitcoin, ethereum, monero, solana in USD, from CoinGecko | none |
| Environment | Temperature, humidity, pressure, lux | sensor stick |
| Clock | Word clock in the QlockTwo style, NTP-synced | none |
| Random | True-random coin, d6, d20, 2d6; roll by pad, tap or shake | sensor stick |
| Settings | Feed status, brightness, auto-dim toggle | none |
The Random screen is not random(): results come from an entropy pool stirred from raw IMU, barometer and light noise plus the microsecond timing of each roll.
Two optional backends
dgx-vitals runs on the GPU box: a small FastAPI web service in a python:3.12-slim container started with --network host, --pid=host, --gpus all for NVML (NVIDIA's GPU management library) and the Docker socket mounted read-only. One GET /vitals on port 9876 returns { host, uptime_s, gpu[], cpu, mem, net, containers[], models[] }. Per-model tokens per second come from the vLLM-style Prometheus /metrics endpoints (plain-text counters) listed in VLLM_METRICS_URLS, which is how it reads the Spark vLLM image and the ASR server. Tested on an NVIDIA DGX Spark, it still serves CPU, memory and containers without a GPU.
Code, shellscp -r dgx-vitals <user>@<gpu-host>:~/
ssh <user>@<gpu-host>
cd ~/dgx-vitals
export VLLM_METRICS_URLS="http://localhost:8000/metrics,http://localhost:8001/metrics"
./run.sh
curl -s http://localhost:9876/vitals | python3 -m json.toolopenclaw-shim matters only if you run OpenClaw, an agent gateway. The shim shells out to the openclaw CLI, digests the output, scans /proc for host load, and asks each agent's voice-call sidecar (Matrix VoIP agent) whether it is on a call. It is stdlib-only Python under a systemd user unit, and GET /agents on port 9787 always answers from cache, preferring stale-but-present data to a blank screen.
Code, shellscp -r openclaw-shim <user>@<gateway-host>:~/openclaw-shim
cp openclaw-shim.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now openclaw-shim.service
curl -s http://localhost:9787/agents | python3 -m json.tool | head -40The scar is the CPU quota. At 50 percent of a core the CLI wait loop was starved, the sessions list call of about 18 seconds was misreported as a timeout, and the agent list went blank. The unit ships at CPUQuota=400%; never go below 200 percent.
Flashing and the LED accent sync
One .env on the dev machine drives everything: WiFi, the backend URLs, poll intervals, the crypto basket, LED brightness and the sea-level pressure for the barometer (1013.25 hPa by default). gen-secrets.sh turns it into secrets.json, and deploy.sh finds the Presto over USB-C, pushes the files and soft-resets it. On boot the device reads secrets.json, joins WiFi, syncs NTP and polls each URL on its own interval: 1.0 s for the GPU box, 2.0 s for the gateway, 10 s for Kuma, 30 s for crypto. Both files are git-ignored; the WiFi password sits in plain text on the device.
Code, shellpip3 install --user mpremote
cp .env.example .env
./deploy.sh
mpremote connect <dev-port> run tools/boottest.pyThe 7 rear LEDs take the current screen's accent color. Brightness defaults to 100 percent, adjustable from 0.1 to 1.0 in Settings and persisted to secrets.json. An auto-dim option reads the LTR559 light sensor and ships off, because an unreliable sensor makes the LEDs flicker.
Things that will go wrong
- Touch dies: taps are ignored, the I2C scan is empty, or every read from the FT6236 touch controller raises OSError 5. Usually the controller or flex cable is wedged; the cure is a full power cycle (unplug USB-C), not a soft reset. The pad still navigates meanwhile.
- WiFi will not join: the radio is 2.4 GHz only, so a 5 GHz-only access point is invisible. Check the SSID and the two-letter country code.
- The Environment screen is empty. The stick was not detected; reseat the Qw/ST cable and check the address.
- The DGX screen shows ECONNRESET. The container is down (
docker start dgx-vitals) or port 9876 is firewalled.
AGENTS.md holds the full deployment, written so an AI agent can carry it out, plus a verification checklist; the rest of the lab's tools are in the catalog.