Raspberry pi kiosk
| README.md | ||
RPI Glance Dashboard Kiosk & Audio Notifier System
This repository contains the complete configuration for a Raspberry Pi-based Kiosk dashboard with high-quality neural text-to-speech, automated browser refreshing, and system-level overlays.
Hardware Required
- 10.1" 1920x1200 Touchscreen - https://amazon.com/dp/B0DDL5VMZF
- Raspberry Pi 4 Model B - https://www.pishop.us/product/raspberry-pi-4-model-b-2gb
- SDCard - https://amazon.com/dp/B08TJRVWV1
1. System Architecture
The system consists of two primary services:
- Chromium Kiosk: Runs a full-screen browser in a restricted environment with remote debugging enabled.
- Audio Notifier: A Flask-based API that orchestrates audio playback, browser reloads, and screen overlays.
graph LR
Client[External Client / curl] -->|POST /play| AudioAPI[Audio Notifier Flask API :5000]
subgraph Raspberry Pi
KioskService[systemd: kiosk.service] -->|Manages| Chrome
AudioService[systemd: audio-notifier.service] -->|Manages| AudioAPI
AudioAPI -->|Text| Piper[Piper Neural TTS]
Piper -->|Audio Stream| HDMI[Audio Output: vc4hdmi0]
AudioAPI -->|Refresh & Overlay Command| Chrome[Chromium Kiosk :9222]
Chrome -->|Render| Display[X11 Display :0]
end
Chrome -->|Fetch Dashboard| WebApp[Glance Dashboard: 192.168.1.101:8081]
2. Audio Notifier Service
Installation Details
- Location:
~/audio - Port:
5000 - TTS Engine: Piper Neural TTS (
amy-mediumvoice) - Audio Output: Fixed to HDMI 0 (
plughw:CARD=vc4hdmi0,DEV=0)
API Reference
- Endpoint:
POST /play - Parameters:
text: String to be spoken via TTS and displayed as an overlay.refresh: Boolean (defaulttrue) to reload the kiosk browser.url: Optional audio URL for file-based playback (fallback).
audio_server.py
Location: ~/audio/audio_server.py
from flask import Flask, request, jsonify
import subprocess
import os
import json
import websocket
import requests
import sys
import time
app = Flask(__name__)
last_refresh_time = 0
def get_target_ws_url():
try:
resp = requests.get("http://127.0.0.1:9222/json/list", timeout=2)
targets = resp.json()
for t in targets:
if t.get('type') == 'page' and ("Command Center" in t.get('title', '') or "8081" in t.get('url', '')):
return t.get('webSocketDebuggerUrl')
except Exception as e:
print(f"Discovery error: {e}")
return None
def run_browser_commands(refresh=False, overlay_text=None):
global last_refresh_time
ws_url = get_target_ws_url()
if not ws_url:
print("DEBUG: No valid browser target found")
return False
try:
ws = websocket.create_connection(ws_url, timeout=5, suppress_origin=True)
# 1. Handle Refresh (with cooldown)
if refresh:
now = time.time()
if (now - last_refresh_time) > 20:
print("DEBUG: Sending Page.reload")
ws.send(json.dumps({"id": 10, "method": "Page.reload"}))
# Wait for response to ensure command is processed
resp = ws.recv()
print(f"DEBUG: Reload response: {resp}")
last_refresh_time = now
# 2. Handle Overlay
if overlay_text:
# If we just refreshed, give the page a split second to start loading
if refresh:
time.sleep(0.5)
print(f"DEBUG: Sending Overlay: {overlay_text}")
css = (
"position:fixed !important; bottom:5% !important; left:50% !important; "
"transform:translateX(-50%) !important; padding:20px 40px !important; "
"background:rgba(0,0,0,0.9) !important; color:#00ff00 !important; "
"font-size:35px !important; font-weight:bold !important; "
"font-family:sans-serif !important; border-radius:20px !important; "
"border:3px solid #00ff00 !important; z-index:2147483647 !important; "
"text-align:center !important; transition:opacity 1s !important; "
"pointer-events:none !important;"
)
js_code = f"""
(function() {{
console.log('Gemini Overlay Triggered');
var div = document.createElement('div');
div.style.cssText = '{css}';
div.innerText = {json.dumps(overlay_text)};
document.body.appendChild(div);
setTimeout(() => {{
div.style.opacity = '0';
setTimeout(() => div.remove(), 1000);
}}, 60000);
}})();
"""
ws.send(json.dumps({"id": 11, "method": "Runtime.evaluate", "params": {"expression": js_code}}))
resp = ws.recv()
print(f"DEBUG: Overlay response: {resp}")
ws.close()
return True
except Exception as e:
print(f"DEBUG: Browser command execution failed: {e}")
return False
@app.route('/play', methods=['POST'])
def play_audio():
# Handle both JSON and Form data for maximum flexibility
if request.is_json:
data = request.get_json(silent=True) or {}
else:
data = request.form.to_dict() or {}
text = data.get('text')
# If text is empty in form data, check if it was sent as a raw string
if not text and request.data:
try:
raw_data = json.loads(request.data)
text = raw_data.get('text')
except:
pass
refresh = data.get('refresh', 'true').lower() == 'true' if isinstance(data.get('refresh'), str) else data.get('refresh', True)
# Run Browser Commands
browser_sync = run_browser_commands(refresh=refresh, overlay_text=text)
# Run Audio
if text:
piper_cmd = [f"{home_dir}/audio/piper/piper", "--model", f"{home_dir}/audio/en_US-amy-medium.onnx", "--output_raw"]
aplay_cmd = ["aplay", "-D", "plughw:CARD=vc4hdmi0,DEV=0", "-r", "22050", "-f", "S16_LE", "-t", "raw"]
p1 = subprocess.Popen(["echo", text], stdout=subprocess.PIPE)
p2 = subprocess.Popen(piper_cmd, stdin=p1.stdout, stdout=subprocess.PIPE)
subprocess.Popen(aplay_cmd, stdin=p2.stdout)
sys.stdout.flush()
return jsonify({
"status": "processed",
"browser_sync": browser_sync,
"audio": bool(text)
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
audio-notifier.service
Location: /etc/systemd/system/audio-notifier.service
[Unit]
Description=High-Quality Audio Notifier Server
After=network.target
[Service]
User=1000
Group=1000
WorkingDirectory=%h/audio
Environment="AUDIO_PASS=password"
Environment="DISPLAY=:0"
Environment="XAUTHORITY=%h/.Xauthority"
ExecStart=/usr/bin/python3 %h/audio/audio_server.py
Restart=always
[Install]
WantedBy=multi-user.target
3. Chromium Kiosk Setup
kiosk.sh
Location: ~/kiosk.sh
#!/bin/bash
GLANCE_HOST="192.168.1.101"
GLANCE_PORT="8081"
export DISPLAY=:0
xset s off
xset s noblank
xset -dpms
unclutter -idle 0.5 -root &
mkdir -p "$HOME/kiosk_profile/Default"
echo '{"partition":{"per_host_zoom_levels":{"'"$GLANCE_HOST"'":{"zoom_level":2.22}}},"webkit":{"webprefs":{"default_font_size":45,"minimum_font_size":22}}}' > "$HOME/kiosk_profile/Default/Preferences"
exec /usr/bin/chromium \
--kiosk \
--user-data-dir="$HOME/kiosk_profile" \
--no-first-run \
--noerrdialogs \
--disable-infobars \
--disable-dev-shm-usage \
--window-size=1920,1200 \
--window-position=0,0 \
--high-dpi-support=1 \
--force-device-scale-factor=1 \
--ignore-certificate-errors \
--remote-debugging-port=9222 \
"http://$GLANCE_HOST:$GLANCE_PORT"
kiosk.service
Location: /etc/systemd/system/kiosk.service
[Unit]
Description=Chromium Kiosk
After=xorg.service
Wants=xorg.service
[Service]
User=1000
Group=1000
Environment=DISPLAY=:0
Environment=XAUTHORITY=%h/.Xauthority
ExecStartPre=/bin/sleep 2
ExecStart=%h/kiosk.sh
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
4. Operational Commands
Triggering a Notification
# Form data (easiest)
curl -X POST http://<PI_IP>:5000/play -d "text=Dashboard Updated"
# JSON data
curl -X POST http://<PI_IP>:5000/play -H "Content-Type: application/json" -d '{"text": "Alert", "refresh": true}'
Service Management
# Restart Services
sudo systemctl restart kiosk.service
sudo systemctl restart audio-notifier.service
# View Logs
journalctl -u audio-notifier.service -f
journalctl -u kiosk.service -f
5. Reconstruction Guide
To rebuild this system on a fresh Raspberry Pi:
- Install dependencies:
apt install python3-flask python3-websocket python3-requests piper unclutter chromium - Clone this directory to
~/audio. - Place
kiosk.shin~/and make it executable (chmod +x). - Copy the
.servicefiles to/etc/systemd/system/. - Enable and start services:
sudo systemctl daemon-reload sudo systemctl enable --now kiosk.service audio-notifier.service