Add README with setup, usage, and deployment docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
360
README.md
Normal file
360
README.md
Normal 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.
|
||||
Reference in New Issue
Block a user