Add README with setup, usage, and deployment docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 16:24:03 -06:00
parent 0c2392b2b3
commit ace1b3bd27

360
README.md Normal file
View File

@@ -0,0 +1,360 @@
# 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.