Kiosk
Digitalt skyltfönster / displayhanterare för Linux (Debian 13). Visar webbsidor, YouTube, bilder, video och lokala HTML-filer i helskärm via Chromium under Sway (Wayland). Styrs via en webbaserad adminpanel.
Installation
sudo bash install.sh
sudo reboot
Kiosken startar automatiskt vid omstart via autoinloggning på tty1.
Krav
| Paket | Syfte |
|---|---|
sway | Wayland-compositor |
chromium | Kiosk-webbläsare |
python3-flask, python3-flask-socketio | Webbserver |
python3-apscheduler | Schemaläggare |
python3-psutil, python3-eventlet | Systeminfo, async |
iw, wpasupplicant | Wi-Fi-hantering |
curl | Nätverkstest vid uppstart |
Adminpanel
http://<maskinens-IP>:5000/admin
Standardinloggning — ändra i kiosk/start.sh:
| Variabel | Standardvärde |
|---|---|
KIOSK_USER | mrfox |
KIOSK_PASS | qweasdzxc |
SECRET_KEY | kiosk-hemlig-nyckel-byt-garna |
Funktioner
Visa nu
Visa något direkt utan att det läggs till i schemat:
- URL — valfri webbadress, YouTube-länk konverteras automatiskt till embed
- Ladda upp fil — video, bild, HTML, PDF/presentation
- Lokal fil — bläddra bland filer på servern
- Töm skärm — visar idle-skärm med IP-adress
Schema
Jobb roterar automatiskt i ordning. Varje jobb har en varaktighet i minuter (decimaler OK, ex. 0.5 = 30 sek).
| Typ | Beskrivning |
|---|---|
url | Valfri webbadress |
youtube | YouTube-liveflöde eller video |
video | Videofil (mp4, webm, avi, mkv, mov) |
image | Bildfil (jpg, png, gif, webp, bmp) |
pdf | PDF eller presentation (ppt, pptx, odp) |
kiosk_chat | Inbyggd livechat i helskärm |
Videolängd detekteras automatiskt vid filval.
Löptext (ticker)
Rullande textrad som overlay. Aktiveras/stängs av utan omstart.
- Källa — manuell text eller RSS-flöde (uppdateras automatiskt)
- Scrollläge — en rad i taget eller löpande (kontinuerligt)
- Position — nederkant eller överkant
- Hastighet, textfärg, storlek, typsnitt, bakgrundsfärg, opacitet
- Separator mellan rader i löpande läge
BBCode:
| Tagg | Effekt |
|---|---|
[b]...[/b] | Fet |
[i]...[/i] | Kursiv |
[u]...[/u] | Understruken |
[s]...[/s] | Genomstruken |
[color=#ff0000]...[/color] | Textfärg |
[size=64]...[/size] | Storlek i px |
[shadow]...[/shadow] | Skugga |
[glow=#00ff00]...[/glow] | Glödeffekt |
Livechat
- Kiosk Livechat — inbyggd chat, besökare ansluter via QR-kod på mobilen. Kan visas som helskärm (schema-typ
kiosk_chat) eller som overlay ovanpå annat innehåll. - YouTube Livechat — overlay som visas automatiskt vid YouTube-liveflöde.
Inställningar per chat: position (höger/vänster), bredd, höjd, bakgrundsfärg, opacitet.
Klocka
Klock-overlay ovanpå allt innehåll.
- Stil — digital (HH:MM:SS) eller analog (canvas med sekundvisare)
- Datum — valfritt datum under klockan (sv: Mån 7 apr)
- Position — övre vänster/höger, nedre vänster/höger, centrerad
- Storlek, färg, opacitet
- Schema — visa alltid, eller bara under schemalagda aktiviteter
- Positioneras automatiskt bort från ticker-listen
Sovläge
Täcker displayen med svart skärm under angiven tid (ex. 22:00–07:00).
Wi-Fi
Hantera trådlöst nätverk direkt från adminpanelen.
- Status — aktuell SSID, IP-adress, signalstyrka
- Skanna — listar tillgängliga nätverk med signal och säkerhetstyp
- Anslut — lösenordsmodal (öppna nätverk kräver inget lösenord)
- Sparade nätverk — lista och ta bort
Krav:
wpa_cliutan lösenord via sudoers. Sätts upp automatiskt avinstall.sh.
Inställningar
- Volym — systemvolym via PipeWire (
wpctl) - Ljudprofil — välj PipeWire-kortprofil (t.ex. HDMI Stereo, Analog Stereo)
- Intern display (eDP-1) — aktivera/inaktivera den inbyggda skärmen med adminpanel
- HDMI-utgång — välj vilken utgång som används (
HDMI-A-1etc.) - Upplösning — välj bland tillgängliga lägen eller skriv in manuellt
Om intern display är inaktiverad men ingen Wi-Fi-anslutning finns vid uppstart — aktiveras den automatiskt så att adminpanelen alltid är tillgänglig.
System
Starta om programmet, starta om datorn, stäng av datorn.
Uppstartflöde
Dator startar
└─ Getty autologin → tty1 → .bash_profile
└─ kiosk-launcher.sh
├─ Väntar på HDMI-A-1
├─ Startar kiosk/start.sh (Flask) i bakgrunden
├─ Väntar på Flask (127.0.0.1:5000)
├─ Väntar på Wi-Fi / IP-adress (max 40 s)
├─ Genererar /tmp/kiosk-sway-outputs.conf
└─ Startar sway
└─ sway-apps.sh
├─ Väntar på Wi-Fi / IP (max 40 s)
├─ Pre-seedar Chromium-prefs (translate av)
├─ Workspace 1 (HDMI): kiosk-display → http://<IP>:5000/display
└─ Workspace 2 (eDP-1): kiosk-admin → http://<IP>:5000/admin
Filstruktur
display/
├── install.sh # Installationsscript
└── kiosk/
├── app.py # Flask-app: core routes, WebSocket, startup
├── auth.py # login_required-dekoratör
├── config.py # Delade konstanter (ALLOWED_ROOTS, UPLOAD_DIR m.m.)
├── db.py # SQLite-helpers + AddonDB
├── extensions.py # Delad socketio-referens (undviker cirkulär import)
├── addon_loader.py # Addon-discovery och Blueprint-registrering
├── scheduler.py # Schemarotation (APScheduler)
├── start.sh # Startar Flask (sätter env-vars)
├── kiosk-launcher.sh # Startar Flask + sway, genererar output-config
├── sway.conf # Sway-konfiguration
├── sway-apps.sh # Startar Chromium på rätt workspace
├── hdmi-monitor.sh # Övervakar HDMI-anslutning
├── hdmi-wait.sh # Väntar på HDMI vid start
├── kiosk.conf # Genereras av admin (ljud/skärm-inställningar)
├── kiosk.db # SQLite-databas
├── addons/ # Addon-paket (ett paket per funktion)
│ ├── ticker/
│ ├── clock/
│ ├── music/
│ ├── livechat/
│ ├── sleep/
│ ├── wifi/
│ ├── bluetooth/
│ ├── filebrowser/
│ ├── settings/
│ └── system/
├── utils/
│ ├── display.py # sway_env, apply_display_mode, apply_saved_audio
│ ├── system.py # local_ip
│ └── youtube.py # yt_extract, playlist_cache
├── uploads/ # Uppladdade mediefiler
├── static/
│ ├── admin.js
│ ├── display.js
│ ├── style.css
│ ├── socket.io.min.js
│ └── addons/ # Statiska filer per addon (overlay.js m.m.)
└── templates/
├── admin.html # Skaltemplate — inkluderar addon-paneler dynamiskt
├── display.html
├── login.html
├── chat_display.html
├── chat_mobile.html
└── addons/ # HTML-fragment per addon-panel
├── ticker/panel.html
├── clock/panel.html
└── ...
Skapa ett addon
Addons är vanliga Python-paket under kiosk/addons/. De autodiscoveras vid uppstart — ingen registrering behövs.
1. Skapa paketmappen
kiosk/addons/mitt_addon/
__init__.py
routes.py
2. Deklarera manifestet i __init__.py
ADDON = {
"name": "mitt_addon", # måste matcha mappnamnet
"label": "Mitt Addon", # visas i adminpanelen
"version": "1.0",
"order": 10, # sorteringsordning i adminpanelen
"has_panel": True, # True om panel.html finns
"has_overlay": False, # True om static/addons/<name>/overlay.js finns
"has_socketio": False, # True om register_socketio() finns
"state_prefix": "mitt_addon_", # prefix för alla DB-nycklar
"default_enabled": True,
}
def create_blueprint():
from .routes import bp
return bp
# Behövs bara om has_socketio = True:
def register_socketio(socketio):
from .events import MittNamespace
socketio.on_namespace(MittNamespace('/mitt_addon'))
3. Definiera routes i routes.py
from flask import Blueprint, jsonify, request, abort
import db
import extensions
from auth import login_required
bp = Blueprint('mitt_addon', __name__, url_prefix='/api/mitt_addon')
@bp.route('/settings', methods=['GET'])
def get_settings():
state = db.get_state()
return jsonify({
'enabled': state.get('mitt_addon_enabled', '0') == '1',
'value': state.get('mitt_addon_value', ''),
})
@bp.route('/settings', methods=['POST'])
@login_required
def set_settings():
data = request.get_json() or {}
enabled = bool(data.get('enabled', False))
value = str(data.get('value', ''))
db.set_state('mitt_addon_enabled', '1' if enabled else '0')
db.set_state('mitt_addon_value', value)
extensions.socketio.emit('mitt_addon_update', {'enabled': enabled, 'value': value})
return '', 204
Tips — AddonDB för namnrymdsavgränsad DB-åtkomst:
from db import AddonDB
adb = AddonDB('mitt_addon_') # prefixar alla nycklar automatiskt
adb.set('enabled', '1') # lagras som "mitt_addon_enabled"
adb.get('value', 'default') # läser "mitt_addon_value"
adb.get_all() # {'enabled': '1', 'value': '...'} (utan prefix)
4. Adminpanel — templates/addons/mitt_addon/panel.html
HTML-fragmentet inkluderas automatiskt i admin.html om has_panel = True. Det behöver ingen <html>/<body> — bara innehållet:
<section>
<h2>Mitt Addon</h2>
<div class="prop-grid">
<label>Aktivera</label>
<label style="display:flex;align-items:center;gap:.5rem;font-size:.9rem">
<input type="checkbox" id="mitt-addon-enabled">
Aktivera funktionen
</label>
<label>Värde</label>
<input type="text" id="mitt-addon-value" placeholder="...">
</div>
<div class="row" style="margin-top:.75rem">
<button onclick="sparaMittAddon()">Spara</button>
<span id="mitt-addon-status" style="color:#aaa;font-size:.85rem;align-self:center"></span>
</div>
</section>
5. Display-overlay — static/addons/mitt_addon/overlay.js (valfritt)
Om has_overlay = True laddas skriptet automatiskt på display-sidan. Skriptet skapar sina egna DOM-element och lyssnar på SocketIO-events:
(function () {
const el = document.createElement('div');
el.id = 'mitt-addon-overlay';
el.style.cssText = 'display:none;position:fixed;z-index:9000;pointer-events:none;';
document.body.appendChild(el);
const socket = window._kioskSocket; // definieras i display.js
socket.on('mitt_addon_update', (data) => {
el.style.display = data.enabled ? 'block' : 'none';
el.textContent = data.value;
});
})();
6. SocketIO-events — events.py (valfritt)
Behövs bara om addons behöver lyssna på WebSocket-events (inte bara sända):
from flask import request
from flask_socketio import Namespace
import extensions
class MittNamespace(Namespace):
def on_connect(self):
extensions.socketio.emit('mitt_status', {'ok': True}, to=request.sid,
namespace='/mitt_addon')
def on_mitt_event(self, data):
# hantera inkommande event
extensions.socketio.emit('mitt_svar', {'echo': data}, namespace='/mitt_addon')
Referens — ADDON-manifest
| Fält | Typ | Beskrivning |
|---|---|---|
name | str | Unikt ID — måste matcha mappnamnet |
label | str | Visningsnamn i adminpanel |
version | str | Fri versionssträng |
order | int | Sorteringsordning för panelen (lägre = högre upp) |
has_panel | bool | True om templates/addons/<name>/panel.html finns |
has_overlay | bool | True om static/addons/<name>/overlay.js finns |
has_socketio | bool | True om register_socketio() är definierad i __init__.py |
state_prefix | str | Prefix för alla nycklar som addons sparar i DB |
default_enabled | bool | Om addons är aktiverat första gången (kan ändras i admin) |
Aktivera / inaktivera ett addon
Varje addon kan slås av och på via API (kräver admin-inloggning):
# Inaktivera
curl -X POST http://localhost:5000/api/addons/mitt_addon/toggle \
-H "Cookie: session=..."
# Lista status för alla addons
curl http://localhost:5000/api/addons -H "Cookie: session=..."
Ändringen sparas i databasen och träder i kraft vid nästa omstart.
Konfiguration
kiosk.conf
Genereras automatiskt av adminpanelen. Kan redigeras manuellt:
KIOSK_DISPLAY_MODE=1920x1080@60Hz
KIOSK_DISPLAY_OUTPUT=HDMI-A-1
KIOSK_INTERNAL_DISPLAY=off
KIOSK_AUDIO_PROFILE_INDEX=1
Miljövariabler (kiosk/start.sh)
export KIOSK_USER=admin
export KIOSK_PASS=mittlösenord
export SECRET_KEY=en-lång-hemlig-sträng
Teknikstack
| Komponent | Teknologi |
|---|---|
| Backend | Python 3, Flask, Flask-SocketIO, eventlet |
| Addon-system | Flask Blueprints + pkgutil.iter_modules() |
| Schemaläggare | APScheduler |
| Databas | SQLite |
| Frontend | Vanilla JS, Socket.IO |
| Compositor | Sway (Wayland) |
| Webbläsare | Chromium (kiosk-läge, ozone-wayland) |
| Ljud | PipeWire (wpctl, pw-cli) |
| Wi-Fi | wpa_supplicant + wpa_cli |
Felsökning
Kiosken startar inte:
bash /home/mrfox/codes/display/kiosk/kiosk-launcher.sh
Flask svarar inte:
cd /home/mrfox/codes/display/kiosk && bash start.sh
Sway startar inte / svart skärm:
# Kontrollera att HDMI är anslutet, sedan:
SWAYSOCK=$(ls /run/user/$(id -u)/sway-ipc.*.sock 2>/dev/null | head -1)
swaymsg -s "$SWAYSOCK" exit
Wi-Fi-sektionen i admin visar "sudo saknas":
echo 'mrfox ALL=(ALL) NOPASSWD: /usr/sbin/wpa_cli' | sudo tee /etc/sudoers.d/kiosk-wifi
sudo chmod 440 /etc/sudoers.d/kiosk-wifi
Markör syns på displayen: Kontrollera att seat seat0 hide_cursor 3000 finns i sway.conf (läggs dit av install.sh).
Translate-popup i Chromium: sway-apps.sh pre-seedar Default/Preferences med "translate": {"enabled": false} innan Chromium startar. Om det ändå visas: ta bort /tmp/kiosk-display och starta om.
Ett addon laddas inte: Kontrollera att mappnamnet matchar ADDON["name"] och att __init__.py innehåller ett ADDON-dict. Addons loggas vid uppstart:
[addon_loader] Laddade addon: mitt_addon (Mitt Addon)