Compare commits

..

2 Commits

Author SHA1 Message Date
e05773450a proper uv management 2026-02-17 12:18:13 +09:00
af7fb2beaf systemd management scripts 2026-02-17 12:13:39 +09:00
6 changed files with 122 additions and 22 deletions

4
.gitignore vendored
View File

@@ -3,6 +3,10 @@
__pycache__/ __pycache__/
*.pyo *.pyo
*.pyc *.pyc
.venv/
# sacrificial
uv.lock
# configs # configs
config/ config/

View File

@@ -5,20 +5,23 @@ WebSocket servers that feed system stats, alarm audio, and status images to the
## File Structure ## File Structure
``` ```
pi/ run_all.py # Launches both servers as child processes
run_all.py # Launches both servers as child processes stats_server.py # Real system stats over WebSocket (port 8765)
stats_server.py # Real system stats over WebSocket (port 8765) contents_server.py # Alarm audio + status images over WebSocket (port 8766)
contents_server.py # Alarm audio + status images over WebSocket (port 8766) mock_server.py # Drop-in replacement for stats_server with random data
mock_server.py # Drop-in replacement for stats_server with random data audio_handler.py # WAV loading, PCM chunking, alarm streaming
audio_handler.py # WAV loading, PCM chunking, alarm streaming image_handler.py # PNG to 1-bit monochrome conversion, alpha compositing
image_handler.py # PNG to 1-bit monochrome conversion, alpha compositing alarm_scheduler.py # Loads and validates alarm config, checks firing schedule
alarm_scheduler.py # Loads and validates alarm config, checks firing schedule requirements.txt
requirements.txt config/
config/ alarms.json # Alarm schedule configuration
alarms.json # Alarm schedule configuration assets/
assets/ alarm/ # WAV files for alarm audio
alarm/ # WAV files for alarm audio img/ # Status images (idle.png, on_alarm.png, sleep.png)
img/ # Status images (idle.png, on_alarm.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 ## 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) 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 ## Servers
### stats_server.py -- port 8765 ### 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_pct`, `mem_pct`, `mem_used_mb`, `disk_pct`
- `cpu_temp` (reads `/sys/class/thermal/` as fallback) - `cpu_temp` (reads `/sys/class/thermal/` as fallback)
- `uptime_hrs`, `net_rx_kbps`, `net_tx_kbps` - `uptime_hrs`, `net_rx_kbps`, `net_tx_kbps` (values are in kB/s despite the field names)
- `services` (mocked until systemd integration) - `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`) - `local_time` fields for RTC sync (`y`, `mo`, `d`, `h`, `m`, `s`)
### contents_server.py -- port 8766 ### 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: Serves alarm audio and status images. Protocol:
**Status image:** **Status image:**
1. Text frame: `{"type":"status_image","width":120,"height":120}` 1. Text frame: `{"type":"status_image","width":200,"height":200}`
2. Binary frame: 1-bit monochrome bitmap (1800 bytes) 2. Binary frame: 1-bit monochrome bitmap (5000 bytes)
**Alarm audio:** **Alarm audio:**
1. Text frame: `{"type":"alarm_start","sample_rate":N,"channels":N,"bits":N}` 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_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_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_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_audio` | `string` | No | WAV path, relative to project root. Default: `assets/alarm/alarm_test.wav`. |
| `alarm_image` | `string` | No | Status PNG path, relative to `pi/`. Default: `assets/img/on_alarm.png`. | | `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. 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 ### audio_handler.py
- `find_wav(path=None)` -- uses the given path if it exists, otherwise falls back to glob in `assets/alarm/` - `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 - `stream_alarm(ws, pcm, sr, ch, bits)` -- streams one alarm cycle over WebSocket
### image_handler.py ### 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 - `send_status_image(ws, img_bytes)` -- sends status image header + binary over WebSocket
### alarm_scheduler.py ### alarm_scheduler.py

10
pyproject.toml Normal file
View File

@@ -0,0 +1,10 @@
[project]
name = "pi-dashboard-server"
version = "0.1.0"
description = "WebSocket servers for the ESP32-S3 RLCD dashboard"
requires-python = ">=3.10"
dependencies = [
"websockets>=12.0",
"psutil>=5.9.0",
"Pillow>=10.0",
]

13
scripts/edit.sh Executable file
View File

@@ -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"

20
scripts/remove.sh Executable file
View File

@@ -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."

40
scripts/setup.sh Executable file
View File

@@ -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 "==> Syncing Python dependencies..."
uv sync --project "${PROJECT_DIR}"
echo "==> Generating systemd unit file..."
cat > "/tmp/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=Pi Dashboard WebSocket Servers
After=network-online.target docker.service
Wants=network-online.target docker.service
[Service]
Type=simple
User=${RUN_USER}
WorkingDirectory=${PROJECT_DIR}
ExecStart=$(command -v uv) run python run_all.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
echo "==> 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}"