5.3 KiB
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 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_pctcpu_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 viadocker ps -a, with a ternary status model (running,warning,stopped). Monitored containers: gitea, samba, pihole, qbittorrent, frpc (ny), pinepods, frpc (ssh), jellyfin.local_timefields for RTC sync (y,mo,d,h,m,s)
contents_server.py -- port 8766
Serves alarm audio and status images. Protocol:
Status image:
- Text frame:
{"type":"status_image","width":200,"height":200} - Binary frame: 1-bit monochrome bitmap (5000 bytes)
Alarm audio:
- Text frame:
{"type":"alarm_start","sample_rate":N,"channels":N,"bits":N} - Binary frames: raw PCM chunks (4096 bytes each, paced at ~90% real-time)
- 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:
[
{
"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 inassets/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 orNoneshould_fire(config)-- checks a single alarm entry against current local time