Named correctly, and fixed to work with Le Potato
This commit is contained in:
@@ -1,24 +1,23 @@
|
||||
[Unit]
|
||||
Description=Sahsa Clock Display
|
||||
Description=KH Clock Display
|
||||
After=network.target
|
||||
DefaultDependencies=no
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
WorkingDirectory=/opt/sahsa_clock
|
||||
WorkingDirectory=/opt/KH_Clock
|
||||
|
||||
# Tell SDL to render directly to the Linux framebuffer (no display server needed)
|
||||
Environment=SDL_VIDEODRIVER=fbcon
|
||||
Environment=SDL_FBDEV=/dev/fb0
|
||||
# Tell SDL to render via KMS/DRM (required on Le Potato and similar ARM boards)
|
||||
Environment=SDL_VIDEODRIVER=kmsdrm
|
||||
|
||||
# Disable console blanking so the TV stays on
|
||||
ExecStartPre=/bin/sh -c 'echo -ne "\033[9;0]" > /dev/tty1'
|
||||
|
||||
# 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:
|
||||
# ExecStart=/usr/bin/python3 /opt/sahsa_clock/main.py
|
||||
# ExecStart=/usr/bin/python3 /opt/KH_Clock/main.py
|
||||
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
14
PLAN.md
14
PLAN.md
@@ -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
|
||||
|
||||
@@ -27,7 +27,7 @@ Pygame can render directly to `/dev/fb0` (the Linux framebuffer) without any dis
|
||||
## Architecture
|
||||
|
||||
```
|
||||
sahsa_clock/
|
||||
KH_Clock/
|
||||
├── main.py # Entry point: asyncio event loop + pygame render loop
|
||||
├── display.py # Pygame rendering: clock face, message overlay
|
||||
├── server.py # aiohttp server: dashboard routes + API routes
|
||||
@@ -37,7 +37,7 @@ sahsa_clock/
|
||||
│ └── app.js
|
||||
├── config.toml # Screen resolution, colors, fonts, port, token, timeout
|
||||
├── requirements.txt
|
||||
└── sahsa-clock.service # systemd unit file
|
||||
└── KH-clock.service # systemd unit file
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### systemd service (`/etc/systemd/system/sahsa-clock.service`)
|
||||
### systemd service (`/etc/systemd/system/KH-clock.service`)
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Sahsa Clock Display
|
||||
Description=KH Clock Display
|
||||
After=network.target
|
||||
DefaultDependencies=no
|
||||
|
||||
@@ -130,7 +130,7 @@ Type=simple
|
||||
User=pi # or whatever the device user is
|
||||
Environment=SDL_VIDEODRIVER=fbcon
|
||||
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
|
||||
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
|
||||
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`
|
||||
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
|
||||
|
||||
28
README.md
28
README.md
@@ -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.
|
||||
|
||||
@@ -17,7 +17,7 @@ A lightweight, boot-persistent clock display for a TV, running on a [Libre Compu
|
||||
## File structure
|
||||
|
||||
```
|
||||
sahsa_clock/
|
||||
KH_Clock/
|
||||
├── main.py # Entry point
|
||||
├── display.py # Pygame rendering (clock + messages)
|
||||
├── server.py # aiohttp web server (dashboard + API)
|
||||
@@ -29,7 +29,7 @@ sahsa_clock/
|
||||
│ ├── style.css
|
||||
│ └── app.js
|
||||
├── 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
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/sahsa_clock
|
||||
sudo chown <your-username>: /opt/sahsa_clock
|
||||
sudo mkdir -p /opt/KH_Clock
|
||||
sudo chown <your-username>: /opt/KH_Clock
|
||||
|
||||
# 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:
|
||||
cd /opt/sahsa_clock
|
||||
cd /opt/KH_Clock
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
@@ -297,7 +297,7 @@ sudo apt install fonts-dejavu
|
||||
### 4. Configure
|
||||
|
||||
```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).
|
||||
@@ -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:
|
||||
|
||||
```bash
|
||||
nano /opt/sahsa_clock/sahsa-clock.service
|
||||
nano /opt/KH_Clock/KH-clock.service
|
||||
```
|
||||
|
||||
### 5. Install and start the systemd service
|
||||
|
||||
```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 enable --now sahsa-clock
|
||||
sudo systemctl enable --now KH-clock
|
||||
```
|
||||
|
||||
**Check that it started:**
|
||||
|
||||
```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.
|
||||
@@ -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:**
|
||||
```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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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:**
|
||||
DejaVu fonts aren't installed. Run `sudo apt install fonts-dejavu` on the device and restart the service.
|
||||
|
||||
@@ -52,7 +52,7 @@ class AppConfig:
|
||||
if not token:
|
||||
token = secrets.token_hex(32)
|
||||
_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")
|
||||
|
||||
return cls(
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<header>
|
||||
<h1>Sahsa Clock</h1>
|
||||
<h1>KH Clock</h1>
|
||||
<form method="post" action="/logout">
|
||||
<button type="submit" class="btn-signout">Sign out</button>
|
||||
</form>
|
||||
|
||||
@@ -147,8 +147,7 @@ class DisplayThread(threading.Thread):
|
||||
if self.dev_mode:
|
||||
os.environ.setdefault("SDL_VIDEODRIVER", "x11")
|
||||
else:
|
||||
os.environ["SDL_VIDEODRIVER"] = "fbcon"
|
||||
os.environ["SDL_FBDEV"] = "/dev/fb0"
|
||||
os.environ["SDL_VIDEODRIVER"] = "kmsdrm"
|
||||
|
||||
try:
|
||||
pygame.init()
|
||||
@@ -216,7 +215,7 @@ class DisplayThread(threading.Thread):
|
||||
w = self.config.width or 1280
|
||||
h = self.config.height or 720
|
||||
surface = pygame.display.set_mode((w, h))
|
||||
pygame.display.set_caption("Sahsa Clock [DEV]")
|
||||
pygame.display.set_caption("KH Clock [DEV]")
|
||||
return surface
|
||||
|
||||
flags = pygame.FULLSCREEN | pygame.NOFRAME
|
||||
|
||||
4
main.py
4
main.py
@@ -8,7 +8,7 @@ from state import MessageState
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Sahsa Clock Display")
|
||||
parser = argparse.ArgumentParser(description="KH Clock Display")
|
||||
parser.add_argument(
|
||||
"--dev",
|
||||
action="store_true",
|
||||
@@ -39,7 +39,7 @@ def main() -> None:
|
||||
try:
|
||||
asyncio.run(run_server(state, config))
|
||||
except KeyboardInterrupt:
|
||||
print("\n[sahsa-clock] Shutting down.")
|
||||
print("\n[KH-clock] Shutting down.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
12
server.py
12
server.py
@@ -14,7 +14,7 @@ from config import AppConfig
|
||||
from state import MessageState
|
||||
|
||||
DASHBOARD_DIR = Path(__file__).parent / "dashboard"
|
||||
SESSION_COOKIE = "sahsa_session"
|
||||
SESSION_COOKIE = "kh_session"
|
||||
|
||||
|
||||
# ── Session store ─────────────────────────────────────────────────────────────
|
||||
@@ -116,12 +116,12 @@ def _login_page(error: str = "") -> str:
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Sahsa Clock</h1>
|
||||
<h1>KH Clock</h1>
|
||||
<p class="subtitle">Sign in to access the dashboard.</p>
|
||||
{err}
|
||||
<form method="post" action="/login">
|
||||
@@ -141,7 +141,7 @@ def _setup_page(error: str = "") -> str:
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -396,6 +396,6 @@ async def run_server(state: MessageState, config: AppConfig) -> None:
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, "0.0.0.0", config.port)
|
||||
await site.start()
|
||||
print(f"[sahsa-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] Dashboard: http://0.0.0.0:{config.port}")
|
||||
print(f"[KH-clock] API: http://0.0.0.0:{config.port}/api/")
|
||||
await asyncio.Event().wait() # run forever
|
||||
|
||||
Reference in New Issue
Block a user