| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| peek-agent.exe | 2026-09-04 | 8.9 MB | |
| PeekESP.exe | 2026-09-04 | 18.6 MB | |
| PeekESP-merged.bin | 2026-09-04 | 1.3 MB | |
| PeekESP v1.0.0 source code.tar.gz | 2026-09-04 | 2.3 MB | |
| PeekESP v1.0.0 source code.zip | 2026-09-04 | 2.3 MB | |
| README.md | 2026-09-04 | 6.1 kB | |
| Totals: 6 Items | 33.5 MB | 0 | |
A physical system-metrics dashboard. An ESP32 with a 1.14" display shows live CPU, RAM, storage, temperature and network throughput for a machine anywhere on the internet — no port forward, no VPN, no account.
By shouravx · MIT
Setup is one code
:::bash
python quickstart.py
Builds the Windows app and flashes the board. Then the device shows a code:
K7M2-P4QX-9R
Type it into the app. Done — the relay URL, the stream and both tokens are derived from that code on both sides, and the relay never sees it.
Nothing else is typed. No account, no token to copy, no port to forward.
What's in the box
Firmware — LVGL dashboard on a LilyGO TTGO T-Display. Two arcs, a bar, a temperature and throughput panel. Values sweep to new readings over 500 ms rather than snapping; the gauges run on a 0–1000 range so a 3 % change still resolves into a visible sweep instead of a staircase. Boot logo, on-device WiFi setup portal with a QR code to join it, and a status line that names what it is doing.
Cloudflare Worker relay — the host pushes, the device polls, both only ever dial out. Three modes on one deployment: paired (no secrets at all), private (two tokens), shared (named streams for several people). 50 automated tests.
Agents — Linux (dietpi/peek-agent.py) and Windows, both standard-library
only. The Windows build adds a tray app with a settings window, and a headless
peek-agent.exe for running as a service.
Prebuilt firmware — firmware/PeekESP-merged.bin, 1.25 MB. Flashing needs
no Arduino IDE, no ESP32 core and no libraries; only esptool, about 3 MB.
Architecture
Two pinned FreeRTOS tasks sharing nothing but a mutex-guarded struct. Core 0
does WiFi, NTP and the blocking HTTPS request. Core 1 runs lv_timer_handler()
and never opens a socket, so a slow link cannot drop a frame.
The pairing derivation is the same three lines in three languages:
stream = SHA-256("peek-stream:" + CODE) first 16 hex
push = SHA-256("peek-push:" + CODE) first 48 hex
read = SHA-256("peek-read:" + CODE) first 48 hex
C++ on the device, Python in the app, JavaScript in the tests — pinned against
vectors generated independently by openssl, because a drift between any two
would mean the PC and the device derive different streams and never meet, with
every request still looking perfectly valid.
Paired streams authenticate by trust-on-first-use. The guarantee is precisely "once a role is claimed, only that token works" — not "only the right token was ever possible". Squatting an unclaimed stream requires the 16-hex id, which requires the code.
Verified
| Firmware | compiles clean, no warnings, 39 % of a 3 MB partition |
| Worker | 50 unit tests, plus 9 live checks against the real deployment |
| Pairing | full round-trip proven end to end on the deployed Worker |
| Windows app | both executables run; settings window driven by smoke tests |
| CI | deploys the Worker on merge, proven with a real change |
Not verified
The display was brought up on real hardware late, and several things there have been exercised only briefly or not at all: the boot splash, the QR setup portal, the on-device SHA-256 derivation, and the TLS handshake under poor signal. The device-side code compiles and the parts that have run, worked — but this is a 1.0.0, not a soak-tested release.
Known limits
Two paired devices per Worker. Cloudflare's free tier allows 100,000 requests/day; a paired set costs about 35,000. A third pair exceeds it for everyone on that deployment. Raising the poll interval from 5 s to 15 s gets you six.
No temperature on Windows. Windows exposes none without a vendor driver, so
the agent reports -1 and the display shows -- rather than a confident wrong
number.
2.4 GHz only, WPA2. An ESP32 has no 5 GHz radio. A network that does not appear in the scan list is usually 5 GHz-only or WPA3-only rather than a fault.
Re-flashing keeps the pairing code. It lives in NVS and survives a firmware
update, which is correct and surprising the first time you want a new one. Use
python tools/flash.py --erase.
An ESP32 cannot join a Tailscale network. Tailscale is WireGuard plus a control plane — node registration, rotating keys, DERP relays — and none of that has an embedded client. The relay exists because of this.
Notable fixes during development
Several of these were only findable by running the thing, and are worth recording because each looked like something else:
- Watchdog reboot loop. Core 0 is the core allowed to block, so the idle-task
watchdog on it was always contradictory.
WebServer::handleClient()waits up to 5 s for a client that never finishes its request — exactly what captive portal probes do — and that starved IDLE0 at precisely the watchdog timeout. - SSIDs were injected into the setup page unescaped. An SSID is chosen by
whoever owns the access point; a neighbour naming theirs
"><script>…had script execution on the page where the WiFi password is typed. - Cloudflare bans the default
Python-urllibuser agent with error 1010, before the Worker runs. Every push would have failed with an opaque 403 that looks exactly like an auth problem. - A build that succeeded and produced a broken binary. Excluding
emailfrom PyInstaller built cleanly and died at launch, becausehttp.serverimports it. - A build that failed and left the previous binary in place, so the test afterwards passed against stale code.
LV_USE_SPINNERandLV_USE_QRCODEdefault to 0 in LVGL and fail at link time, not compile time.
Thanks
LVGL, TFT_eSPI, ArduinoJson, and LilyGO for the board.