diff --git a/README.md b/README.md new file mode 100644 index 0000000..d482fad --- /dev/null +++ b/README.md @@ -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 ' \ + -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 ' \ + -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 ' + +# Check current state +curl http://localhost:8080/api/status \ + -H 'Authorization: Bearer ' +``` + +--- + +## 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://: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 ` 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://:8080/api/message \ + -H 'Authorization: Bearer ' \ + -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://:8080/api/message \ + -H 'Authorization: Bearer ' +``` + +### 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 +``` + +**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 : /opt/sahsa_clock + +# Copy files (from your dev machine): +scp -r . @:/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://: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://: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 # 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.