Ein Raspberry Pi MQTT Display ist ein Touchscreen-Panel, das anzeigt, was Ihre Geräte gerade tun, und mit dem Sie sie per Fingertipp steuern. In diesem Tutorial bauen Sie eines in Python mit Tkinter und paho-mqtt: Tasten veröffentlichen Befehle, Beschriftungen aktualisieren sich über die Zustands-Topics der Geräte selbst, und das Panel startet im Vollbild, sobald der Pi bootet. Das Muster funktioniert mit Relais, Leuchten, Pumpen oder allem anderen, das MQTT spricht.
Befehls-Topics vs. Zustands-Topics
Der häufigste Fehler bei selbst gebauten Bedienpanels: Die Tastenfarbe ändert sich in dem Moment, in dem sie gedrückt wird. Das zeigt, was Sie angefordert haben, nicht, was tatsächlich passiert ist. Ist die Relaisplatine offline, lügt das Panel.
Das robuste Muster trennt zwei Topics pro Gerät:
| Topic | Wer veröffentlicht | Retained | Zweck |
|---|---|---|---|
.../<device>/set |
Das Panel (oder ein beliebiger Controller) | Nein | Eine Anforderung: ON, OFF, TOGGLE |
.../<device>/state |
Das Gerät selbst | Ja | Die Wahrheit: ON oder OFF |
.../<device>/status |
Das Gerät, plus Last Will | Ja | online oder offline |
Das Panel veröffentlicht an set und ändert seine Anzeige erst, wenn auf state eine Nachricht eintrifft. Weil state retained ist, erfährt das Panel den aktuellen Zustand jedes Geräts sofort beim Abonnieren, auch direkt nach einem Neustart. Befehle sind bewusst nicht retained, sonst würde ein Gerät nach der Wiederverbindung einen alten Befehl erneut ausführen. Der Leitfaden zu Retained Messages erklärt, warum diese Trennung wichtig ist.
Falls Sie noch keine Geräte haben: Das Tutorial Raspberry Pi Relais über MQTT baut die Geräteseite, die dieses Panel steuert.
Hardware
- Raspberry Pi 4 oder 5, oder ein Pi 3B+ für ein einfaches Panel.
- Das offizielle Raspberry Pi Touch Display (7 Zoll oder Touch Display 2) oder ein beliebiger HDMI-Bildschirm mit USB-Touch.
- Raspberry Pi OS mit Desktop (Bookworm). Tkinter läuft auf dem Wayland-Desktop über Xwayland ohne zusätzliche Konfiguration.
Abhängigkeiten installieren
sudo apt update
sudo apt install -y python3-tk python3-venv
python3 -m venv --system-site-packages ~/panel-env
~/panel-env/bin/pip install "paho-mqtt>=2.0"
Das Flag --system-site-packages erlaubt der virtuellen Umgebung, das Tkinter-Paket des Systems zu nutzen, das sich nicht über pip installieren lässt.
Die Threading-Regel, die Tkinter stabil hält
loop_start() von paho-mqtt erledigt die Netzwerkverarbeitung in einem Hintergrund-Thread, und die Callbacks laufen auf diesem Thread. Tkinter ist nicht threadsicher: Werden Widgets aus einem anderen Thread angefasst, kommt es zu zufälligen Einfrierern und Abstürzen, oft erst Tage später. Die Lösung ist eine Queue. MQTT-Callbacks legen Nachrichten nur in eine queue.Queue, und die Tkinter-Hauptschleife leert die Queue alle 100 ms mit root.after(). Alle Widget-Aktualisierungen finden auf dem GUI-Thread statt.
Der Code für das Raspberry Pi MQTT Display
Speichern Sie ihn als ~/panel.py:
#!/usr/bin/env python3
"""Touchscreen MQTT control panel for Raspberry Pi."""
import json
import os
import queue
import ssl
import tkinter as tk
import paho.mqtt.client as mqtt
HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME") # YOUR_USERNAME
PASSWORD = os.getenv("MQTT_PASSWORD") # YOUR_PASSWORD
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", "panel-e40b77") # YOUR_CLIENT_ID
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/e40b77/home")
DEVICES = [
("hall_light", "Hall light"),
("garden_pump", "Garden pump"),
("garage_door", "Garage door"),
("heater", "Heater"),
]
SENSOR_TOPIC = f"{PREFIX}/living/climate"
COLORS = {"ON": "#2e7d32", "OFF": "#37474f", "UNKNOWN": "#5d4037", "OFFLINE": "#b71c1c"}
inbox = queue.Queue()
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
inbox.put(("_conn", f"error: {reason_code}"))
return
subs = [(f"{PREFIX}/{d}/state", 1) for d, _ in DEVICES]
subs += [(f"{PREFIX}/{d}/status", 1) for d, _ in DEVICES]
subs.append((SENSOR_TOPIC, 0))
client.subscribe(subs)
inbox.put(("_conn", "connected"))
def on_disconnect(client, userdata, flags, reason_code, properties):
inbox.put(("_conn", "reconnecting"))
def on_message(client, userdata, msg):
inbox.put((msg.topic, msg.payload.decode(errors="replace")))
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id=CLIENT_ID, protocol=mqtt.MQTTv5)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
client.reconnect_delay_set(min_delay=1, max_delay=30)
client.connect_async(HOST, PORT, keepalive=30)
client.loop_start()
# ---------- GUI ----------
root = tk.Tk()
root.title("Control Panel")
root.configure(bg="#121212")
root.attributes("-fullscreen", True)
root.bind("<Escape>", lambda e: root.destroy())
header = tk.Frame(root, bg="#121212")
header.pack(fill="x", padx=20, pady=(16, 8))
climate_lbl = tk.Label(header, text="-- °C --%", font=("DejaVu Sans", 28),
fg="white", bg="#121212")
climate_lbl.pack(side="left")
conn_lbl = tk.Label(header, text="connecting", font=("DejaVu Sans", 16),
fg="#ffb300", bg="#121212")
conn_lbl.pack(side="right")
grid = tk.Frame(root, bg="#121212")
grid.pack(expand=True, fill="both", padx=20, pady=20)
for c in range(2):
grid.columnconfigure(c, weight=1)
for r in range((len(DEVICES) + 1) // 2):
grid.rowconfigure(r, weight=1)
buttons = {}
state = {d: "UNKNOWN" for d, _ in DEVICES}
online = {d: True for d, _ in DEVICES}
def send_toggle(dev):
client.publish(f"{PREFIX}/{dev}/set", "TOGGLE", qos=1)
buttons[dev].configure(text=f"{dict(DEVICES)[dev]}\n...") # ausstehend, nicht angenommen
def render(dev):
label = dict(DEVICES)[dev]
if not online[dev]:
text, color = f"{label}\nOFFLINE", COLORS["OFFLINE"]
else:
text, color = f"{label}\n{state[dev]}", COLORS.get(state[dev], COLORS["UNKNOWN"])
buttons[dev].configure(text=text, bg=color, activebackground=color)
for i, (dev, label) in enumerate(DEVICES):
b = tk.Button(grid, text=f"{label}\n--", font=("DejaVu Sans", 26, "bold"),
fg="white", bg=COLORS["UNKNOWN"], relief="flat", bd=0,
highlightthickness=0, command=lambda d=dev: send_toggle(d))
b.grid(row=i // 2, column=i % 2, sticky="nsew", padx=10, pady=10)
buttons[dev] = b
def pump_inbox():
while True:
try:
topic, payload = inbox.get_nowait()
except queue.Empty:
break
if topic == "_conn":
conn_lbl.configure(text=payload,
fg="#66bb6a" if payload == "connected" else "#ffb300")
continue
if topic == SENSOR_TOPIC:
try:
d = json.loads(payload)
climate_lbl.configure(text=f"{d.get('t', '--')} °C {d.get('rh', '--')}%")
except ValueError:
pass
continue
parts = topic.split("/")
dev, kind = parts[-2], parts[-1]
if dev not in state:
continue
if kind == "state":
state[dev] = payload.strip().upper()
elif kind == "status":
online[dev] = payload.strip().lower() == "online"
render(dev)
root.after(100, pump_inbox)
root.after(100, pump_inbox)
try:
root.mainloop()
finally:
client.loop_stop()
client.disconnect()
Eine gedrückte Taste zeigt ..., bis das Gerät seinen neuen Zustand bestätigt. Kommt keine Bestätigung, bleibt die Markierung für „ausstehend“ stehen und signalisiert dem Benutzer, dass etwas nicht stimmt, statt es zu verbergen. Tasten von Geräten, deren Status-Topic offline meldet, werden rot. Das Fenster läuft im Vollbild ohne Titelleiste; zum Beenden während des Testens drücken Sie Escape auf einer Tastatur.
Ein Panel gestalten, das sich wirklich bedienen lässt
Ein Bedienpanel an der Wand wird von Menschen in Eile benutzt, oft mit nassen Händen oder Handschuhen. Deshalb zählen einige Layoutregeln mehr als visueller Feinschliff:
- Große Bedienflächen. Finger sind weit ungenauer als eine Maus. Das Raster oben gibt jedem Gerät auf einem 7-Zoll-Display ein Viertel des Bildschirms, was ungefähr passt. Packen Sie nicht mehr als sechs bis acht Geräte auf eine Seite.
- Farbe plus Text. Grün und Grau vermitteln den Zustand schnell, doch manche Benutzer können Farben nicht unterscheiden. Der Tastentext zeigt daher immer zusätzlich
ON,OFFoderOFFLINE. - Ausstehende Zustände ehrlich zeigen. Die Markierung
...nach einem Tippen sagt dem Benutzer, dass der Befehl gesendet wurde und das Panel auf das Gerät wartet. Das verhindert wiederholtes Tippen, das ein Gerät zweimal umschaltet. - Verbindungsanzeige sichtbar halten. Ist das Panel selbst offline, kann jeder angezeigte Zustand veraltet sein. Die Beschriftung im Kopfbereich macht das deutlich.
- Explizite Befehle für riskante Geräte.
TOGGLEist für Leuchten bequem. Bei einer Heizung oder einem Tor verhindern getrennteON- undOFF-Tasten oder ein Bestätigungsschritt eine versehentliche Aktivierung.
Mit simulierten Geräten testen
Sie können das gesamte Panel testen, bevor überhaupt Hardware existiert. Öffnen Sie auf einem anderen Rechner ein Terminal und geben Sie sich als Flurlicht aus, indem Sie einen retained Zustand veröffentlichen:
mosquitto_pub -h broker.connectmq.com -p 1883 -i sim-e40b77 -r -q 1 \
-t connectmq/demo/e40b77/home/hall_light/state -m ON
Die Taste wird grün. Beobachten Sie nun die Befehle, die das Panel sendet:
mosquitto_sub -h broker.connectmq.com -p 1883 -i spy-e40b77 \
-t 'connectmq/demo/e40b77/home/+/set' -v
Tippen Sie auf die Taste für das Flurlicht, und TOGGLE erscheint. Veröffentlichen Sie OFF auf das Zustands-Topic, um den Kreis zu schließen. Denken Sie daran, dass der öffentliche MQTT-Broker für alle offen ist: Wer Ihr Topic-Präfix errät, könnte ebenfalls Ihre Tasten drücken. Für eine Demo ist das in Ordnung, für ein Garagentor inakzeptabel.
Das Panel beim Booten starten
Lassen Sie den Pi mit automatischer Anmeldung in den Desktop booten (sudo raspi-config, System Options, Boot / Auto Login, Desktop Autologin) und deaktivieren Sie unter Display Options die Bildschirmabschaltung, wenn das Panel immer sichtbar sein soll. Fügen Sie dann einen Autostart-Eintrag hinzu. Auf dem labwc-Desktop aktueller Bookworm-Images:
mkdir -p ~/.config/labwc
echo "/home/pi/panel-env/bin/python /home/pi/panel.py &" >> ~/.config/labwc/autostart
Auf älteren Bookworm-Versionen mit Wayfire tragen Sie denselben Befehl in einen Abschnitt [autostart] in ~/.config/wayfire.ini ein. Ersetzen Sie pi durch Ihren Benutzernamen.
Ein Raspberry Pi MQTT Display für echte Geräte absichern
Ein Bedienpanel veröffentlicht Befehle, die physische Dinge bewegen. Es braucht daher einen privaten Broker mit Authentifizierung und Verschlüsselung. Ein verwalteter MQTT-Broker wie ConnectMQ bietet genau das, ohne dass Broker-Software auf dem Pi laufen muss. Legen Sie im Portal einen Broker an, kopieren Sie den generierten Benutzernamen, das Passwort und die Client ID und starten Sie das Panel mit:
export MQTT_PORT=8883
export MQTT_USERNAME=YOUR_USERNAME
export MQTT_PASSWORD=YOUR_PASSWORD
export MQTT_CLIENT_ID=YOUR_CLIENT_ID
export MQTT_PREFIX=home
/home/pi/panel-env/bin/python /home/pi/panel.py
Legen Sie dieselben Variablen für den Autostart in ein kleines Wrapper-Skript. ConnectMQ bindet jede Client ID an ihre Zugangsdaten, daher brauchen das Panel und jedes Gerät eigene Zugangsdaten. Das bedeutet auch, dass sich mit einem geleakten Gerätepasswort nicht das Panel imitieren lässt. Im kostenlosen Plan ist QoS auf 1 begrenzt, was genau dem entspricht, was dieses Panel nutzt.
Sorgen Sie auch am Gerät für Sicherheit. Eine Garagentorsteuerung sollte TOGGLE ignorieren, wenn ihre eigenen Sensoren ein Hindernis melden, unabhängig davon, wer den Befehl gesendet hat. Das Panel ist eine Komfortfunktion, kein Sicherheitssystem.
Fehlerbehebung
| Symptom | Ursache | Lösung |
|---|---|---|
| Fenster friert nach Stunden ein | Widgets aus dem MQTT-Thread aktualisiert | Widgets nur in pump_inbox anfassen |
Tasten bleiben braun (UNKNOWN) |
Gerät veröffentlicht keinen retained Zustand | Zustand mit Retain veröffentlichen |
| Tippen hat keine sichtbare Wirkung | Gerät offline oder falsches Topic | Das set-Topic mit mosquitto_sub beobachten |
_tkinter.TclError: no display |
Per SSH ohne Desktop gestartet | Autostart nutzen oder export DISPLAY=:0 |
| Verbindung wechselt ständig | Dieselbe Client ID doppelt verwendet | Jeder Client braucht eine eindeutige ID |
Starten Sie mit ConnectMQ
Bevor Ihr Panel etwas Echtes steuert, ziehen Sie es auf einen privaten Broker um: Registrieren Sie sich unter app.connectmq.com für den kostenlosen Serverless-Plan, ohne Kreditkarte, und prüfen Sie Ihre Geräte-Topics mit dem MQTT-Client des Portals im Browser. Für einen reinen Anzeigebildschirm lesen Sie das Raspberry Pi Kiosk-Dashboard, für die Grundlagen hinter diesem Projekt das Raspberry Pi MQTT Tutorial.
Häufig gestellte Fragen
Warum Tkinter statt einer Webseite für den Touchscreen?
Tkinter wird mit Python ausgeliefert, startet in einer Sekunde und braucht sehr wenig Speicher, was älteren Pis entgegenkommt. Ein browserbasiertes Panel ist ebenfalls eine gute Wahl und leichter zu gestalten, wenn Sie HTML bereits beherrschen.
Können mehrere Panels dieselben Geräte steuern?
Ja. Jedes Panel abonniert dieselben retained Zustands-Topics, sodass alle automatisch synchron bleiben. Jedes braucht eine eigene Client ID.
Was passiert, wenn der Pi beim Tastendruck das Netzwerk verliert?
Der Befehl erreicht das Gerät möglicherweise nie, aber die Taste zeigt die Markierung „ausstehend“, bis eine Zustandsbestätigung eintrifft. Der Benutzer sieht, dass nichts passiert ist, und kann erneut tippen, sobald das Panel wieder „connected“ anzeigt.
Funktioniert Tkinter mit dem Wayland-Desktop unter Bookworm?
Ja, es läuft automatisch über Xwayland. Touch-Eingaben funktionieren bei Tasten genauso wie Mausklicks.



