Named correctly, and fixed to work with Le Potato

This commit is contained in:
2026-02-24 23:46:36 -06:00
parent d2c8079231
commit 80697c7da5
8 changed files with 40 additions and 42 deletions

View File

@@ -1,24 +1,23 @@
[Unit] [Unit]
Description=Sahsa Clock Display Description=KH Clock Display
After=network.target After=network.target
DefaultDependencies=no DefaultDependencies=no
[Service] [Service]
Type=simple Type=simple
User=pi User=pi
WorkingDirectory=/opt/sahsa_clock WorkingDirectory=/opt/KH_Clock
# Tell SDL to render directly to the Linux framebuffer (no display server needed) # Tell SDL to render via KMS/DRM (required on Le Potato and similar ARM boards)
Environment=SDL_VIDEODRIVER=fbcon Environment=SDL_VIDEODRIVER=kmsdrm
Environment=SDL_FBDEV=/dev/fb0
# Disable console blanking so the TV stays on # Disable console blanking so the TV stays on
ExecStartPre=/bin/sh -c 'echo -ne "\033[9;0]" > /dev/tty1' ExecStartPre=/bin/sh -c 'echo -ne "\033[9;0]" > /dev/tty1'
# If using a virtualenv (recommended): # If using a virtualenv (recommended):
ExecStart=/opt/sahsa_clock/venv/bin/python3 /opt/sahsa_clock/main.py ExecStart=/opt/KH_Clock/venv/bin/python3 /opt/KH_Clock/main.py
# If using system Python instead, replace the line above with: # If using system Python instead, replace the line above with:
# ExecStart=/usr/bin/python3 /opt/sahsa_clock/main.py # ExecStart=/usr/bin/python3 /opt/KH_Clock/main.py
Restart=always Restart=always
RestartSec=5 RestartSec=5

14
PLAN.md
View File

@@ -1,4 +1,4 @@
# Plan: Sahsa Clock — Lightweight Boot-Persistent Display with Webhook Support # Plan: KH Clock — Lightweight Boot-Persistent Display with Webhook Support
## Context ## Context
@@ -27,7 +27,7 @@ Pygame can render directly to `/dev/fb0` (the Linux framebuffer) without any dis
## Architecture ## Architecture
``` ```
sahsa_clock/ KH_Clock/
├── main.py # Entry point: asyncio event loop + pygame render loop ├── main.py # Entry point: asyncio event loop + pygame render loop
├── display.py # Pygame rendering: clock face, message overlay ├── display.py # Pygame rendering: clock face, message overlay
├── server.py # aiohttp server: dashboard routes + API routes ├── server.py # aiohttp server: dashboard routes + API routes
@@ -37,7 +37,7 @@ sahsa_clock/
│ └── app.js │ └── app.js
├── config.toml # Screen resolution, colors, fonts, port, token, timeout ├── config.toml # Screen resolution, colors, fonts, port, token, timeout
├── requirements.txt ├── requirements.txt
└── sahsa-clock.service # systemd unit file └── KH-clock.service # systemd unit file
``` ```
### Runtime flow ### Runtime flow
@@ -117,11 +117,11 @@ session_timeout_hours = 8
**No TLS required** — LAN-only. If ever internet-facing, add nginx in front with TLS. **No TLS required** — LAN-only. If ever internet-facing, add nginx in front with TLS.
### systemd service (`/etc/systemd/system/sahsa-clock.service`) ### systemd service (`/etc/systemd/system/KH-clock.service`)
```ini ```ini
[Unit] [Unit]
Description=Sahsa Clock Display Description=KH Clock Display
After=network.target After=network.target
DefaultDependencies=no DefaultDependencies=no
@@ -130,7 +130,7 @@ Type=simple
User=pi # or whatever the device user is User=pi # or whatever the device user is
Environment=SDL_VIDEODRIVER=fbcon Environment=SDL_VIDEODRIVER=fbcon
Environment=SDL_FBDEV=/dev/fb0 Environment=SDL_FBDEV=/dev/fb0
ExecStart=/usr/bin/python3 /opt/sahsa_clock/main.py ExecStart=/usr/bin/python3 /opt/KH_Clock/main.py
Restart=always Restart=always
RestartSec=5 RestartSec=5
@@ -259,4 +259,4 @@ The TV should show a plain text console at this point — that's expected. The c
5. Status bar in dashboard reflects the current message within 5 seconds 5. Status bar in dashboard reflects the current message within 5 seconds
6. `curl -X POST http://<device-ip>:8080/api/message -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' -d '{"text":"API test", "duration": 30}'` — message appears 6. `curl -X POST http://<device-ip>:8080/api/message -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' -d '{"text":"API test", "duration": 30}'` — message appears
7. Same curl without the header → `401 Unauthorized` 7. Same curl without the header → `401 Unauthorized`
8. `sudo systemctl enable --now sahsa-clock` → service starts, reboot device, clock appears without intervention 8. `sudo systemctl enable --now KH-clock` → service starts, reboot device, clock appears without intervention

View File

@@ -1,4 +1,4 @@
# Sahsa Clock # 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. 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.
@@ -17,7 +17,7 @@ A lightweight, boot-persistent clock display for a TV, running on a [Libre Compu
## File structure ## File structure
``` ```
sahsa_clock/ KH_Clock/
├── main.py # Entry point ├── main.py # Entry point
├── display.py # Pygame rendering (clock + messages) ├── display.py # Pygame rendering (clock + messages)
├── server.py # aiohttp web server (dashboard + API) ├── server.py # aiohttp web server (dashboard + API)
@@ -29,7 +29,7 @@ sahsa_clock/
│ ├── style.css │ ├── style.css
│ └── app.js │ └── app.js
├── requirements.txt ├── requirements.txt
└── sahsa-clock.service # systemd unit file └── KH-clock.service # systemd unit file
``` ```
--- ---
@@ -276,14 +276,14 @@ ls -la /dev/fb0 # should exist, group should be 'video'
### 2. Install the application ### 2. Install the application
```bash ```bash
sudo mkdir -p /opt/sahsa_clock sudo mkdir -p /opt/KH_Clock
sudo chown <your-username>: /opt/sahsa_clock sudo chown <your-username>: /opt/KH_Clock
# Copy files (from your dev machine): # Copy files (from your dev machine):
scp -r . <user>@<ip>:/opt/sahsa_clock/ scp -r . <user>@<ip>:/opt/KH_Clock/
# On the Le Potato — create a venv and install dependencies: # On the Le Potato — create a venv and install dependencies:
cd /opt/sahsa_clock cd /opt/KH_Clock
python3 -m venv venv python3 -m venv venv
venv/bin/pip install -r requirements.txt venv/bin/pip install -r requirements.txt
``` ```
@@ -297,7 +297,7 @@ sudo apt install fonts-dejavu
### 4. Configure ### 4. Configure
```bash ```bash
nano /opt/sahsa_clock/config.toml 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). You only need to verify the `[server]` port and `[rate_limit]` values. Everything else is handled automatically on first run (token generation, password setup).
@@ -305,21 +305,21 @@ You only need to verify the `[server]` port and `[rate_limit]` values. Everythin
If the username on your device is not `pi`, open the service file and update the `User=` line: If the username on your device is not `pi`, open the service file and update the `User=` line:
```bash ```bash
nano /opt/sahsa_clock/sahsa-clock.service nano /opt/KH_Clock/KH-clock.service
``` ```
### 5. Install and start the systemd service ### 5. Install and start the systemd service
```bash ```bash
sudo cp /opt/sahsa_clock/sahsa-clock.service /etc/systemd/system/ sudo cp /opt/KH_Clock/KH-clock.service /etc/systemd/system/
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable --now sahsa-clock sudo systemctl enable --now KH-clock
``` ```
**Check that it started:** **Check that it started:**
```bash ```bash
sudo systemctl status sahsa-clock 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. 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.
@@ -340,7 +340,7 @@ The TV should now show the clock. Open `http://<device-ip>:8080` from a laptop o
**Clock doesn't appear after reboot:** **Clock doesn't appear after reboot:**
```bash ```bash
sudo journalctl -u sahsa-clock -n 50 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. Common causes: wrong `User=` in the service file, user not in the `video` group, `/dev/fb0` not present.
@@ -354,7 +354,7 @@ sudo usermod -aG video <user> # add if missing, then log out and back in
The desktop environment may still be running. Confirm `systemctl get-default` returns `multi-user.target` and that the display manager is disabled. The desktop environment may still be running. Confirm `systemctl get-default` returns `multi-user.target` and that the display manager is disabled.
**Dashboard unreachable:** **Dashboard unreachable:**
Check the service is running (`systemctl status sahsa-clock`) and that `port = 8080` in `config.toml` isn't blocked by a firewall. 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:** **Font looks pixelated:**
DejaVu fonts aren't installed. Run `sudo apt install fonts-dejavu` on the device and restart the service. DejaVu fonts aren't installed. Run `sudo apt install fonts-dejavu` on the device and restart the service.

View File

@@ -52,7 +52,7 @@ class AppConfig:
if not token: if not token:
token = secrets.token_hex(32) token = secrets.token_hex(32)
_update_config_field(p, "token", token) _update_config_field(p, "token", token)
print(f"\n[sahsa-clock] Generated API bearer token (saved to config.toml):") print(f"\n[KH-clock] Generated API bearer token (saved to config.toml):")
print(f" {token}\n") print(f" {token}\n")
return cls( return cls(

View File

@@ -3,14 +3,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sahsa Clock</title> <title>KH Clock</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header> <header>
<h1>Sahsa Clock</h1> <h1>KH Clock</h1>
<form method="post" action="/logout"> <form method="post" action="/logout">
<button type="submit" class="btn-signout">Sign out</button> <button type="submit" class="btn-signout">Sign out</button>
</form> </form>

View File

@@ -147,8 +147,7 @@ class DisplayThread(threading.Thread):
if self.dev_mode: if self.dev_mode:
os.environ.setdefault("SDL_VIDEODRIVER", "x11") os.environ.setdefault("SDL_VIDEODRIVER", "x11")
else: else:
os.environ["SDL_VIDEODRIVER"] = "fbcon" os.environ["SDL_VIDEODRIVER"] = "kmsdrm"
os.environ["SDL_FBDEV"] = "/dev/fb0"
try: try:
pygame.init() pygame.init()
@@ -216,7 +215,7 @@ class DisplayThread(threading.Thread):
w = self.config.width or 1280 w = self.config.width or 1280
h = self.config.height or 720 h = self.config.height or 720
surface = pygame.display.set_mode((w, h)) surface = pygame.display.set_mode((w, h))
pygame.display.set_caption("Sahsa Clock [DEV]") pygame.display.set_caption("KH Clock [DEV]")
return surface return surface
flags = pygame.FULLSCREEN | pygame.NOFRAME flags = pygame.FULLSCREEN | pygame.NOFRAME

View File

@@ -8,7 +8,7 @@ from state import MessageState
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Sahsa Clock Display") parser = argparse.ArgumentParser(description="KH Clock Display")
parser.add_argument( parser.add_argument(
"--dev", "--dev",
action="store_true", action="store_true",
@@ -39,7 +39,7 @@ def main() -> None:
try: try:
asyncio.run(run_server(state, config)) asyncio.run(run_server(state, config))
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n[sahsa-clock] Shutting down.") print("\n[KH-clock] Shutting down.")
sys.exit(0) sys.exit(0)

View File

@@ -14,7 +14,7 @@ from config import AppConfig
from state import MessageState from state import MessageState
DASHBOARD_DIR = Path(__file__).parent / "dashboard" DASHBOARD_DIR = Path(__file__).parent / "dashboard"
SESSION_COOKIE = "sahsa_session" SESSION_COOKIE = "kh_session"
# ── Session store ───────────────────────────────────────────────────────────── # ── Session store ─────────────────────────────────────────────────────────────
@@ -116,12 +116,12 @@ def _login_page(error: str = "") -> str:
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sahsa Clock — Sign In</title> <title>KH Clock — Sign In</title>
<style>{_PAGE_STYLE}</style> <style>{_PAGE_STYLE}</style>
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<h1>Sahsa Clock</h1> <h1>KH Clock</h1>
<p class="subtitle">Sign in to access the dashboard.</p> <p class="subtitle">Sign in to access the dashboard.</p>
{err} {err}
<form method="post" action="/login"> <form method="post" action="/login">
@@ -141,7 +141,7 @@ def _setup_page(error: str = "") -> str:
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sahsa Clock — First Run Setup</title> <title>KH Clock — First Run Setup</title>
<style>{_PAGE_STYLE}</style> <style>{_PAGE_STYLE}</style>
</head> </head>
<body> <body>
@@ -396,6 +396,6 @@ async def run_server(state: MessageState, config: AppConfig) -> None:
await runner.setup() await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", config.port) site = web.TCPSite(runner, "0.0.0.0", config.port)
await site.start() await site.start()
print(f"[sahsa-clock] Dashboard: http://0.0.0.0:{config.port}") print(f"[KH-clock] Dashboard: http://0.0.0.0:{config.port}")
print(f"[sahsa-clock] API: http://0.0.0.0:{config.port}/api/") print(f"[KH-clock] API: http://0.0.0.0:{config.port}/api/")
await asyncio.Event().wait() # run forever await asyncio.Event().wait() # run forever