TL;DR: Das Projekt auf den Punkt gebracht
Dieses Tutorial zeigt die schrittweise Umsetzung einer professionellen, ausfallsicheren Digital-Signage-Werbetafel nach dem KISS-Prinzip auf Basis eines Raspberry Pi 4 und eines 22″ Monitors.
- Schlanker Linux-Unterbau: Das System läuft auf Raspberry Pi OS Lite – komplett ohne ressourcenhungrige Desktop-Umgebung, was maximale Stabilität garantiert.
- Absolut selbsterklärend: Nicht-technische Bediener verwalten die Werbetafel über ein modernes, passwortgeschütztes Web-Dashboard im lokalen Netzwerk.
- Volle Browser-Steuerung: Bilder hochladen, Medien löschen, das Wechselintervall anpassen oder den Pi per Knopfdruck sicher herunterfahren – alles ohne IT-Kenntnisse.
- Intelligente Netzwerk-Features: Beim Systemstart wird die aktuelle IP für 10 Sekunden auf dem Monitor eingeblendet. Zudem ist der Pi dank mDNS permanent über die feste Adresse
http://werbetafel.local erreichbar.
Vor Kurzem stand ich vor der spannenden Aufgabe, ein maßgeschneidertes Kundenprojekt im Bereich Digital Signage umzusetzen. Die Zielsetzung war klar definiert: Es sollte eine kostengünstige, extrem robuste und vor allem wartungsfreie digitale Werbetafel realisiert werden.
Da das System im täglichen Betrieb von wechselnden, absolut nicht-technischen Bedienern verwaltet wird, durfte die Administration keinerlei IT-Kenntnisse erfordern. Keine komplizierten Menüs, kein SSH-Terminal – das System musste vollständig selbsterklärend sein.
Die Projekt-Anforderungen im Detail:
- Hardware-Basis: Ein kompakter Raspberry Pi 4, angeschlossen an einen robusten 22″ Full-HD Monitor über HDMI.
- Betriebssystem: Ein schlankes, stabiles Linux-Fundament ohne unnötigen Desktop-Ballast, um die Hardware maximal zu schonen.
- Bedienung: Vollständig über ein intuitives Web-Interface im lokalen Netzwerk steuerbar (Bilder hochladen, Intervalle anpassen, Medien löschen).
- Ausfallsicherheit: Automatischer Kiosk-Modus direkt nach dem Einschalten. Keine sichtbaren Mauszeiger, keine Fehlermeldungen, kein Linux-Unterbau für den Betrachter sichtbar.
- Komfort-Funktionen: Ein direkter, sicherer Shutdown-Button im Webbrowser sowie eine automatische Erkennung und Anzeige der IP-Adresse beim Systemstart.
Um dieses Projekt schlank und stabil zu halten, wurde auf fertige, überladene Kiosk-Distributionen verzichtet. Stattdessen kam das KISS-Prinzip (Keep It Simple, Stupid) zum Einsatz: Ein nacktes Linux, kombiniert mit dem extrem schnellen Webserver Nginx, einer schlanken PHP-Logik und dem performanten Bildbetrachter feh.
Schritt 1: Basis-Setup und Headless-Netzwerk-Einrichtung
Da der Raspberry Pi 4 später fest hinter dem 22″ Monitor verbaut wird und ohne Tastatur oder Maus auskommen muss, richten wir das System von Anfang an im sogenannten „Headless“-Modus ein.
Betriebssystem flashen
Nutze den offiziellen Raspberry Pi Imager. Wähle als Device den Raspberry Pi 4 und als Betriebssystem das Raspberry Pi OS Lite (64-Bit). Das Lite-Image verzichtet komplett auf eine schwere Desktop-Umgebung, was dem Pi 4 eine enorme Stabilität im Dauerbetrieb verleiht. Bevor du auf „Schreiben“ klickst, öffnest du über das Zahnrad-Symbol (Erweiterte Optionen) die OS-Customization:
- Hostname festlegen:
werbetafel
- SSH aktivieren: Passwort-Authentifizierung erlauben.
- Benutzer anlegen: Erstelle einen dedizierten Admin-User, in unserem Beispiel nutzen wir den User
jaf mit einem sicheren Passwort.
- WLAN einrichten: Gib deine SSID und das WLAN-Passwort des Kunden-Netzwerks ein und setze die Wireless-LAN-Länderkennung (z.B. CH oder DE).
Nach dem ersten Booten verbindet sich der Raspberry Pi 4 automatisch mit dem WLAN. Ermittle die IP-Adresse über den Router oder greife direkt über den Hostnamen per SSH auf das Gerät zu:
ssh jaf@werbetafel.local
Schritt 2: Installation der Software-Pakete & Nginx-Konfiguration
Nachdem wir per SSH eingeloggt sind, bringen wir das Linux-System auf den neuesten Stand und installieren alle notwendigen Komponenten für den Webserver und die Grafikausgabe.
sudo apt update && sudo apt upgrade -y
sudo apt install nginx php-fpm feh xserver-xorg x11-xserver-utils xinit unclutter -y
Nginx für PHP-Verarbeitung konfigurieren
Standardmäßig liefert Nginx keine PHP-Dateien aus. Das ändern wir, indem wir die Standard-Konfiguration der Website anpassen. Zudem erhöhen wir direkt das maximale Upload-Limit, damit Kunden auch hochauflösende Werbeplakate ohne den Fehler 413 Request Entity Too Large hochladen können.
Öffne die Konfigurationsdatei:
sudo nano /etc/nginx/sites-available/default
Ersetze den Inhalt vollständig durch folgende saubere Konfiguration:
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.php index.html index.htm;
server_name _;
# Erlaubt Datei-Uploads bis zu einer Größe von 100 Megabyte
client_max_body_size 100M;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}
Speichere die Datei mit Strg+O, Enter und schließe sie mit Strg+X. Starte den Webserver neu:
sudo systemctl restart nginx
Schritt 3: Das selbsterklärende Web-Dashboard (index.php)
Das Herzstück der Verwaltung ist dieses hochprofessionelle PHP-Skript im modernen Sidebar-Design. Es schützt den Upload-Bereich durch ein sicheres Passwort (Werbetafel2026), implementiert ein automatisches Session-Timeout von 15 Minuten bei Inaktivität, erlaubt komfortable Multi-File-Uploads, steuert die Intervallzeiten des 22″ Monitors und ermöglicht das direkte Herunterfahren des Systems.
Zuerst bereiten wir das Webverzeichnis vor, sodass der Webserver-User www-data die nötigen Schreibrechte besitzt:
sudo mkdir -p /var/www/html/images
sudo rm -f /var/www/html/index.nginx-debian.html
sudo chown -R www-data:www-data /var/www/html
sudo chmod -R 755 /var/www/html
Erstelle nun die Datei /var/www/html/index.php:
sudo nano /var/www/html/index.php
Füge den folgenden Quellcode vollständig und unverändert ein:
<?php
// Session-Sicherheitseinstellungen vor dem Start setzen
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
session_start();
$password_secret = "Werbetafel2026";
$timeout_duration = 900; // 15 Minuten in Sekunden
$login_error = '';
$message = '';
// Pfade für die Steuerung
$intervalFile = __DIR__ . '/interval.txt';
$triggerFile = __DIR__ . '/tmp/reload_feh';
$stopFile = __DIR__ . '/stop_slideshow';
// Status auslesen
$currentInterval = file_exists($intervalFile) ? intval(file_get_contents($intervalFile)) : 60;
$isStopped = file_exists($stopFile);
// Tab-Verwaltung nach dem Neuladen (Standard: overview)
$activeTab = 'overview';
// Logout-Logik
if (isset($_GET['logout'])) {
session_unset();
session_destroy();
header("Location: index.php");
exit;
}
// 1. Login-Formular verarbeiten
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
if ($_POST['password'] === $password_secret) {
$_SESSION['logged_in'] = true;
$_SESSION['last_activity'] = time();
header("Location: index.php");
exit;
} else {
$login_error = "Falsches Passwort! Bitte versuche es erneut.";
}
}
// 2. Session-Timeout und Login-Status prüfen
$is_authenticated = false;
if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] < $timeout_duration)) {
$_SESSION['last_activity'] = time();
$is_authenticated = true;
} else {
session_unset();
session_destroy();
$login_error = "Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.";
}
}
// 3. Datei- und Ordnerlogik (nur ausführen, wenn eingeloggt)
$uploadDir = __DIR__ . '/images/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if ($is_authenticated) {
// Schalter für Start/Stopp verarbeiten
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['toggle_slideshow'])) {
$activeTab = 'overview';
if ($isStopped) {
if (file_exists($stopFile)) {
unlink($stopFile);
}
$isStopped = false;
$message = "Werbetafel wurde erfolgreich gestartet!";
} else {
file_put_contents($stopFile, '1');
$isStopped = true;
$message = "Werbetafel wurde pausiert. Monitor zeigt nun die Konsole.";
}
}
// System direkt über Sudo herunterfahren
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['shutdown_system'])) {
$activeTab = 'overview';
$message = "Der Raspberry Pi wird jetzt sicher heruntergefahren. Der Monitor schaltet sich in wenigen Sekunden ab.";
shell_exec('sudo /sbin/shutdown -h now > /dev/null 2>&1 &');
}
// Intervall-Formular verarbeiten
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['interval'])) {
$activeTab = 'overview';
$newInterval = intval($_POST['interval']);
if (in_array($newInterval, array(30, 60, 90, 120))) {
file_put_contents($intervalFile, $newInterval);
file_put_contents($triggerFile, '1');
$currentInterval = $newInterval;
$message = "Anzeigedauer auf " . $newInterval . " Sekunden geändert!";
}
}
// Upload-Logik für mehrere Bilder
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['werbebilder'])) {
$activeTab = 'upload';
$uploadedCount = 0;
foreach ($_FILES['werbebilder']['tmp_name'] as $key => $tmpName) {
if ($_FILES['werbebilder']['error'][$key] === UPLOAD_ERR_OK) {
$fileName = basename($_FILES['werbebilder']['name'][$key]);
$targetFilePath = $uploadDir . $fileName;
$fileType = strtolower(pathinfo($targetFilePath, PATHINFO_EXTENSION));
$allowedTypes = array('jpg', 'jpeg', 'png', 'gif', 'webp');
if (in_array($fileType, $allowedTypes)) {
if (move_uploaded_file($tmpName, $targetFilePath)) {
$uploadedCount++;
}
}
}
}
if ($uploadedCount > 0) {
$message = $uploadedCount . " Bilder wurden erfolgreich hochgeladen!";
} else {
$login_error = "Es wurden keine gültigen Bilder hochgeladen.";
}
}
// Lösch-Logik
if (isset($_GET['delete'])) {
$activeTab = 'manage';
$fileToDelete = basename($_GET['delete']);
$targetDeletePath = $uploadDir . $fileToDelete;
if ($fileToDelete !== '.' && $fileToDelete !== '..' && file_exists($targetDeletePath)) {
unlink($targetDeletePath);
$message = "Bild '" . $fileToDelete . "' wurde gelöscht.";
}
}
// Aktuelle Bilder auslesen
$images = [];
if (is_dir($uploadDir)) {
$images = array_diff(scandir($uploadDir), array('.', '..'));
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Werbetafel Control Panel</title>
<style>
:root {
--primary: #1a252f;
--secondary: #34495e;
--success: #27ae60;
--success-hover: #2ecc71;
--danger: #e74c3c;
--danger-hover: #c0392b;
--warning: #e67e22;
--warning-hover: #d35400;
--bg-main: #ffffff;
--bg-sidebar: #1a252f;
--border-color: #e2e8f0;
--text-dark: #334155;
--text-light: #f8fafc;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
margin: 0;
padding: 0;
background: var(--bg-main);
color: var(--text-dark);
display: flex;
min-height: 100vh;
}
/* LOGIN LAYOUT */
.login-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
background: #f1f5f9;
}
.login-card {
background: #ffffff;
padding: 3rem;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.05);
width: 100%;
max-width: 460px;
border: 1px solid var(--border-color);
box-sizing: border-box;
}
.login-card h1 {
margin-top: 0;
font-size: 1.6rem;
color: var(--primary);
border-bottom: 2px solid var(--border-color);
padding-bottom: 0.75rem;
text-align: center;
}
/* DASHBOARD SIDEBAR */
.sidebar {
width: 280px;
background: var(--bg-sidebar);
color: var(--text-light);
display: flex;
flex-direction: column;
border-right: 1px solid rgba(255,255,255,0.1);
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
}
.sidebar-brand {
padding: 2rem 1.5rem;
font-size: 1.2rem;
font-weight: bold;
letter-spacing: 0.5px;
border-bottom: 1px solid rgba(255,255,255,0.05);
text-align: center;
}
.sidebar-status {
padding: 1rem 1.5rem;
background: rgba(255,255,255,0.03);
border-bottom: 1px solid rgba(255,255,255,0.05);
font-size: 0.85rem;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 0.8rem;
margin-top: 5px;
}
.status-badge.active { background: var(--success); color: white; }
.status-badge.paused { background: var(--warning); color: white; }
.sidebar-menu {
list-style: none;
padding: 0;
margin: 1.5rem 0;
flex: 1;
}
.sidebar-menu li a {
display: block;
padding: 0.85rem 1.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
cursor: pointer;
border-left: 4px solid transparent;
}
.sidebar-menu li a:hover, .sidebar-menu li.active a {
color: var(--text-light);
background: rgba(255,255,255,0.05);
}
.sidebar-menu li.active a {
border-left-color: var(--success);
background: rgba(255,255,255,0.08);
}
.sidebar-footer {
padding: 1.5rem;
border-top: 1px solid rgba(255,255,255,0.05);
}
.logout-btn {
display: block;
text-align: center;
background: rgba(231, 76, 60, 0.2);
color: #f87171;
text-decoration: none;
padding: 10px;
border-radius: 6px;
font-size: 0.9rem;
font-weight: bold;
transition: background 0.2s ease;
}
.logout-btn:hover {
background: var(--danger);
color: white;
}
/* MAIN CONTENT AREA */
.main-content {
flex: 1;
margin-left: 280px;
padding: 3rem;
background: #ffffff;
box-sizing: border-box;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.content-panel {
display: none;
animation: fadeIn 0.3s ease-in-out;
max-width: 800px;
width: 100%;
}
.content-panel.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
h1 {
color: var(--primary);
font-size: 2rem;
margin-top: 0;
margin-bottom: 1.5rem;
font-weight: 600;
}
.lead-text {
color: #64748b;
margin-bottom: 2.5rem;
font-size: 1.05rem;
}
/* CARD ARCHITECTURE */
.card {
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.02);
}
.card h3 {
margin-top: 0;
margin-bottom: 1.25rem;
color: var(--secondary);
font-size: 1.2rem;
}
/* FORMS & INPUTS */
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--secondary);
font-size: 0.95rem;
}
input[type="password"], select {
width: 100%;
padding: 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
box-sizing: border-box;
background: #f8fafc;
font-size: 1rem;
color: var(--text-dark);
transition: border 0.2s ease;
margin-bottom: 1.25rem;
}
input[type="password"]:focus, select:focus {
outline: none;
border-color: var(--secondary);
background: #fff;
}
input[type="file"] {
width: 100%;
padding: 2rem;
border: 2px dashed #cbd5e1;
border-radius: 8px;
background: #f8fafc;
box-sizing: border-box;
cursor: pointer;
margin-bottom: 1.5rem;
text-align: center;
}
/* BUTTONS */
button {
width: 100%;
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
box-sizing: border-box;
}
.btn-primary { background: var(--secondary); color: white; }
.btn-primary:hover { background: var(--primary); }
.btn-success { background: var(--success); color: white; }
.btn-success:hover { background: var(--success-hover); }
.btn-danger { background: var(--danger); color: white; }
.btn-danger:hover { background: var(--danger-hover); }
.btn-warning { background: var(--warning); color: white; }
.btn-warning:hover { background: var(--warning-hover); }
/* NOTIFICATION MESSAGES */
.msg, .error-msg {
padding: 1rem;
border-radius: 6px;
font-weight: 600;
margin-bottom: 2rem;
font-size: 0.95rem;
border-left: 4px solid;
max-width: 800px;
}
.msg {
color: #166534;
background: #f0fdf4;
border-left-color: var(--success);
}
.error-msg {
color: #991b1b;
background: #fef2f2;
border-left-color: var(--danger);
}
/* MEDIA MANAGEMENT LIST */
.image-table-wrapper {
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.image-list-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 0.95rem;
}
.image-list-table th {
background: #f8fafc;
padding: 1rem;
font-weight: 600;
color: var(--secondary);
border-bottom: 1px solid var(--border-color);
}
.image-list-table td {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
color: #475569;
}
.image-list-table tr:last-child td {
border-bottom: none;
}
.btn-table-delete {
background: #fee2e2;
color: var(--danger);
padding: 6px 12px;
font-size: 0.85rem;
border-radius: 4px;
text-decoration: none;
font-weight: bold;
transition: all 0.2s ease;
}
.btn-table-delete:hover {
background: var(--danger);
color: white;
}
/* SYSTEM FOOTER */
.footer-wrapper {
margin-top: auto;
padding-top: 4rem;
max-width: 800px;
width: 100%;
}
hr {
border: 0;
border-top: 1px solid var(--border-color);
margin-bottom: 1.5rem;
}
.footer {
text-align: center;
font-size: 0.85rem;
color: #94a3b8;
}
.footer a {
color: var(--primary);
text-decoration: none;
font-weight: bold;
}
.footer a:hover {
text-decoration: underline;
}
.footer-logo {
display: block;
max-width: 120px;
height: auto;
margin: 1rem auto 0 auto;
opacity: 0.8;
}
</style>
</head>
<body>
<?php if (!$is_authenticated): ?>
<!-- UNAUTHENTICATED: LOGIN DIALOG -->
<div class="login-wrapper">
<div class="login-card">
<h1>Werbetafel Administration</h1>
<?php if (!empty($login_error)): ?>
<div class="error-msg"><?php echo htmlspecialchars($login_error); ?></div>
<?php endif; ?>
<form action="index.php" method="post">
<label for="password">Passwort erforderlich:</label>
<input type="password" name="password" id="password" required autofocus>
<button type="submit" class="btn-success">Anmelden</button>
</form>
</div>
</div>
<?php else: ?>
<!-- AUTHENTICATED: CONTROL PANEL DASHBOARD -->
<!-- SIDEBAR NAVIGATION -->
<div class="sidebar">
<div class="sidebar-brand">
DBH_ITSYSTEMS
</div>
<div class="sidebar-status">
System-Status:<br>
<?php if ($isStopped): ?>
<span class="status-badge paused">Pausiert (Terminal)</span>
<?php else: ?>
<span class="status-badge active">Live-Slideshow</span>
<?php endif; ?>
</div>
<ul class="sidebar-menu">
<li id="menu-overview" class="<?php echo $activeTab === 'overview' ? 'active' : ''; ?>">
<a onclick="switchTab('overview')">Anzeige & Steuerung</a>
</li>
<li id="menu-upload" class="<?php echo $activeTab === 'upload' ? 'active' : ''; ?>">
<a onclick="switchTab('upload')">Bilder hochladen</a>
</li>
<li id="menu-manage" class="<?php echo $activeTab === 'manage' ? 'active' : ''; ?>">
<a onclick="switchTab('manage')">Medien verwalten (<?php echo count($images); ?>)</a>
</li>
</ul>
<div class="sidebar-footer">
<a href="?logout=1" class="logout-btn">Abmelden</a>
</div>
</div>
<!-- MAIN INTERFACE VIEW CONTENT -->
<div class="main-content">
<?php if (!empty($message)): ?>
<div class="msg"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?>
<?php if (!empty($login_error)): ?>
<div class="error-msg"><?php echo htmlspecialchars($login_error); ?></div>
<?php endif; ?>
<!-- TAB 1: OVERVIEW & STEUERUNG -->
<div id="panel-overview" class="content-panel <?php echo $activeTab === 'overview' ? 'active' : ''; ?>">
<h1>Anzeige & Steuerung</h1>
<p class="lead-text">Steuere den Live-Status der Monitor-Ausgabe und passe das Wechselintervall an.</p>
<div class="card">
<h3>Präsentations-Modus</h3>
<form action="index.php" method="post">
<input type="hidden" name="toggle_slideshow" value="1">
<?php if ($isStopped): ?>
<p style="margin-bottom: 1.5rem; color: #64748b;">Der Präsentationsmodus ist aktuell deaktiviert. Der angeschlossene Monitor zeigt das Linux-Terminal an.</p>
<button type="submit" class="btn-success">Werbetafel jetzt aktivieren</button>
<?php else: ?>
<p style="margin-bottom: 1.5rem; color: #64748b;">Die Werbetafel läuft aktuell im Vollbildmodus auf dem angeschlossenen Monitor.</p>
<button type="submit" class="btn-warning">Werbetafel pausieren / Konsole freigeben</button>
<?php endif; ?>
</form>
</div>
<div class="card">
<h3>Geschwindigkeit</h3>
<form action="index.php" method="post">
<label for="interval">Wechselintervall der Bilder:</label>
<select name="interval" id="interval">
<option value="30" <?php echo $currentInterval == 30 ? 'selected' : ''; ?>>30 Sekunden</option>
<option value="60" <?php echo $currentInterval == 60 ? 'selected' : ''; ?>>60 Sekunden (Standard)</option>
<option value="90" <?php echo $currentInterval == 90 ? 'selected' : ''; ?>>90 Sekunden</option>
<option value="120" <?php echo $currentInterval == 120 ? 'selected' : ''; ?>>120 Sekunden</option>
</select>
<button type="submit" class="btn-primary">Intervall speichern</button>
</form>
</div>
<div class="card">
<h3>System-Optionen</h3>
<form action="index.php" method="post" onsubmit="return confirm('Möchtest du den Raspberry Pi wirklich komplett herunterfahren?');">
<input type="hidden" name="shutdown_system" value="1">
<p style="margin-bottom: 1.5rem; color: #64748b;">Schaltet den Raspberry Pi ordnungsgemäß und sicher aus. Nach dem Klicken schaltet sich das System innerhalb weniger Sekunden ab.</p>
<button type="submit" class="btn-danger">Raspberry Pi herunterfahren</button>
</form>
</div>
</div>
<!-- TAB 2: UPLOAD -->
<div id="panel-upload" class="content-panel <?php echo $activeTab === 'upload' ? 'active' : ''; ?>">
<h1>Bilder hochladen</h1>
<p class="lead-text">Füge neue Ankündigungen oder Werbebilder zur Diashow hinzu. Mehrfachauswahl ist möglich.</p>
<div class="card">
<h3>Medien-Upload</h3>
<form action="index.php" method="post" enctype="multipart/form-data">
<label for="werbebilder">Dateien auswählen (JPG, PNG, GIF, WEBP):</label>
<input type="file" name="werbebilder[]" id="werbebilder" multiple accept="image/jpeg, image/png, image/gif, image/webp" required>
<button type="submit" class="btn-success">Dateien an den Raspberry übertragen</button>
</form>
</div>
</div>
<!-- TAB 3: MANAGE -->
<div id="panel-manage" class="content-panel <?php echo $activeTab === 'manage' ? 'active' : ''; ?>">
<h1>Medien verwalten</h1>
<p class="lead-text">Hier siehst du alle Bilder, die sich aktuell in der Schleife befinden. Du kannst alte Einträge dauerhaft entfernen.</p>
<div class="card" style="padding: 1rem 0;">
<?php if (empty($images)): ?>
<p style="padding: 0 2rem; color: #64748b;">Aktuell sind keine Bilder im System hinterlegt. Bitte lade zuerst Medien hoch.</p>
<?php else: ?>
<div class="image-table-wrapper">
<table class="image-list-table">
<thead>
<tr>
<th>Dateiname</th>
<th style="width: 120px; text-align: center;">Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($images as $img): ?>
<tr>
<td><?php echo htmlspecialchars($img); ?></td>
<td style="text-align: center;">
<a class="btn-table-delete" href="?delete=<?php echo urlencode($img); ?>" onclick="return confirm('Möchtest du dieses Bild wirklich dauerhaft von der Werbetafel löschen?');">Löschen</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- GLOBAL SYSTEM FOOTER -->
<div class="footer-wrapper">
<hr>
<div class="footer">
<p>Developed by <a href="https://blake-hofer.net" target="_blank">Dominique Blake-Hofer, DBHIT_Systems</a></p>
<img src="DBH_Systems.jpg" alt="DBH_Systems Logo" class="footer-logo">
</div>
</div>
</div>
<!-- TABS LIVE LOGIC -->
<script>
function switchTab(tabId) {
document.querySelectorAll('.content-panel').forEach(function(panel) {
panel.classList.remove('active');
});
document.querySelectorAll('.sidebar-menu li').forEach(function(li) {
li.classList.remove('active');
});
document.getElementById('panel-' + tabId).classList.add('active');
document.getElementById('menu-' + tabId).classList.add('active');
}
</script>
<?php endif; ?>
</body>
</html>
Schritt 4: Autostart, Kiosk-Modus und der Hintergrund-Watchdog (~/.profile)
Herkömmliche Linux-Systeme starten X-Server über Display-Manager. Das verbraucht beim Raspberry Pi 4 im reinen Kiosk-Betrieb unnötigen RAM und verlangsamt den Boot-Prozess.
Wir nutzen stattdessen den extrem zuverlässigen Console Autologin-Weg. Der Pi loggt sich beim Start direkt auf der Textkonsole (tty1) ein und triggert über das Benutzerprofil die Kiosk-Schleife im Vollbildmodus.
Console Autologin aktivieren
Rufe auf dem Pi das Konfigurationstool auf:
sudo raspi-config
Navigiere zu: 1 System Options -> S5 Boot / Auto Login -> B2 Console Autologin (Textkonsole mit automatischer Anmeldung des gesetzten Users). Bestätige mit Enter und verlasse das Menü ohne sofortigen Reboot.
Die Autostart-Schleife in der .profile hinterlegen
Öffne die Profilkonfiguration deines Kiosk-Users (z.B. /home/jaf/.profile):
nano ~/.profile
Füge ganz am Ende der Datei folgenden Shell-Code ein:
# Werbetafel automatisch starten, wenn auf tty1 eingeloggt
if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
# Aktuelle IP-Adresse ermitteln
IP_ADDR=$(hostname -I | awk '{print $1}')
# IP-Adresse für 10 Sekunden groß auf dem Monitor einblenden
clear
echo "========================================================"
echo " DIGITAL SIGNAGE SYSTEM - MONITOR SYSTEM START"
echo "========================================================"
echo ""
echo " AKTUELLER NETZWERK-ZUGRIFF:"
echo " -> IP-Adresse: http://$IP_ADDR"
echo " -> Name im Netz: http://werbetafel.local"
echo ""
echo "========================================================"
echo " Die Bilderschleife startet automatisch in 10 Sekunden..."
sleep 10
# Hintergrund-Watchdog: Schießt feh ab, wenn pausiert wird oder ein Intervall-Reload kommt
(
while true; do
if [ -f /var/www/html/stop_slideshow ]; then
pkill xinit 2>/dev/null
pkill feh 2>/dev/null
elif [ -f /tmp/reload_feh ]; then
rm -f /tmp/reload_feh
pkill feh 2>/dev/null
fi
sleep 2
done
) &
# Endlosschleife für die Diashow
while true; do
if [ -f /var/www/html/stop_slideshow ]; then
sleep 2
else
INTERVAL=$(cat /var/www/html/interval.txt 2>/dev/null)
# Sicherheitsprüfung: Wenn INTERVAL leer ist oder keine Zahl, setze es hart auf 60
if ! [[ "$INTERVAL" =~ ^[0-9]+$ ]]; then
INTERVAL=60
fi
xinit /usr/bin/feh -F -Z -Y -D $INTERVAL -R 60 /var/www/html/images/ -- :0 -nocursor -s 0 dpms
sleep 1
fi
done
fi
Schritt 5: PHP-Limits anpassen (Das Fundament für große Bilddateien)
Nginx blockiert standardmäßig alles, was über 1 MB groß ist. PHP limitiert Uploads nativ auf 2 MB. Da moderne Werbe-Grafiken oder Plakate als hochauflösende PNGs schnell größere Datenmengen erreichen können, brechen wir diese Limits gezielt auf.
Die Anpassung in Nginx haben wir bereits erledigt (client_max_body_size 100M;). Nun bringen wir PHP auf denselben Stand. Anstatt die unübersichtliche Standard-php.ini manuell zu editieren, legen wir eine eigene Override-Datei im Konfigurationsbaum von PHP-FPM an.
Erstelle die Override-Datei (angepasst an die standardmäßige PHP 8.2 Version unter aktuellen Raspberry Pi OS Bookworm-Systemen):
sudo nano /etc/php/8.2/fpm/conf.d/99-upload-limits.ini
Füge folgende Konfiguration ein:
upload_max_filesize = 100M
post_max_size = 100M
Starte den PHP-FPM Dienst neu, damit die Änderungen live gehen:
sudo systemctl restart php8.2-fpm
Schritt 6: Passwortlose Sudo-Rechte für den sicheren Shutdown
Damit die nicht-technischen Bediener den Raspberry Pi 4 am Ende des Tages über das Web-Dashboard sicher herunterfahren können, führt PHP im Hintergrund den Befehl sudo /sbin/shutdown -h now aus.
Aus Sicherheitsgründen fordert Linux bei sudo-Aufrufen eine Passworteingabe. Da der Webserver unter dem Systemuser www-data läuft, würde der Befehl blockieren, da im Hintergrund niemand ein Passwort eintippen kann.
Wir lösen das über eine restriktive Ausnahme im Sudoers-System von Linux. Der Webserver darf ausschließlich diesen einen, spezifischen Befehl ohne Passwort ausführen. Alles andere bleibt streng verriegelt.
Führe folgenden Befehl im Terminal aus, um die Ausnahmeregel anzulegen:
echo "www-data ALL=(ALL) NOPASSWD: /sbin/shutdown" | sudo tee /etc/sudoers.d/www-data-shutdown
Das System validiert die Syntax automatisch. Ab jetzt schaltet der Button im Dashboard das System absolut verzögerungsfrei ab.
Schritt 7: Den Netzwerknamen permanent einrichten (mDNS)
Niemand möchte sich im täglichen Betrieb kryptische IP-Adressen merken müssen, die sich nach einem Router-Neustart eventuell verändern.
Dank des mDNS-Dienstes (Avahi-Daemon) auf dem Raspberry Pi 4 ist das System im Netzwerk immer unter seinem Hostnamen erreichbar. Wir taufen den Raspberry Pi abschließend um:
sudo hostnamectl set-hostname werbetafel
Führe nun einen vollständigen Systemneustart durch:
sudo reboot
Das Endergebnis im Live-Betrieb
Sobald der Pi hochfährt, sieht der Bediener für 10 Sekunden den Begrüßungsbildschirm mitsamt der IP-Anzeige auf dem 22″ Monitor. Danach schaltet das System in den Vollbildmodus und startet die Diashow der hochgeladenen Grafiken.
Jeder im lokalen Netzwerk kann nun auf seinem PC, Tablet oder Smartphone einen Browser öffnen und folgende selbsterklärende Adresse aufrufen:
http://werbetafel.local
Nach der Eingabe des Passworts Werbetafel2026 steht das vollflächige Dashboard zur Verfügung. Das Projekt zeigt eindrucksvoll, wie sich mit minimalem Software-Einsatz eine hochprofessionelle, ausfallsichere und absolut bedienerfreundliche Digital-Signage-Lösung realisieren lässt.