# Pi Dashboard Servers WebSocket servers that feed system stats, alarm audio, and status images to the ESP32-S3 RLCD dashboard. ## File Structure ``` run_all.py # Launches both servers as child processes stats_server.py # Real system stats over WebSocket (port 8765) contents_server.py # Alarm audio + status images over WebSocket (port 8766) mock_server.py # Drop-in replacement for stats_server with random data audio_handler.py # WAV loading, PCM chunking, alarm streaming image_handler.py # PNG to 1-bit monochrome conversion, alpha compositing alarm_scheduler.py # Loads and validates alarm config, checks firing schedule requirements.txt config/ alarms.json # Alarm schedule configuration assets/ alarm/ # WAV files for alarm audio img/ # Status images (idle.png, on_alarm.png, sleep.png) scripts/ setup.sh # Install deps + create and enable systemd service edit.sh # Edit alarm config and restart service remove.sh # Stop, disable, and remove systemd service ``` ## Requirements Python 3.10+ ``` pip install -r requirements.txt ``` Dependencies: `websockets`, `psutil`, `Pillow` ## Running Start both servers: ``` python run_all.py # both servers, default config python run_all.py --config path/to.json # both servers, custom config ``` Or run individually: ``` python stats_server.py # port 8765 only python contents_server.py --config path/to.json # port 8766, custom config python mock_server.py # port 8765, random data (no psutil needed) ``` ### Running as a systemd service Use the helper scripts in `scripts/` to manage a `pi-dashboard` systemd service: ```bash bash scripts/setup.sh # install deps, create + enable service bash scripts/edit.sh # edit alarm config, restart service bash scripts/remove.sh # stop + remove service ``` ## Servers ### stats_server.py -- port 8765 Pushes a JSON object every 2 seconds with real system metrics from `psutil`: - `cpu_pct`, `mem_pct`, `mem_used_mb`, `disk_pct` - `cpu_temp` (reads `/sys/class/thermal/` as fallback) - `uptime_hrs`, `net_rx_kbps`, `net_tx_kbps` (values are in kB/s despite the field names) - `services` — live Docker container statuses via `docker ps -a`, with a ternary status model (`running`, `warning`, `stopped`). Monitored containers: gitea, samba, pihole, qbittorrent, frpc (ny), pinepods, frpc (ssh), jellyfin. - `local_time` fields for RTC sync (`y`, `mo`, `d`, `h`, `m`, `s`) ### contents_server.py -- port 8766 Serves alarm audio and status images. Protocol: **Status image:** 1. Text frame: `{"type":"status_image","width":200,"height":200}` 2. Binary frame: 1-bit monochrome bitmap (5000 bytes) **Alarm audio:** 1. Text frame: `{"type":"alarm_start","sample_rate":N,"channels":N,"bits":N}` 2. Binary frames: raw PCM chunks (4096 bytes each, paced at ~90% real-time) 3. Text frame: `{"type":"alarm_stop"}` Loads alarm config from `config/alarms.json` (override with `--config`). Checks schedule every 5 seconds, fires once per matched minute. If no config or empty config, sends idle image and blocks forever. On alarm: switches to alarm image, streams audio, switches back to idle. ### mock_server.py -- port 8765 Same JSON schema and 2-second push interval as `stats_server.py`, but all values are randomized. No `psutil` dependency -- useful for development on non-Pi machines. Does not include `local_time` fields. ## Alarm Configuration Config file: `config/alarms.json` -- a single alarm object or an array of alarm objects. Example with two alarms: ```json [ { "alarm_time": "0730", "alarm_days": ["Mon", "Tue", "Wed", "Thu", "Fri"], "alarm_audio": "assets/alarm/alarm_test.wav", "alarm_image": "assets/img/on_alarm.png" }, { "alarm_time": "2300", "alarm_audio": "assets/alarm/sleep.wav", "alarm_image": "assets/img/sleep.png" } ] ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `alarm_time` | `string` | Yes | 4-digit HHMM, 24-hour. Fires on the matched minute. | | `alarm_days` | `string[]` | No | 3-letter abbreviations: `Mon`–`Sun`. If omitted, fires every day. | | `alarm_dates` | `string[]` | No | `MM/DD` strings. Ignored if `alarm_days` is also set. | | `alarm_audio` | `string` | No | WAV path, relative to project root. Default: `assets/alarm/alarm_test.wav`. | | `alarm_image` | `string` | No | Status PNG path, relative to project root. Default: `assets/img/on_alarm.png`. | If both `alarm_days` and `alarm_dates` are present, `alarm_days` takes priority. ## Modules ### audio_handler.py - `find_wav(path=None)` -- uses the given path if it exists, otherwise falls back to glob in `assets/alarm/` - `read_wav(path)` -- reads WAV, normalizes audio to 0 dBFS, returns `(pcm_bytes, sample_rate, channels, bits)` - `stream_alarm(ws, pcm, sr, ch, bits)` -- streams one alarm cycle over WebSocket ### image_handler.py - `load_status_image(path)` -- loads PNG, composites transparency onto white, converts to 1-bit 200x200 monochrome bitmap (black=1, MSB-first) - `send_status_image(ws, img_bytes)` -- sends status image header + binary over WebSocket ### alarm_scheduler.py - `load_config(path)` -- reads and validates alarm JSON; returns list of alarm dicts or `None` - `should_fire(config)` -- checks a single alarm entry against current local time