Compare commits
4 Commits
d2c8079231
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 72efa94a06 | |||
| cbdd4a2952 | |||
| aab94d5e96 | |||
| 80697c7da5 |
@@ -1,24 +1,24 @@
|
|||||||
[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=sasha
|
||||||
WorkingDirectory=/opt/sahsa_clock
|
WorkingDirectory=/opt/KH_Clock
|
||||||
|
|
||||||
# Tell SDL to render directly to the Linux framebuffer (no display server needed)
|
# SDL renders offscreen; pixels are blit directly to /dev/fb0 (Amlogic kmsdrm is unsupported)
|
||||||
Environment=SDL_VIDEODRIVER=fbcon
|
Environment=SDL_VIDEODRIVER=offscreen
|
||||||
Environment=SDL_FBDEV=/dev/fb0
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# 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
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
|
## 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
|
||||||
|
|||||||
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.
|
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.
|
||||||
|
|||||||
7
check_sdl.py
Normal file
7
check_sdl.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import ctypes
|
||||||
|
sdl = ctypes.CDLL('libSDL2-2.0.so.0')
|
||||||
|
sdl.SDL_GetVideoDriver.restype = ctypes.c_char_p
|
||||||
|
n = sdl.SDL_GetNumVideoDrivers()
|
||||||
|
print('Available SDL2 drivers:')
|
||||||
|
for i in range(n):
|
||||||
|
print(' ', sdl.SDL_GetVideoDriver(i).decode())
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
39
display.py
39
display.py
@@ -1,3 +1,4 @@
|
|||||||
|
import mmap
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -147,8 +148,9 @@ 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"
|
# SDL kmsdrm is incompatible with the Amlogic MESON DRM driver.
|
||||||
os.environ["SDL_FBDEV"] = "/dev/fb0"
|
# Use offscreen rendering and blit pixels directly to /dev/fb0.
|
||||||
|
os.environ["SDL_VIDEODRIVER"] = "offscreen"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pygame.init()
|
pygame.init()
|
||||||
@@ -167,6 +169,19 @@ class DisplayThread(threading.Thread):
|
|||||||
clock = pygame.time.Clock()
|
clock = pygame.time.Clock()
|
||||||
screen_w, screen_h = screen.get_size()
|
screen_w, screen_h = screen.get_size()
|
||||||
|
|
||||||
|
# Open /dev/fb0 for direct pixel writes (framebuffer mode only)
|
||||||
|
fb0_mmap = None
|
||||||
|
fb0_file = None
|
||||||
|
if not self.dev_mode:
|
||||||
|
try:
|
||||||
|
bpp = int(Path("/sys/class/graphics/fb0/bits_per_pixel").read_text())
|
||||||
|
fb0_file = open("/dev/fb0", "rb+")
|
||||||
|
fb0_mmap = mmap.mmap(fb0_file.fileno(), screen_w * screen_h * (bpp // 8))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[display] Failed to open /dev/fb0: {e}")
|
||||||
|
pygame.quit()
|
||||||
|
return
|
||||||
|
|
||||||
clock_font_path = _find_font(self.config.clock_font_path, _CLOCK_FONT_CANDIDATES)
|
clock_font_path = _find_font(self.config.clock_font_path, _CLOCK_FONT_CANDIDATES)
|
||||||
msg_font_path = _find_font(self.config.message_font_path, _MESSAGE_FONT_CANDIDATES)
|
msg_font_path = _find_font(self.config.message_font_path, _MESSAGE_FONT_CANDIDATES)
|
||||||
|
|
||||||
@@ -206,9 +221,17 @@ class DisplayThread(threading.Thread):
|
|||||||
last_msg_text = None
|
last_msg_text = None
|
||||||
self._draw_clock(screen, clock_font, screen_w, screen_h)
|
self._draw_clock(screen, clock_font, screen_w, screen_h)
|
||||||
|
|
||||||
pygame.display.flip()
|
if fb0_mmap is not None:
|
||||||
|
fb0_mmap.seek(0)
|
||||||
|
fb0_mmap.write(pygame.image.tostring(screen, "BGRA"))
|
||||||
|
else:
|
||||||
|
pygame.display.flip()
|
||||||
clock.tick(self.config.fps)
|
clock.tick(self.config.fps)
|
||||||
|
|
||||||
|
if fb0_mmap:
|
||||||
|
fb0_mmap.close()
|
||||||
|
if fb0_file:
|
||||||
|
fb0_file.close()
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
|
|
||||||
def _create_surface(self) -> pygame.Surface:
|
def _create_surface(self) -> pygame.Surface:
|
||||||
@@ -216,13 +239,13 @@ 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
|
# Read actual framebuffer dimensions from sysfs
|
||||||
if self.config.width and self.config.height:
|
size_str = Path("/sys/class/graphics/fb0/virtual_size").read_text().strip()
|
||||||
return pygame.display.set_mode((self.config.width, self.config.height), flags)
|
w, h = map(int, size_str.split(","))
|
||||||
return pygame.display.set_mode((0, 0), flags)
|
return pygame.display.set_mode((w, h))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _draw_clock(
|
def _draw_clock(
|
||||||
|
|||||||
4
main.py
4
main.py
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
server.py
12
server.py
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user