- Rust 95.6%
- Nix 3.3%
- Shell 1.1%
| .direnv | ||
| src | ||
| .envrc | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
whereAmI
Axum service that drives an ESP32 NeoPixel strip from famtrack locations. The server owns all state — the strip is a dumb renderer that opens a WebSocket and applies whatever LED frames the server pushes.
┌─────── famtrack (locations)
├─────── BigDataCloud (reverse geocode)
▼
ESP32 ◀── wss:// frame push ── whereAmI ◀── browser (web UI, password-gated)
The server polls each device's location every 60s, maps the resolved country to a list of LED positions, renders a sparse frame, and broadcasts it to every connected ESP. Color/brightness/name per device are configurable from the web UI and persisted to disk.
Web UI
https://<host>/ — dashboard with one card per device (color picker,
brightness slider, rename), a country-inspect panel for territory mapping,
a strip-wide test panel, and a "connected strips" indicator.
First visit redirects to /login. Enter the password from WEB_PASSWORD;
the resulting cookie lasts a year and survives server restarts (sessions
are persisted).
ESP endpoint
GET wss://<host>/ws/esp — gated by X-API-Key. The ESP sends two extra
headers on the handshake:
| Header | Example | Purpose |
|---|---|---|
X-Strip-Id |
esp-AABBCCDDEEFF |
Identifier shown in /admin/connected and logs |
X-Device-Mac |
AA:BB:CC:DD:EE:FF |
Raw MAC, logged on connect |
Once upgraded, the server sends the current frame immediately and a new frame every time anything changes (poll result, UI edit, override toggle).
Frame format (server → ESP, JSON text)
{ "type": "frame",
"leds": [ { "pos": 222, "rgb": [255, 128, 0] },
{ "pos": 252, "rgb": [255, 128, 0] } ] }
Sparse: only lit LEDs are included. The ESP clears the strip and applies the
list each frame. Brightness scaling is applied server-side (rgb is the final
value).
API endpoints (X-API-Key)
| Method | Path | Description |
|---|---|---|
GET |
/ws/esp |
ESP WebSocket upgrade |
GET |
/admin/connected |
JSON list of currently connected strip IDs |
GET |
/get/location |
Legacy: returns {"country":"XX"} for {"key":"<device>"}. Kept for transition; not used by new ESP firmware. |
Configuration
All config via env vars. A .env in the working directory is loaded if present.
| Var | Required | Description |
|---|---|---|
API_KEYS |
yes | Comma-separated list of keys accepted as X-API-Key by the ESP and the legacy endpoint. |
WEB_PASSWORD |
yes | Password for the web UI. Empty values rejected — server refuses to start. |
FAMTRACK_BASE_URL |
yes | e.g. https://famtrack.mrfluffy.xyz |
FAMTRACK_TOKEN |
yes | famtrack API token with the locations:read scope. |
FAMTRACK_DEVICES |
yes | device_key=GROUP_UUID/USER_UUID entries, comma-separated. Devices listed here appear on the dashboard. |
STATE_PATH |
no | Where to persist per-device state (default ./device_state.json). |
SESSIONS_PATH |
no | Where to persist web UI sessions (default ./sessions.json). |
CACHE_PATH |
no | Legacy /get/location stale-fallback cache (default ./last_country.json). |
FAMTRACK_DEVICES format
FAMTRACK_DEVICES=device42=11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222,alice-esp=11111111-1111-1111-1111-111111111111/33333333-3333-3333-3333-333333333333
Each device_key is a tracked person, not a strip. One strip displays the
union of all configured devices' locations.
Getting the UUIDs and token
FAMTRACK=https://famtrack.mrfluffy.xyz
# 1. Log in.
JWT=$(curl -s -X POST $FAMTRACK/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"<you>","password":"<pw>"}' | jq -r .token)
# 2. List groups — pick FAMTRACK_GROUP_ID.
curl -s $FAMTRACK/v1/groups -H "Authorization: Bearer $JWT" | jq
# 3. List group members — pick user UUIDs for FAMTRACK_DEVICES.
curl -s $FAMTRACK/v1/groups/$GID/members -H "Authorization: Bearer $JWT" | jq
# 4. Mint a long-lived API token (save the returned token field — only shown once).
curl -s -X POST $FAMTRACK/v1/auth/api-keys \
-H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
-d '{"label":"whereAmI bridge","scopes":["locations:read"]}' | jq
Running
nix develop
cargo run --release
Listens on 0.0.0.0:3000.
Behind nginx
location /ws/esp {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Persistence
Three JSON files written to the working directory by default:
| File | Contents | Surviving a wipe |
|---|---|---|
device_state.json |
Per-device name, color, brightness, last-known country. | UI re-defaults to white/100/no-country; auto-populated again as the poller runs. |
sessions.json |
Active web UI session tokens. | All logged-in browsers get bounced back to /login. |
last_country.json |
Legacy /get/location cache. |
Safe to delete. |
Writes are atomic-ish (write-to-tmp + rename).
ESP client
See /home/mrfluffy/Documents/projects/platformIO/whereAmI-esp32. The ESP no
longer needs to know about devices or countries — it just opens the WS and
renders whatever frames arrive. Strip identity is MAC-derived (esp-<mac>),
so reflashing keeps the same ID.
To add a new tracked person:
- They register in famtrack and join the relevant group.
- Find their user UUID via
GET /v1/groups/$GID/members. - Append
their-key=$GID/$UIDtoFAMTRACK_DEVICESand restart the server. - Their card appears on the dashboard with default color/brightness; tweak there.