Files
KH_Clock/README.md

11 KiB
Raw Blame History

KH Clock

A lightweight, boot-persistent clock display for a TV, running on a Libre Computer AML-S905X-CC ("Le Potato"). 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
pip install -r requirements.txt

If you're on Python 3.10 or older, also install tomli:

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:

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)

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 provides one automatically. On older setups you may need 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:

# 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.

[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:

sudo apt install fonts-dejavu

To use a custom font, provide the full path to a .ttf file:

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:

python3 -c "import bcrypt; print(bcrypt.hashpw(b'newpassword', bcrypt.gensalt()).decode())"

Then update config.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:

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.

curl -X DELETE http://<ip>:8080/api/message \
  -H 'Authorization: Bearer <token>'

GET /api/status

Returns the current display state as 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):

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):

sudo usermod -aG video <your-username>

Disable console blanking (prevents the TV from going to sleep):

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:

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

sudo mkdir -p /opt/KH_Clock
sudo chown <your-username>: /opt/KH_Clock

# Copy files (from your dev machine):
scp -r . <user>@<ip>:/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)

sudo apt install fonts-dejavu

4. Configure

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:

nano /opt/KH_Clock/KH-clock.service

5. Install and start the systemd service

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:

sudo systemctl status KH-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 above)
  7. Reboot the device — clock comes back on its own

Troubleshooting

Clock doesn't appear after reboot:

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:

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