Files
KH_Clock/README.md
2026-02-24 16:24:03 -06:00

361 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sahsa Clock
A lightweight, boot-persistent clock display for a TV, running on a [Libre Computer AML-S905X-CC ("Le Potato")](https://libre.computer/products/aml-s905x-cc/). Renders directly to the Linux framebuffer — no desktop environment, no browser, no X11. Staff can push announcements to the screen from any phone or laptop on the same network.
---
## How it works
- **Display**: Python + Pygame renders a full-screen 12-hour clock (`12:00:00 AM`) directly to `/dev/fb0`. No display server required.
- **Messages**: When a message is active, it replaces the clock entirely — large, centered, auto-sized text fills the screen for easy reading from across the room.
- **Dashboard**: A browser-based control panel (password-protected) lets staff type a message, pick a duration, and send it to the screen.
- **API**: A Bearer-token-protected HTTP API accepts webhook calls from other systems.
- **Auto-start**: A systemd service starts the clock at boot without any human intervention.
---
## File structure
```
sahsa_clock/
├── main.py # Entry point
├── display.py # Pygame rendering (clock + messages)
├── server.py # aiohttp web server (dashboard + API)
├── state.py # Thread-safe shared message state
├── config.py # Config loading and saving
├── config.toml # Your configuration (edit this)
├── dashboard/
│ ├── index.html # Dashboard UI
│ ├── style.css
│ └── app.js
├── requirements.txt
└── sahsa-clock.service # systemd unit file
```
---
## Requirements
- Python 3.11+
- pip packages: `pygame`, `aiohttp`, `bcrypt`
```bash
pip install -r requirements.txt
```
> If you're on Python 3.10 or older, also install `tomli`:
> ```bash
> pip install tomli
> ```
---
## Local development and testing
You do not need the Le Potato to develop or test. The server and dashboard work on any machine; the display can be tested in a window.
### Test the server and dashboard (no display needed)
This works on any machine, including Windows/WSL, with no pygame or display required:
```bash
pip install aiohttp bcrypt
python3 main.py --no-display
```
Open `http://localhost:8080` in a browser. You will be directed to the **first-run setup wizard** to create a dashboard password. After that you can use the full dashboard.
The API bearer token is auto-generated on first run, printed to the terminal, and saved to `config.toml`.
### Test the display in a window (requires a graphical environment)
```bash
pip install -r requirements.txt
python3 main.py --dev
```
This opens a 1280×720 window showing the clock. Use it alongside the server to test message rendering. Press **Escape** to close the window.
> **WSL2 note**: `--dev` mode requires a display server. On Windows 11 with WSL2, [WSLg](https://github.com/microsoft/wslg) provides one automatically. On older setups you may need [VcXsrv](https://sourceforge.net/projects/vcxsrv/) and `export DISPLAY=:0` before running. If you can't get a display working locally, `--no-display` is sufficient to develop and test everything except the visual rendering.
### Test the API with curl
After starting the server (with or without `--no-display`), copy the token from `config.toml` and run:
```bash
# Send a message for 30 seconds
curl -X POST http://localhost:8080/api/message \
-H 'Authorization: Bearer <your-token>' \
-H 'Content-Type: application/json' \
-d '{"text": "Service starts in 5 minutes", "duration": 30}'
# Send a persistent message (stays until cleared)
curl -X POST http://localhost:8080/api/message \
-H 'Authorization: Bearer <your-token>' \
-H 'Content-Type: application/json' \
-d '{"text": "Please move to the fellowship hall", "persist": true}'
# Clear the message
curl -X DELETE http://localhost:8080/api/message \
-H 'Authorization: Bearer <your-token>'
# Check current state
curl http://localhost:8080/api/status \
-H 'Authorization: Bearer <your-token>'
```
---
## Configuration
Edit `config.toml` before deploying. All settings have sensible defaults and are documented inline.
```toml
[display]
# Uncomment to fix resolution instead of auto-detecting:
# width = 1920
# height = 1080
fps = 10 # 10 FPS is plenty for a clock
clock_font_path = "" # Leave blank to auto-detect a system monospace font
message_font_path = "" # Leave blank to auto-detect a system sans-serif font
[server]
port = 8080
default_duration_seconds = 20 # Used when a caller doesn't specify a duration
[api]
token = "" # Leave blank — auto-generated on first run and saved here
[rate_limit]
requests_per_minute = 20 # Per source IP, applies to /api/* only
[dashboard]
password_hash = "" # Leave blank — set via the first-run setup wizard
session_timeout_hours = 8
```
### Font notes
On Debian, the auto-detection checks for DejaVu, Liberation, FreeFont, and Noto families. If none are found, pygame's built-in bitmap font is used as a fallback (functional but less sharp at large sizes).
To install good fonts on the Le Potato:
```bash
sudo apt install fonts-dejavu
```
To use a custom font, provide the full path to a `.ttf` file:
```toml
clock_font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"
message_font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
```
---
## Dashboard usage
Open `http://<device-ip>:8080` in any browser on the same network.
- **First visit**: You'll be directed to a one-time setup page to create a password. This is saved as a bcrypt hash in `config.toml` — the plaintext password is never stored.
- **After login**: The dashboard has a message text area, a duration picker, and two buttons.
- **Send to Screen**: Displays the message immediately. The clock disappears; the message fills the screen.
- **Clear Screen**: Removes the message and returns to the clock.
- The **status bar** at the bottom shows what is currently displayed and updates every 5 seconds.
- Sessions expire after 8 hours of inactivity (configurable).
To change the dashboard password later, generate a new hash and paste it into `config.toml`:
```bash
python3 -c "import bcrypt; print(bcrypt.hashpw(b'newpassword', bcrypt.gensalt()).decode())"
```
Then update `config.toml`:
```toml
[dashboard]
password_hash = "$2b$12$..."
```
And restart the service.
---
## API reference
All `/api/*` routes require a `Authorization: Bearer <token>` header. The token is in `config.toml` under `[api] token`.
### POST /api/message
Send a message to the screen.
**Body** (JSON):
| Field | Type | Description |
|---|---|---|
| `text` | string | Required. The message to display. |
| `duration` | number | Seconds to display. `0` means persistent. |
| `persist` | boolean | `true` means display until cleared. |
If neither `duration` nor `persist` is provided, `default_duration_seconds` from `config.toml` is used.
**Example:**
```bash
curl -X POST http://<ip>:8080/api/message \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"text": "Doors open in 10 minutes", "duration": 60}'
```
### DELETE /api/message
Clear the current message.
```bash
curl -X DELETE http://<ip>:8080/api/message \
-H 'Authorization: Bearer <token>'
```
### GET /api/status
Returns the current display state as JSON.
```json
{
"active": true,
"text": "Doors open in 10 minutes",
"remaining_seconds": 42.3,
"persistent": false
}
```
**Rate limiting**: `/api/*` routes are limited to `requests_per_minute` per source IP (default: 20). Excess requests receive `429 Too Many Requests`.
---
## Deploying to the Le Potato
### 1. Prepare the system (one time only)
SSH into the Le Potato from another machine. You won't need a monitor for this step.
**Switch to headless boot** (if a desktop is currently configured):
```bash
systemctl get-default # returns graphical.target? then continue
sudo systemctl set-default multi-user.target
sudo systemctl disable gdm3 # or lightdm — run both, the wrong one is harmless
```
**Add your user to the video group** (needed to write to `/dev/fb0`):
```bash
sudo usermod -aG video <your-username>
```
**Disable console blanking** (prevents the TV from going to sleep):
```bash
sudo nano /etc/default/grub
# Find GRUB_CMDLINE_LINUX_DEFAULT and append: consoleblank=0
sudo update-grub
```
> If the Le Potato uses U-Boot/extlinux instead of GRUB, edit `/boot/extlinux/extlinux.conf` and append `consoleblank=0` to the `APPEND` line instead.
**Reboot and verify:**
```bash
sudo reboot
# After reconnecting:
systemctl get-default # should print multi-user.target
ls -la /dev/fb0 # should exist, group should be 'video'
```
### 2. Install the application
```bash
sudo mkdir -p /opt/sahsa_clock
sudo chown <your-username>: /opt/sahsa_clock
# Copy files (from your dev machine):
scp -r . <user>@<ip>:/opt/sahsa_clock/
# On the Le Potato — create a venv and install dependencies:
cd /opt/sahsa_clock
python3 -m venv venv
venv/bin/pip install -r requirements.txt
```
### 3. Install fonts (if not already present)
```bash
sudo apt install fonts-dejavu
```
### 4. Configure
```bash
nano /opt/sahsa_clock/config.toml
```
You only need to verify the `[server]` port and `[rate_limit]` values. Everything else is handled automatically on first run (token generation, password setup).
If the username on your device is not `pi`, open the service file and update the `User=` line:
```bash
nano /opt/sahsa_clock/sahsa-clock.service
```
### 5. Install and start the systemd service
```bash
sudo cp /opt/sahsa_clock/sahsa-clock.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now sahsa-clock
```
**Check that it started:**
```bash
sudo systemctl status sahsa-clock
```
The TV should now show the clock. Open `http://<device-ip>:8080` from a laptop or phone on the same network to reach the dashboard.
### 6. Verify end-to-end
1. Clock appears on the TV at boot — no keyboard/mouse/monitor needed
2. Dashboard loads at `http://<device-ip>:8080`
3. Type a message, pick a duration, press **Send to Screen** — message appears
4. **Clear Screen** returns to the clock
5. Status bar reflects the current state within 5 seconds
6. `curl` API test works (see [Test the API](#test-the-api-with-curl) above)
7. Reboot the device — clock comes back on its own
---
## Troubleshooting
**Clock doesn't appear after reboot:**
```bash
sudo journalctl -u sahsa-clock -n 50
```
Common causes: wrong `User=` in the service file, user not in the `video` group, `/dev/fb0` not present.
**`/dev/fb0` permission denied:**
```bash
groups # check 'video' is listed
sudo usermod -aG video <user> # add if missing, then log out and back in
```
**Framebuffer not found (`/dev/fb0` doesn't exist):**
The desktop environment may still be running. Confirm `systemctl get-default` returns `multi-user.target` and that the display manager is disabled.
**Dashboard unreachable:**
Check the service is running (`systemctl status sahsa-clock`) and that `port = 8080` in `config.toml` isn't blocked by a firewall.
**Font looks pixelated:**
DejaVu fonts aren't installed. Run `sudo apt install fonts-dejavu` on the device and restart the service.