diff --git a/README.md b/README.md index d098fee..3f2b175 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,23 @@ WebSocket servers that feed system stats, alarm audio, and status images to the ## File Structure ``` -pi/ - 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) +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 @@ -48,6 +51,16 @@ 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 @@ -56,8 +69,8 @@ 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` -- `services` (mocked until systemd integration) +- `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 @@ -65,8 +78,8 @@ Pushes a JSON object every 2 seconds with real system metrics from `psutil`: Serves alarm audio and status images. Protocol: **Status image:** -1. Text frame: `{"type":"status_image","width":120,"height":120}` -2. Binary frame: 1-bit monochrome bitmap (1800 bytes) +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}` @@ -108,8 +121,8 @@ Example with two alarms: | `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 `pi/`. Default: `assets/alarm/alarm_test.wav`. | -| `alarm_image` | `string` | No | Status PNG path, relative to `pi/`. Default: `assets/img/on_alarm.png`. | +| `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. @@ -118,12 +131,12 @@ If both `alarm_days` and `alarm_dates` are present, `alarm_days` takes priority. ### 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, returns `(pcm_bytes, sample_rate, channels, bits)` +- `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 120x120 monochrome bitmap (black=1, MSB-first) +- `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 diff --git a/scripts/edit.sh b/scripts/edit.sh new file mode 100755 index 0000000..61ddcf1 --- /dev/null +++ b/scripts/edit.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Open the alarm config in an editor, then restart the service. +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CONFIG="${PROJECT_DIR}/config/alarms.json" + +${EDITOR:-nano} "${CONFIG}" + +echo "==> Restarting pi-dashboard service..." +sudo systemctl restart pi-dashboard + +echo "==> Done. Check status with: systemctl status pi-dashboard" diff --git a/scripts/remove.sh b/scripts/remove.sh new file mode 100755 index 0000000..1b2d553 --- /dev/null +++ b/scripts/remove.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Stop, disable, and remove the pi-dashboard systemd service. +set -euo pipefail + +SERVICE_NAME="pi-dashboard" +UNIT_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + +echo "==> Stopping ${SERVICE_NAME}..." +sudo systemctl stop "${SERVICE_NAME}" || true + +echo "==> Disabling ${SERVICE_NAME}..." +sudo systemctl disable "${SERVICE_NAME}" || true + +echo "==> Removing unit file..." +sudo rm -f "${UNIT_FILE}" + +echo "==> Reloading systemd..." +sudo systemctl daemon-reload + +echo "==> Done. Service removed." diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..a5f531b --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Install dependencies, generate a systemd unit, and enable the pi-dashboard service. +set -euo pipefail + +SERVICE_NAME="pi-dashboard" +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +UNIT_FILE="/etc/systemd/system/${SERVICE_NAME}.service" +RUN_USER="$(whoami)" + +echo "==> Installing Python dependencies..." +uv pip install -r "${PROJECT_DIR}/requirements.txt" + +echo "==> Generating systemd unit file..." +cat > "/tmp/${SERVICE_NAME}.service" < Installing unit file to ${UNIT_FILE}..." +sudo cp "/tmp/${SERVICE_NAME}.service" "${UNIT_FILE}" +rm "/tmp/${SERVICE_NAME}.service" + +echo "==> Reloading systemd and enabling service..." +sudo systemctl daemon-reload +sudo systemctl enable --now "${SERVICE_NAME}" + +echo "==> Done. Check status with: systemctl status ${SERVICE_NAME}"