# 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 ```bash 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://: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_cli` utan lösenord via sudoers. Sätts upp automatiskt av `install.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-1` etc.) - **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://:5000/display └─ Workspace 2 (eDP-1): kiosk-admin → http://: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` ```python 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//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` ```python 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:** ```python 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 ``/`` — bara innehållet: ```html

Mitt Addon

``` ### 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: ```javascript (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): ```python 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//panel.html` finns | | `has_overlay` | bool | `True` om `static/addons//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): ```bash # 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: ```bash 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) ```bash 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 bash /home/mrfox/codes/display/kiosk/kiosk-launcher.sh ``` **Flask svarar inte:** ```bash cd /home/mrfox/codes/display/kiosk && bash start.sh ``` **Sway startar inte / svart skärm:** ```bash # 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":** ```bash 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) ```