# KH 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 ``` KH_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 └── KH-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/KH_Clock sudo chown : /opt/KH_Clock # Copy files (from your dev machine): scp -r . @:/opt/KH_Clock/ # On the Le Potato — create a venv and install dependencies: cd /opt/KH_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/KH_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/KH_Clock/KH-clock.service ``` ### 5. Install and start the systemd service ```bash sudo cp /opt/KH_Clock/KH-clock.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now KH-clock ``` **Check that it started:** ```bash sudo systemctl status KH-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 KH-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 KH-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.