Skip to content

About

LAN desktop hardware monitor: macOS FastAPI/WebSocket server streams CPU, disk, and Claude Code usage to an ESP32 (MicroPython) rendering labeled bars on a 128x32 OLED. Pluggable metric providers.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

30 Commits

Folders and files

Repository files navigation

esp32utilsdesks

A LAN desktop hardware monitor: a macOS server streams live system metrics over WebSocket to an ESP32, which renders them as labeled progress bars on a 128x32 SSD1306 OLED sitting on your desk.

Out of the box it shows CPU%, RAM%, and Claude Code weekly usage% (CLDE); a session-usage provider (SESS) is also available. Which metrics are shown is a one-line edit in config.toml — no code changes, no re-flashing. See docs/DEVELOPMENT.md to add a metric of your own.

How it works

┌─────────────────────────────┐        WebSocket, ~1s          ┌──────────────────────────────┐
│  macOS (server/)             │  {"ts": …, "readings": [...]}  │  ESP32 (client/, MicroPython) │
│  FastAPI + psutil + providers│ ──────────────────────────────>│  ws_client → display → widgets│
└─────────────────────────────┘                                 └───────────────┬──────────────┘
                                                                                  │
                                                                                  v
                                                                     128x32 SSD1306 OLED
                                                                     (labeled progress bars)

Each broadcast tick, the server's ProviderRegistry collects one Reading per active provider and sends an ordered JSON list; the ESP32 draws each reading as a bar (percent) or text row, one per display row.

Requirements

Developed and tested on macOS (Apple Silicon). The disk provider is macOS-aware — it reads the APFS Data volume so the percentage matches reality.

Server (your Mac)

Requirement Details
OS macOS
Python 3.12+
Runtime packages fastapi, uvicorn[standard], psutil — server/requirements.txt
Dev/test packages pytest, pytest-asyncio — server/requirements-dev.txt (includes the runtime deps; only needed for the test suite)
Install make setup creates .venv and installs both, plus ampy for flashing
claude CLI Optional — only for the Claude usage (CLDE) bar. Must be installed and authenticated so claude -p /usage succeeds from a terminal.

ESP32 client (hardware)

Requirement Details
Board Any ESP32 flashed with MicroPython (tested on v1.28.0)
Display 128x32 SSD1306 OLED, connected over I2C
Wiring OLED VCC → 3V3, GND → GND, SDA → GPIO21, SCL → GPIO22 (pins configurable in config.toml)
USB / serial A data USB cable. Boards with a CH340 USB-serial chip need ampy to flash on macOS (see step 3).

Flashing tool

Option Notes
ampy (adafruit-ampy) Recommended — works with CH340 boards on macOS
Thonny GUI alternative (see step 3)

Quick start

The Makefile wraps the whole workflow — run make help to list every command:

Command What it does
make setup Create .venv and install the server, dev, and flashing dependencies
make init Copy config.example.toml → config.toml (only if it doesn't exist yet) and fill in this Mac's detected LAN IP
make ip Detect this Mac's LAN IP and write it into config.toml [server] ip
make config Generate client/config.py from config.toml
make run Start the server (0.0.0.0, port from config.toml)
make flash Upload the client files to the ESP32 (auto-detects the serial port)
make test Run the test suite

First run, start to finish:

make setup
make init                  # creates config.toml, with this Mac's LAN IP already filled in
$EDITOR config.toml        # fill in your WiFi SSID + password
make config                # regenerates client/config.py for the board
make run                   # and, in another shell:
make flash

The sections below explain each step in detail.

1. Configure

make init      # copies config.example.toml -> config.toml, and fills in the LAN IP

make init also detects this Mac's LAN IP and writes it into [server] ip for you. If detection fails (this Mac isn't on the network yet), it says so and leaves the value empty — set it yourself, or run make ip once you're connected.

Then edit config.toml:

Setting Value
[display] providers Which metrics to show, top to bottom. Ships as ["cpu", "ram", "claude"]. Available ids: cpu, ram, disk, claude, claude-session. The 128x32 screen fits 3 rows; extra ids are collected but not drawn.
[wifi] ssid / password Your WiFi network. 2.4 GHz only — the ESP32 has no 5 GHz radio.
[server] ip Your Mac's LAN IP, as seen from the ESP32. Normally auto-filled by make init; make ip refreshes it when the address changes. Only set it by hand if detection failed — find it with ipconfig getifaddr en0.
[server] port 8000 by default. make run binds to this too, so the server and the board can't drift apart.
[i2c] sda / scl Default 21/22 — check your board's wiring.

Changing [display] providers only needs a server restart. The other sections live on the device, so they need make config + make flash + a board reset.

Then generate the device config:

make config    # writes client/config.py from config.toml

config.toml is gitignored — it holds your WiFi password and must never be committed. client/config.py is a generated artifact (the ESP32 has no TOML parser): also gitignored, and never edited by hand — change config.toml and re-run make config instead.

2. Run the server

make run

This binds 0.0.0.0 on the [server] port from config.toml and exposes a WebSocket at /ws. To run it manually instead of through the Makefile, do it from the repo root so the server package resolves:

source .venv/bin/activate            # fish: source .venv/bin/activate.fish
python -m uvicorn server.app:app --host 0.0.0.0 --port 8000

3. Flash the ESP32

make flash     # auto-detects the port; override with: make flash PORT=/dev/cu.usbserial-XXXX

Then reset the board (press the EN/RST button, or power-cycle it).

CH340 boards: use ampy, not mpremote. CH340's DTR/RTS auto-reset makes the board reboot every time the serial port opens, which lands mpremote's raw-REPL handshake mid-boot ("could not enter raw repl"). ampy --delay N waits after opening the port so the board finishes booting first — --delay 2 is confirmed working. If your board isn't CH340-based, mpremote works fine.

make flash does exactly this, per file, in dependency order:

# find the port
ls /dev/cu.usbserial-*

PORT=/dev/cu.usbserial-XXXX

for f in config.py wifi.py ws_client.py display.py ssd1306.py widgets.py main.py; do
  ampy --port "$PORT" --delay 2 put "client/$f" "$f"
done

ampy is installed into .venv by make setup; standalone, it's pip install adafruit-ampy.

GUI alternative: if you'd rather not use the CLI, open each client/*.py file in Thonny, select the ESP32 as the interpreter, and "Save copy... > MicroPython device" for each file. client/config.py must be generated by make config first.

4. What you should see

The OLED shows three rows — with the shipped defaults, CPU, RAM, and CLDE (Claude Code weekly usage) — each as a label + progress bar + percentage. The CLDE bar appears a few seconds after boot: it refreshes in the background rather than blocking startup.

Metrics / providers

Five providers ship built-in. Which ones are displayed — and in what order, top to bottom — is the [display] providers list in config.toml:

[display]
providers = ["cpu", "ram", "claude"]

| Id | Measures | Data source | Kind | In the shipped default? | |---|---|---|---|---|---| | cpu | CPU utilization % | psutil.cpu_percent | percent | YES | | ram | RAM utilization % | psutil.virtual_memory().percent | percent | YES | | disk | Disk usage % | psutil.disk_usage (macOS: APFS Data volume; else /) | percent | NO — add "disk" to the list to show it | | claude | Claude Code weekly usage % | claude -p /usage subprocess | percent | YES | | claude-session | Claude Code current-session usage % | claude -p /usage subprocess | percent | NO — add "claude-session" to the list to show it |

To swap the view, edit the list and restart the server. Nothing else changes: the ESP32 renders whatever it is sent, so no make config, no re-flash. An unknown id fails fast at startup with the list of valid ones, rather than silently dropping a metric. Only the first 3 readings fit on the display.

What's in the project

server/              FastAPI app: WebSocket broadcast + the metric providers
client/              MicroPython firmware for the ESP32 (WiFi, WS, OLED)
tools/               config helpers backing `make config` and `make ip`/`make init`
tests/               CPython tests for the client widgets
config.example.toml  tracked config template; `make init` copies it to config.toml
Makefile             setup / init / ip / config / run / flash / test

Each broadcast tick the server collects one reading per active provider and sends an ordered JSON list; the ESP32 draws each reading as a bar (percent) or a text row. See docs/DEVELOPMENT.md for the file-by-file layout.

Development & extending

This project is built to be extended:

  • Pluggable server providers — add a new metric by writing one provider class and registering it in server/providers/selection.py. No ESP32 re-flash needed if it reuses an existing kind.
  • Customizable OLED widgets — the client renders by kind through a widget registry, so new display styles are one function + one entry.

See docs/DEVELOPMENT.md for the architecture, step-by-step "add a provider" and "add a widget" guides, the built-in provider reference, testing, and platform support.

Troubleshooting

Symptom Cause / fix
OLED shows "Server retry..." Wrong [server] ip in config.toml (see "The Mac's IP changed" below), or the server isn't running / isn't reachable on the LAN.
Board joins WiFi but never connects Check [server] port matches on both sides — it does automatically if you started the server with make run.
WiFi connect feels slow Tune POLL_INTERVAL_SECONDS / CONNECT_RETRIES in client/wifi.py.
DISK% looks wrong on macOS Expected on APFS: / is a small read-only system snapshot. The provider already queries /System/Volumes/Data instead — if you still see a mismatch, check that volume exists on your machine.
CLDE bar never appears Requires the claude CLI installed and authenticated (claude -p /usage must succeed from a terminal).
SESS bar never appears Same as CLDE — both read from the same claude -p /usage subprocess. If CLDE works, SESS should too.
make config refuses to run It won't generate a device config from an unfilled template. It names what's wrong: replace the placeholder WiFi values in config.toml, and fill any empty key it lists (make ip fills [server] ip).
A make command says the venv is missing Run make setup — it creates .venv and installs everything.
make flash finds no port Plug the board in, or pass it explicitly: make flash PORT=/dev/cu.usbserial-XXXX.

The Mac's IP changed

The ESP32 is the one that opens the WebSocket, and it reads the server's IP from its own filesystem — so the address is baked into the board at flash time. When your Mac's LAN IP changes, the board keeps dialing the old one:

make ip                    # detect this Mac's LAN IP, write it to config.toml
make config flash          # regenerate client/config.py and push it to the board

Then reset the board. make ip reads the interface backing the default route, so it works on WiFi or Ethernet; override the detection with make ip LAN_IP=192.168.1.50.

There is no way around the USB re-flash, because the board has to be told the new address before it can reach anything. The durable fix is to stop the IP from changing: give your Mac a DHCP reservation (a static lease) on your router — bind its MAC address to a fixed IP — and the value you flash stays valid. make ip is then only for first setup.

License

MIT. client/ssd1306.py is a vendored third-party SSD1306 driver, also MIT-licensed — see the file header for its original attribution.

About

LAN desktop hardware monitor: macOS FastAPI/WebSocket server streams CPU, disk, and Claude Code usage to an ESP32 (MicroPython) rendering labeled bars on a 128x32 OLED. Pluggable metric providers.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages