- Python 57.3%
- TypeScript 35.4%
- JavaScript 3%
- HTML 1.9%
- CSS 1.6%
- Other 0.8%
|
Some checks failed
ci / version (push) Successful in 3s
ci / icd (push) Successful in 15s
ci / server (push) Successful in 40s
ci / bridge (push) Successful in 22s
ci / e2e (push) Successful in 47s
ci / app (push) Failing after 16s
ci / image (push) Successful in 40s
Producer under bridges/simulator pushes synthetic EV, MCU, plant, radar and lightning channels, plus optional Open-Meteo weather and ISS position, through the ICD-002 values API. Adds make demo-data/test-simulator and runs the simulator tests in CI. |
||
|---|---|---|
| .forgejo/workflows | ||
| app | ||
| assets/brand | ||
| bridges | ||
| deploy/infra | ||
| icd | ||
| server | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| CLAUDE.md | ||
| Makefile | ||
| README.md | ||
Cathode Ray
Live mini dashboards for Even Realities G2 glasses. A small "Grafana for your face": print progress and temperatures from OctoPrint, server health from InfluxDB, all updating in real time.
InfluxDB ─┐ ┌─────────────── phone ───────────────┐
OctoPrint ─┼─▶ Cathode Ray server ══ wss ══▶ │ Even App ▶ WebView: Cathode Ray app │ ──BLE──▶ G2
(more…) ─┘ crt.lucavs.nl └─────────────────────────────────────┘
polls, formats, thresholds layout, input, reconnect
| Folder | What | Owner of |
|---|---|---|
icd/ |
Interface contracts (ICD-001 v1/v2, ICD-002, ICD-003): docs, JSON Schemas, golden examples. The single source of truth. | The contracts |
server/ |
Python/FastAPI container. Talks to data sources, serves the glasses over WebSocket. | What to show |
app/ |
Even Hub app (Vite + TypeScript). Renders on the glasses. | How it fits on 576×288 |
bridges/dmm121gw/ |
EEVblog 121GW Bluetooth reader. Pushes readings to the server. | Talking to the meter |
deploy/infra/ |
Template for the deployment folder in the infra repo. | Running it on Indium |
Rule of the house: a change that crosses a boundary starts in icd/ (document, schema, examples), then
code. Server and app tests read the same golden examples from icd/.
Agents: start with AGENTS.md.
Repositories and CI
Cathode Ray and the homelab infra are two separate repos, and neither contains the other:
| Repo | Contains | Changes when |
|---|---|---|
Cathode-Ray (this one, git.lucavs.nl/lucavanstraaten/Cathode-Ray) |
code, contracts, tests, CI, deployment templates | you build features |
| infra | cathode-ray/: compose, production dashboards.yaml, .env (gitignored), nginx vhost. forgejo-runner/. |
you deploy or reconfigure |
They're connected by a versioned image, not by shared files.
CI runs on Forgejo Actions (.forgejo/workflows/ci.yml). Every push runs:
- ICD checks
- server, bridge and app tests
- an end-to-end test (the app's real client against the real server)
- a build and smoke test of the server image
Tagging v0.2.0 also pushes git.lucavs.nl/lucavanstraaten/cathode-ray-server:0.2.0 to Forgejo's own registry.
Deploying means setting that version in the infra repo and running docker compose pull && docker compose up -d.
Templates:
- runner:
deploy/infra/forgejo-runner/ - deployment:
deploy/infra/cathode-ray/
Where each setting is edited (web page, app, glasses, git): ARCHITECTURE.md.
Multimeter on your glasses
Run the bridge on whichever laptop is next to the meter. It pushes to the server, and the glasses show the 121GW dashboard (big 7-segment digits):
cd bridges/dmm121gw && pip install -r requirements.txt
python -m dmm121gw --server https://crt.lucavs.nl --token <token> # real meter over Bluetooth
python -m dmm121gw --server https://crt.lucavs.nl --token <token> --fake # simulated meter
Fully local: run make dev-init, then make dev-meter PUSH_KEY=<printed producer key> (METER=real for Bluetooth). Point the app at ws://<laptop>:8080/ws/v2 and enter the printed viewer key on the phone. Details: bridges/dmm121gw/README.md.
Live UI Test Data
The producer simulator in bridges/simulator/simulator.py
pushes independent channel groups through the existing values API. No server config is required.
All groups use the authorized testdata prefix: EV inverter/motor/BMS, MCU debug,
industrial pump/PLC, living-room mmWave occupancy, and simulated lightning.
They are synthetic UI fixtures, not hardware measurements or safety information.
The normal run cycles through threshold states, regen, empty rooms and no-data readings.
python3 -m venv bridges/simulator/.venv
bridges/simulator/.venv/bin/pip install -r bridges/simulator/requirements-dev.txt
# For a fresh checkout, populate bridges/simulator/.env using .env.example.
make demo-data
make demo-data DEMO_ARGS="--public" # add weather and ISS
make demo-data DEMO_ARGS="--sources ev,mcu --faults" # MCU drops out: stale then recovery
make demo-data DEMO_ARGS="--extra 180 --interval 0.2 --duration 60" # bounded load test
make demo-data DEMO_ARGS="--dry-run" # inspect payloads without network
The script loads its adjacent, gitignored .env; exported environment variables take precedence.
The tracked bridges/simulator/.env.example contains no key.
Use --server http://127.0.0.1:8080 and an exported local producer key/prefix for a local server.
The default location for weather is Utrecht; set DEMO_LATITUDE/DEMO_LONGITUDE or use CLI options.
--public polls Open-Meteo every five minutes (model-derived current
weather, not a local sensor) and Where the ISS at? every 15 seconds
(orbital tracking). They use a separate, unauthenticated HTTP client. Outages never produce fake
public readings: their channels become stale if refreshes stop. Real lightning needs a suitable
licensed feed; the demo does not scrape a lightning network.
On the glasses, open Live, or use the phone channel picker to assemble quick views from
testdata.ev.*, testdata.mcu.*, etc. Labels distinguish groups. Above 100 channels the server
pages Live into subviews. Each POST contains at most 50 channels, with at most ten POSTs per second;
larger frames automatically take longer. Extra load is capped at 250 channels. Existing server
channels also count toward the global 1,000-channel limit. Ctrl-C stops the producer without
deleting data; channels become stale, then expire under the server's idle-expiry policy. --faults
also intentionally raises MCU staleness alerts (best used with the default one-second interval).
Run focused tests with make test-simulator PY=bridges/simulator/.venv/bin/python.
Quick demo (no infrastructure needed)
pip install -r server/requirements-dev.txt && (cd app && npm install)
make dev # server with a simulated printer on :8080 + the app's dev server on :5173
Run make dev-init before the first make dev; it creates scoped development keys in the gitignored .local-data/ directory and prints their secrets once. Open http://localhost:5173/?server=ws://localhost:8080/ws/v2 and enter the viewer key in the form. Back opens the menu; select opens a view. Keys never go in a URL.
Production serves public docs at /docs/, password-session administration at /admin/, and scoped producer endpoints under /api/v2. The v1 endpoints are removed: deploy the new server and app together. Action execution remains deferred to phase 4.
On the real glasses
See app/README.md: sideload with a QR code while developing, or install the packed .ehpk.
Deploy on Indium
See deploy/infra/cathode-ray/README.md.
Tests
make test # everything below except e2e and the image build, which CI also runs
| Suite | Count | Covers |
|---|---|---|
| ICD | 4 contracts | every schema is valid; golden examples pass, invalid examples fail for the intended rule |
| server | pytest + focused unittest | v2 protocol, producer API, admin sessions, config, formatting, sources, persistence and prefix isolation |
| bridge | 30 | 121GW packet decoding, reassembly from BLE fragments, min/max tracking |
| app | vitest | v2 decoder, menus/paging, reconnect/backoff, layout maths, renderer diffing and hero rendering |
| e2e (CI) | 1 | real v2 client/server: rendering, live updates and quick-view create/delete |
| image (CI) | 1 | the Docker image builds, starts and answers health |
Not covered yet: InfluxDB queries against a real InfluxDB, the nginx config, and anything that needs the glasses or the meter.