Raspberry Pi

Raspberry Pi MQTT Display: Touchscreen-Bedienpanel mit Python bauen

Bauen Sie ein Vollbild-Touchscreen-Bedienpanel auf einem Raspberry Pi. Tasten veröffentlichen MQTT-Befehle, der Bildschirm zeigt den echten Gerätezustand aus Retained Messages, und das Panel startet automatisch beim Booten.

Raspberry Pi Touch Display mit Tasten und Gerätezuständen als Raspberry Pi MQTT Display Bedienpanel
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

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, OFF oder OFFLINE.
  • 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. TOGGLE ist für Leuchten bequem. Bei einer Heizung oder einem Tor verhindern getrennte ON- und OFF-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.

Verwandte Artikel

Raspberry Pi RFID MQTT: Zutrittsprotokollierung mit dem RC522-Leser

Schließen Sie einen RC522-RFID-Leser an einen Raspberry Pi an, lesen Sie Tags mit Python, prüfen Sie sie gegen eine lokale Allowlist und veröffentlichen Sie jeden Zutrittsversuch als MQTT-Ereignis, das Logger, Dashboards und Alarmwerkzeuge verarbeiten können.

7 Min. Lesezeit

Pico W vs. Pico 2 W für MQTT: RP2040 vs. RP2350 bei Leistung, Speicher und TLS

Der Pico 2 W behält Formfaktor und WLAN-Chip des Pico W, ersetzt aber den RP2040 durch den schnelleren RP2350. Dieser Vergleich zeigt, was das für MQTT-Geräte bedeutet: CPU, Speicher, TLS, Sicherheitsfunktionen, Softwareunterstützung und welches Board für welches Projekt.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.