Raspberry pi kiosk
Find a file
2026-02-25 11:59:40 -05:00
README.md Update README.md 2026-02-25 11:59:40 -05:00

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


1. System Architecture

The system consists of two primary services:

  1. Chromium Kiosk: Runs a full-screen browser in a restricted environment with remote debugging enabled.
  2. 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-medium voice)
  • 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 (default true) 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:

  1. Install dependencies: apt install python3-flask python3-websocket python3-requests piper unclutter chromium
  2. Clone this directory to ~/audio.
  3. Place kiosk.sh in ~/ and make it executable (chmod +x).
  4. Copy the .service files to /etc/systemd/system/.
  5. Enable and start services:
    sudo systemctl daemon-reload
    sudo systemctl enable --now kiosk.service audio-notifier.service