Heißkaltes Farbspiel - MQTT Part IV

Heißkaltes Farbspiel - MQTT Part IV

Nachdem du letztes Mal einen LED-Streifen mit coolen Animationen bespielt hast, geht es in diesem Artikel darum Messdaten in Steuerbefehle zu verwandeln. Wir werden die Farbe vom LED-Streifen von der Temperatur und die Animationsgeschwindigkeit von der Luftfeuchtigkeit abhängig machen.

Bearbeitungszeit: 2 - 4 Stunden

Lernziele:

  • ESP32 mit MQTTS (Zertifikat auf den ESP bringen)
  • DHT11/DHT22 Temperatur- und Luftfeuchtesensor anschließen und auslesen
  • Python-Skript um MQTT zu subscriben und publishen
  • Und natürlich LEUCHT (s. gif)
Gif-Animation, die einen LED-Streifen zeigt der zunächst grün blinkt. In eine Espressotasse wird Wasser eingegossen. Weitere Elektronik im Bild (Sensor) wird über die Tasse gehalten. Das Blinken wird schneller und die Farbe wechselt langsam zu rot.

Die gesamte Blogreihe befasst sich mit der Frage, wie du Geräte zuhause per MQTT miteinander vernetzen kannst. Hier findest du nochmal die vergangenen Beiträge:

Teil 1 - MQTT auf dem Raspberry Pi: https://plantprogrammer.de/mqtt-auf-dem-raspberry-pi-mosquitto/
Teil 2 - MQTT-Kommunikation, aber sicher: TLS und Berechtigungen: https://plantprogrammer.de/mqtt-auf-dem-raspberry-pi-mosquitto-part-ii/
Teil 3 - Es Werde Licht mit McLighting
https://plantprogrammer.de/mqtt-auf-dem-raspberry-pi-mosquitto-part-iii/

Hardwarevoraussetzungen

Dieser Beitrag geht davon aus, dass der LED-Streifen aus Teil 3 und Raspberry Pi mit Mosquitto aus Teil 1 und Teil 2 vorhanden und installiert sind. Zusätzliche Hardware, die notwendig ist für diesen Teil:

eingetragene Preise waren aktuell zum Zeitpunkt des Schreibens (6. April 2021)

Als Amazon-Partner verdiene ich an qualifizierten Verkäufen, wenn du die Links verwendest und die Produkte kaufst. Das kostet nicht mehr als wenn du sie einfach so kaufst und hilft mir mehr Zeit für coole Tutorials aufzubringen. Wenn du das nicht möchtest, sind daneben jeweils die Links ohne Partnerprogramm gelistet.

Softwarevoraussetzung

  • Arduino IDE installiert (https://arduino.cc)
  • Paho MQTT library (Übersicht: https://www.eclipse.org/paho/index.php?page=clients/c/embedded/index.php) Der Download heißt "Arduino client library 1.0.0" auf der Downloadseite
    Zur Installation lade die .zip Datei herunter und füge sie über Sketch->Include Library->Add .ZIP Library (s. Abbildung 1a)
  • DHT sensor library by Adafruit
  • Boarddefinitionen für ESP32 in Arduino IDE
    Dafür öffnest du die Preferences und klickst hinter Additional Board Manager URLs auf den Button. Im neuen Fenster kannst du die URL https://dl.espressif.com/dl/package_esp32_index.json als neue Zeile hinzufügen (Abb. 1b).
  • Ein Texteditor wie Visual Studio Code ist hilfreich, aber optional
Abb. 1a: Arduino IDE Menü um Libraries hinzuzufügen.
Abb. 1b: Dialog um neue Boardmanager (in diesem Fall ESP32) in den Preferences hinzuzufügen

Zertifikate

Falls noch nicht geschehen, ist der erste Schritt ein Zertifikat für den ESP zu erstellen, indem du einen Key und einen Certificate Sign Request erstellst und diesen dann mit deinem CA-Zertifikat und -Key unterschreibst um das Client Zertifikat zu erhalten. Der Vorgang ist in Teil 2 dieser Blogpost-Reihe beschrieben.

Im Folgenden wirst du einen Client-Key brauchen (ich nenne diesen client_dht.key), ein Client-Zertifikat (client_dht.crt) und das CA-Zertifikat, dass dieses unterschrieben hat (ca.crt)

Client-Zertifikate haben das Format

-----BEGIN TRUSTED CERTIFICATE-----
AAAAABBBBBBBCCCCCCCDDDDDDDDEEEEEEFFFFFFFGGGGGGGG
...
AAAAABBBBBBBCCCCCCCDDDDDDDDEEEEE
-----END TRUSTED CERTIFICATE-----

Das CA-Zertifikat hat kein TRUSTED und der Client-Key beginnt stattdessen mit BEGIN RSA PRIVATE KEY.

Da diese Zertifikate gleich als Text in den Arduino Code eingebunden werden soll, benötigst du es in einem anderen Format. Jede Zeile muss in doppelte Anführungszeichen gefasst werden und innerhalb der Anführungszeichen am Ende mit einem Newline-Character (\n) versehen werden. Der einfachste Weg ist, die Zertifikate und den Key in Visual Studio Code zu öffnen und mit Suchen und Ersetzen zu arbeiten. Stelle sicher, dass reguläre Ausdrücke beim Suchen aktiviert sind (drittes Icon in der Suchzeile) und 1. suche nach ^ und ersetze mit " und 2. suche nach $ und ersetze mit \\n" Das Dach steht für den Zeilenbeginn und das $ für Zeilenende. Der Doppel-Backslash führt dazu, dass das \n nicht als Zeilenumbruch sondern als Buchstaben eingefügt wird. So kannst du mit einem Klick auf "Alle Ersetzen" die komplette Datei umformatieren. Das Ergebnis sollte in etwa so aussehen.

"-----BEGIN CERTIFICATE-----\n"
"AAAABBBBBCCCCCDDDDEEEEFFFFFFGGGGGHHHHJJJJJJKKKKK\n"
...
"-----END CERTIFICATE-----\n"
Abb. 2: VS Code: Suchen und Ersetzen Funktion mit eingeschalteten regulären Ausdrücken

Arduino Programmierung

Für die Programmierung des ESP brauchst du drei Dinge

  1. Netzwerkverbindung
  2. DHT Sensor auslesen
  3. MQTT-Interaktion

Hier ist der gesamte Code, darunter findest du eine Schritt-für-Schritt Erklärung.

#include <DHT.h>

#include <WiFiClientSecure.h>

#include <Countdown.h>
#include <IPStack.h>
#include <MQTTClient.h>


// DHT config
#define DHTPIN 5
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
unsigned long previous_millis = 0;
unsigned long interval = 2000;     // measurement every 2 seconds


// WIFI config
const char* ssid = "My Accesspoint"; // Enter your WiFi name
const char* password =  "iwtbotiwtwot"; // Enter WiFi password

const char* CLIENT_CERT = "-----BEGIN CERTIFICATE-----\n"
"MIIC2zCXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n"
"...\n"
"-----END CERTIFICATE-----";

const char* CLIENT_KEY = "-----BEGIN RSA PRIVATE KEY-----\n"
"MIIEowXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n"
"...\n"
"-----END RSA PRIVATE KEY-----\n";

const char* CA_CERT = "-----BEGIN CERTIFICATE-----\n"
"MIIDRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n"
"...\n"
"-----END CERTIFICATE-----\n";


// MQTT config
char* mqtt_host = "10.0.0.34";
uint16_t mqtt_port = 8883;
char* mqtt_user = "heizung";
char* mqtt_pw = "12345";
char* mqtt_topic_temp = "sensors/office1/temp";
char* mqtt_topic_humi = "sensors/office1/humi";
char* mqtt_clientID = "thermometer";


// Variables needed for network and mqtt
WiFiClientSecure wifi;
IPStack ipstack(wifi);
MQTT::Client<IPStack, Countdown, 250, 1> client = MQTT::Client<IPStack, Countdown, 250, 1>(ipstack);
MQTT::Message message;


// Initialize WiFi, MQTT and DHT
void setup(){
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  Serial.println(WiFi.localIP());
  wifi.setCACert(CA_CERT);
  wifi.setCertificate(CLIENT_CERT);
  wifi.setPrivateKey(CLIENT_KEY);
  connectMQTT();

  dht.begin();
} 


void connectMQTT()
{
  if(!client.isConnected()){
    char buffer[128];
    sprintf(buffer, "Connecting to: %s:%d", mqtt_host, mqtt_port);
    Serial.println(buffer);
    int rc = ipstack.connect(mqtt_host, mqtt_port);
    if (rc != 1)
    {
      Serial.print("rc from TCP connect is ");
      Serial.println(rc);
    }
    MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
    data.MQTTVersion = 3;
    data.clientID.cstring = mqtt_clientID;
    data.username.cstring = mqtt_user;
    data.password.cstring = mqtt_pw;
  
  
    sprintf(buffer, "Connecting as: %s", mqtt_clientID);
    Serial.println(buffer);
    rc = client.connect(data);
    if (rc != 0)
    {
      Serial.print("rc from MQTT connect is ");
      Serial.println(rc);
      return;
      
    }
    Serial.println("MQTT connected");
  }
}


void loop()
{
  // reconnect if necessary
  if (!client.isConnected())
    {
      connectMQTT();
    }
  // skip loop until interval is reached
  if(millis() - previous_millis < interval){
    return;
  }
  
  float temp = dht.readTemperature();
  float humi = dht.readHumidity();
  if(!isnan(temp)){
    char str[6];
    str[5] = '\0';
    snprintf(str, 5, "%3.1f", temp);
    message.retained = false;
    message.payload = str;
    message.payloadlen = strlen(str);
    client.publish(mqtt_topic_temp, message);
    snprintf(str, 5, "%3.1f", humi);
    message.retained = false;
    message.payload = str;
    message.payloadlen = strlen(str);
    client.publish(mqtt_topic_humi, message);
    //Serial.println("Temp: " + String(temp, 1));
    //Serial.println("Humi: " + String(humi, 1));
  }
  previous_millis = millis();
}

Zeile 1 - 7: Die Includes geben an, welche externen Programmbibliotheken geladen werden sollen. Dabei ist Zeile 1 für den Temperatursensor, 3 für die WLAN-Verbindung und der Rest für MQTT da.
Zeile 11 definiert den Data Pin für den Sensor. In diesem Fall habe ich Pin D5 des ESP32 benutzt
Zeile 12 definiert welcher Sensor verwendet wird (s. Hardwarevoraussetzungen). Mögliche Werte sind DHT11und DHT22
Zeile 13 legt die Variable dht an, mit der du nachher den Sensor steuern kannst.
Zeile 14/15: Um nicht zu oft pro Sekunde zu messen, kannst du in Zeile 15 ein Messintervall in Tausendstel Sekunde anzugeben. Die andere Variable Zeile 14 ist dafür da sich die letzte Messung zu merken (Auswertung kommt im loop)
Zeile 19/20: Hier gibst du die Zugangsdaten zu deinem WLAN an
Zeile 22/27/32: Hier kommen die im letzten Abschnitt vorbereiteten Texte mit Anführungszeichen und \n hin. 22) client_dht.crt, 27) client_dht.key, 32) ca.crt
Zeile 39-45: Hier gibst du die MQTT Zugangsdaten und topics für Temperatur (temp) und Luftfeuchte (humi) an. Achtung, diesmal ist der Port 8883 statt 1883, weil wir TLS-Zertifikate nutzen.
Zeile 49-52: Legt die Variablen wifi, client und message an, die wir nachher verwenden um WLAN und MQTT nutzen zu können. Die Zahl 250 in Zeile 51 gibt an, wie lang MQTT Nachrichten sein dürfen. Gibt es mal Fehler, die durch keinen anderen Grund erklärbar sind, kann es sein, dass Topic + Payload zusammen zu lang sind.
Zeile 56-61: In der setup Methode wird die serielle Schnittstelle gestartet (hauptsächlich, damit du mit dem seriellen Monitor der Arduino IDE sehen kannst, was der ESP zurückmeldet). Danach wird die WLAN-Verbindung aufgebaut und in 59-61 darauf gewartet, dass das erfolgreich ist.
Zeile 62: Die zugewiesene IP-Adresse wird auf dem seriellen Monitor ausgegeben.
Zeile 63-64: Hier werden die oben definierten Zertifikate in den Speicher geladen, damit MQTT diese nutzen kann.
Zeile 66 ruft die Verbindungsroutine für MQTT auf. Diese ist separat ausgelagert, damit der ESP, falls die Verbindung mal ausfällt ohne verdoppelten Code die MQTT Verbindung erneut aufbauen kann.
Zeile 68: Zu guter Letzt wird noch der Temperatursensor initialisiert. Damit ist das Setup abgeschlossen.

Zeilen 72-103 beinhalten den Verbindungscode für MQTT. Das meiste sind Ausgaben mit Statuswerten um Fehler schneller zu finden. Die wirklich relevanten Zeilen sind 78) für die Initialisierung der Netzwerkverbindung zum Broker, 84-88) für die Konfiguration der MQTT Verbindung und schließlich 93) zum Aufbau der Datenverbindung. Die Konfiguration benutzt die ganz oben definierten MQTT Zugangsdaten.

ab Zeile 106 im loopwird definiert was nach dem Setup immer wieder bis zum nächsten Neustart auf dem ESP läuft.
Zeilen 109 -112 bauen die MQTT Verbindung neu auf, falls sie abgebrochen ist (oder nicht aufgebaut werden konnte)
Zeilen 113 - 116 Überspringen den aktuellen loop-Durchgang, wenn noch nicht mindestens so viele Millisekunden vergangen sind, wie in interval definiert wurden. Anders gesagt, sorgt das dafür, dass der restliche Code alle x Millisekunden ausgeführt wird.
Zeile 118/119: Der Sensor wird ausgelesen und die Temperatur (temperature) sowie die Luftfeuchte (humidity) in Variablen gespeichert.
Zeile 120 ist wichtig um nicht fehlerhafte Daten auf die MQTT-Topics zu veröffentlichen. isnan(variable) überprüft ob eine variable den Wert NaN (not a number bzw. keine gültige Zahl) hat. Dieser Wert wird vom Sensor zurückgegeben, wenn er falsch/nicht verkabelt ist. Der Code der die MQTT-Nachrichten sendet soll aber natürlich nur ausgeführt werden, wenn die Rückgabewerte gültige Zahlen sind.
Zeile 121 legt eine Variable str an für eine Zeichenkette mit 6 Buchstaben
Zeile 122 setzt den 6. Buchstaben auf den Null-Terminator, der Programmen sagt, dass der Text dort endet. Achtung: Indizes beginnen in C++ bei 0, deshalb ist [5] ein Zugriff auf das 6. Element.
Zeilen 123-127 und 128-132 sind im Wesentlichen gleich, mit dem einzigen Unterschied, dass einmal die Temperatur und einmal die Luftfeuchtigkeit verwendet wird.
Zeile 123 konvertiert die Temperatur (eine Komma-Zahl) in eine Zeichenkette mit %3.1f (Lies: Drei Stellen vor dem Komma ohne führende Null, eine Stelle nach dem Komma). Das sind 5 Zeichen, die dann durch den snprintf Befehl an die ersten 5 Stellen der Variable str geschrieben werden. Warum? MQTT erwartet, dass die message, die du senden möchtest vom Typ char[] bzw. char* ist, deshalb musst du die Konvertierung selbst übernehmen. Es gibt dafür viele Wege, dieser ermöglicht dir gleichzeitig die Formatierung (Anzahl Stellen) zu konfigurieren.
Zeile 124 sagt dass die Nachricht an alle Subscriber ausgesendet werden soll, die gerade online sind, aber nicht per Nachsendeauftrag.
Zeile 125 setzt den Nachrichteninhalt (payload) auf den Wert, den du in str zusammengebaut hast, bei 9,5 Grad zum Beispiel 9.5.
Zeile 126 fügt der Nachricht nochmal die Länge hinzu (das sind üblicherweise Mechanismen, damit die Computer wissen, von wo bis wo sie im Speicher überhaupt lesen sollen, damit sie dann nicht die benachbarten Daten im Speicher mitlesen). Die Länge könntest du hier fix mit 6 angeben, weil das auch die Länge der Zeichenkette in str ist. Dynamischer für andere Anwendungen später ist es aber die hier verwendete strlen()-Funktion zu verwenden.
Zeile 127: Hier wird zu guter Letzt die publish-Funktion vom  MQTT-Client aufgerufen. Die Funktion erwartet zwei Parameter, das Topic (hast du oben in Zeile 43 definiert) und die message (aus den vorangegangenen Zeilen)
Wenn etwas nicht funktioniert und du nicht sicher bist warum, kannst du die //-Kommentar-Marker in Zeile 133/134 entfernen, dann gibt der ESP die Messdaten auch auf dem seriellen Monitor zurück.

Abschließend wird in Zeile 136 die aktuelle Zeit des ESPs (Laufzeit in Millisekunden) abgespeichert für den Vergleich mit dem Intervall in Zeilen 113-116.

Geschafft! Sehr gut. Dieses Beispiel ist bewusst klein gehalten und verzichtet auf hohe Flexibilität.

Wenn dich interessiert, wie du die ganzen Variablen (WLAN, MQTT-Konfiguration, Intervall) nicht direkt in den Code schreibst, sondern zur Laufzeit des ESPs über ein Webinterface konfigurieren kannst, schau mal in diesem Blogpost vorbei. Dort geht es zwar um RFID-Tokens und nicht um einen Temperatursensor, aber das Funktionskonzept ist das gleiche. Was dich dort erwartet sind ca. 150 Zeilen mehr Code, der die Variablen abspeichert, nach Neustart wieder lädt und im Webinterface registriert.

Wenn du den Codeblock von oben einfach kopiert hast musst du hauptsächlich in den Zeilen 19-45 Anpassungen machen.

Ähnlich wie beim letzten Mal musst du noch das richtige Board und den richtigen Port (z.B. COM3 unter Windows oder /dev/cu.usbserial0001 unter OS X) einstellen und dann auf Upload klicken. Damit ist das Programm nach kurzer Wartezeit auf dem ESP und startet automatisch. Im seriellen Monitor (Im Menü Tools) solltest du jetzt verfolgen können, wie der ESP bootet, mit dem WLAN verbindet, mit MQTT verbindet und dann nichts mehr tut, denn der Sensor ist noch nicht angeschlossen. Ab hier kannst du den ESP an einer beliebigen Spannungsversorgung (5V) betreiben und musst nicht mehr mit dem Computer verbunden sein.

ESP Verkabeln

Trenne bitte als erstes deinen ESP von allen Stromquellen, damit nichts kaputtgeht. Als nächstes verbindest du mit den beiliegenden Jumperwires die Pins vom DHT11/22 mit deinem ESP. Dafür verbindest du Ground (viereckige Lötstelle auf dem DHT-Sensor) mit einem beliebigen Ground Pin am ESP. Danach verbindest du VCC am DHT mit dem 5V Pin (manchmal auch VIN) am ESP. Als letztes verbindest du den Data Pin am DHT mit dem Pin den du in Codezeile 11 definiert hast (bei mir ist das 5). Das ist beispielhaft in Abbildung 3 gezeigt.

Gibt der DHT Sensor NaN zurück oder wird sehr warm 20 Sekunden nachdem er Strom hat, ist die Verkabelung vermutlich falsch.

ACHTUNG: Die meisten Modelle vom DHT11 auf Platine haben eine Überbrückung zwischen Pin 2 und 4 (von GND aus gezählt), sodass die nach außen geführten Pins in der Reihenfolge GND, 5V, DATA liegen. Manchmal (selten) gibt es aber diese Verbindung nicht und dann sind die herausgeführten Pins GND, DATA, 5V. Also lohnt ein Blick auf die Rückseite, ob die in Abbildung 3 eingezeichnete Verbindung da ist. Wenn ja, dann gilt die Verkabelung von oben.

Abb. 3: (links)Rückseite der DHT11-Platine mit eingezeichneter Überbrückung zwischen NULL-Pin (2) und VCC-Pin(4). (rechts) Schemazeichnung für den Anschluss der DHT11 Pins an ein ESP32-devkit Modul

Wenn bis hierhin alles geklappt hat, sollte der ESP jetzt auf deinem gewählten Topic (z.B. sensors/office1/temp) Daten senden und zwar im Abstand von so vielen Millisekunden wie du bei Intervall angegeben hast.

Hier bietet sich eine kurze Pause an, der schwierigste Teil ist geschafft, die Hardware tut was sie soll und meldet ihre Daten per MQTT. Doch was passiert mit den Daten? Im Moment verwirft der Broker die wieder. Um damit das Licht zu steuern werden wir einen zweiten Client bauen, der die Messdaten empfängt und dann auf dem LED-Streifen Topic Kommandos zur Veränderung der LEDs sendet. Genieße die Pause :)

Subscribe auf Temperatur in Python und Publish einer Farbe zum LED-Streifen

Auf MQTT Daten mit einfachen Mitteln reagieren zu können, macht dieses Protokoll so mächtig, deshalb möchte ich dir in diesem Abschnitt zeigen, wie du auf deinem Raspberry Pi ein Python-Skript laufen lassen kannst, dass auf die eben veröffentlichten Daten lauscht und mit Kommandos an den LED-Streifen damit reagiert. Warum Python? Ich halte Python für einfach und trotzdem sehr vielseitig einsetzbar. Du wirst hier nur auf MQTT-Nachrichten hören und welche versenden, es ist aber auch denkbar die Messdaten zur Archivierung in eine Datenbank zu schreiben um die Daten später zu analysieren (leider würde das den Rahmen an dieser Stelle sprengen).

Der folgende Code soll auf deinem raspberry pi laufen, du kannst ihn entweder direkt dort schreiben oder in Visual Studio Code (z.B.) und später per SSH hochladen. Speichere den folgenden Code als temp_to_light.py ab.

import paho.mqtt.client as mqtt

LED_TOPIC = "LEDstrip1/in"
LIGHT_PAYLOAD = "#{white:02X}{red:02X}{green:02X}{blue:02X}"
ANIMATION_PAYLOAD = "?{speed}"

TEMPHUM_TOPIC = "sensors/office1/"

temp_limits = (15, 35)
humi_limits = (20,100)


def remap(value, value_range, target_range):
    percentage = (value - value_range[0]) / (value_range[1]-value_range[0])
    return percentage * (target_range[1]-target_range[0]) + target_range[0]


def on_message_callback(client, userdata, message):
    topic = str(message._topic, encoding="UTF-8")
    value = float(message.payload)
    if topic == TEMPHUM_TOPIC + "temp":
        value = int(remap(value, temp_limits, (0,255)))
        payload = LIGHT_PAYLOAD.format(
            white=0,
            red=value,
            green=255 - value,
            blue=0
        )
        client.publish(LED_TOPIC, payload)
    if topic == TEMPHUM_TOPIC + "humi":
        value = int(remap(value, humi_limits, (0, 255)))
        payload = ANIMATION_PAYLOAD.format(speed=value)
        client.publish(LED_TOPIC, payload)


def on_connect_callback(client, userdata, flags_dict, result):
    client.subscribe(TEMPHUM_TOPIC + "temp")
    client.subscribe(TEMPHUM_TOPIC + "humi")


def connect(host: str, port: int, user: str = "", password: str = ""):
    if user:
        client.username_pw_set(user, password)
    client.on_connect = on_connect_callback
    client.on_message = on_message_callback
    client.connect(host, port)


if __name__ == "__main__":
    client = mqtt.Client("python_temp2light")
    connect("localhost",  # host
            1883,         # port
            "plant",      # user
            "programmer"  # password
    )
    client.loop_forever()

Zeile 1 importiert die paho-MQTT Bibliothek (das ist die gleiche wie auf dem ESP aber für Python). Die Installation kommt im nächsten Abschnitt.
Zeile 3 enthält das Topic, auf das der LED-Streifen hört. Das ist immer <name>/in und wenn du den LED-Streifen beim letzten Mal auch LEDstrip1 genannt hast so wie angegeben.
Zeile 4/5 enthalten Formatdefinitionen für die Befehle wie im letzten Post angegeben. #WWRRGGBB für die Hauptfarbe und %XXX für die Animationsgeschwindigkeit. Die Formatplatzhalter ermöglichen später an den entsprechenden Stellen Werte einzufügen und daraus eine Zeichenkette zu machen. {white:02X}bedeutet dabei zum Beispiel, dass hier ein Wert unter dem Namen "white" erwartet wird, der als Hexadezimalzahl ("X") dargestellt werden soll und zweistellig mit führender Null ist ("02"). Ich gebe bei Zeile 23 nochmal ein konkretes Beispiel.
Zeile 7 enthält den gemeinsamen Teil des Topics, damit wir nachher gut entscheiden können, ob die eingetroffene Nachricht überhaupt Messdaten (temp oder humi) enthält.
Zeilen 9 und 10 geben Bereiche an in denen du deine Messwerte erwartest (hier: zwischen 15°C und 35°C Temperatur und zwischen 20% und 100% Luftfeuchtigkeit), weil der LED-Streifen Werte zwischen 0 und 255 benötigt und so eine Umrechnung möglich wird.
Zeilen 13-15 definieren eine Umrechnungsfunktion remap die genau diese Umrechung von 15-35 nach 0-255 macht. Die Mathematik dahinter ist eine Art erweiterter Dreisatz. Die Funktion nimmt einen Wert und einen Quellbereich sowie einen Zielbereich als Übergabeparameter entgegen und gibt den Wert in den Zielbereich transformiert zurück.
Zeilen 18-33 Definieren, was passieren soll, wenn eine Nachricht eintrifft.
Als erstes in Zeile 19 konvertiert der Code die Daten die (als bytes) hereinkommen in einen richtigen String (Text). Danach wird in Zeile 20 der Payload extrahiert und als Fließkommazahl interpretiert, da wir ja nur Kommawerte erwarten.
Zeilen 21-29 und 30-33 sind im Grunde wieder gleich und unterscheiden nur, ob es Temperatur oder Luftfeuchtigkeit ist, die behandelt wird.
Die beiden Codeblöcke bestehen jeweils aus einer Zeile Umrechnung des Messwertes in den neuen Bereich (Hier wird einmal temp_limits und einmal humi_limits verwendet, der Zielbereich ist aber gleich). NB: Nach der Umrechnung wird der Wert noch mit int() in eine Ganzzahl konvertiert (Nachkommastellen werden abgeschnitten), weil nur Ganzzahlen zwischen 0-255 in zweistellige Hexadezimalzahlen formatiert werden können.
Die zweite Zeile besteht daraus, die vorher definierten Formatierungsplatzhalter mit Werten zu befüllen. Prinzipiell geht das mit "eine {platzhalter}".format(platzhalter=5) -> eine 5. Im vorliegenden Fall wenn der Messwert zum Beispiel 15 ist, kommt als value 0 zurück und der Formatstring wird mit den Werten (white=0, red=0, green=255, blue=0)aufgerufen, sodass der finale Payload #0000FF00 ist. Beim unteren Limit sind die LEDs also grün, beim oberen Limit rot und dazwischen eine Mischform (hellgrün -> gelb -> orange). Tobe dich hier gerne selber aus, am Ende soll es dir gefallen :)
In der letzten Zeile wird dann ganz analog zu Arduino die publish()-Funktion vom MQTT-Client aufgerufen. Diese benötigt ein Topic und einen Payload.

Zeilen 36-38 definieren was passieren soll, wenn die Verbindung zum MQTT-Broker erfolgreich war. Die Übergabeparameter sind von der Bibliothek vorgeschrieben und ich habe sie aus der Anleitung kopiert, wir werden nur client benutzen.
Zeile 37 sagt dem Client, der wenn dieser Code ausgeführt wird schon erfolgreich mit MQTT verbunden ist, dass er auf das Topic sensors/office1/temp subscriben soll. Dasselbe passiert in Zeile 38 für .../humi

Das war die ganze Programm-Logik, was passieren soll, wenn MQTT verbunden ist. Jetzt musst du noch definieren wie und wohin verbunden werden soll.

Die Methode in Zeilen 41-46 akzeptiert Übergabevariablen, die sagen wohin verbunden werden soll (host und port) und eventuell benötigte Zugangsdaten (user und password) enthalten.
In Zeile 42/43 steht, dass Benutzer und Passwort nur verwendet werden, wenn auch ein User angegeben wurde. Warum wird hier nur auf User und nicht auf leeres Passwort überprüft? Weil ein leeres Passwort grundsätzlich in Ordnung ist. Wohingegen ein Passwort ohne User keinen Sinn ergibt.
In Zeilen 44/45 werden die vorhin programmierten Funktionen an den Client gebunden, sodass der Client weiß: "wann immer eine Nachricht ankommt führe den Code von on_message_callback aus!" und: "wann immer du erfolgreich die MQTT-Verbindung erstellt hast (normalerweise nur einmal), führe den Code von on_connect_callback aus!"
Zu guter Letzt wird mit client.connect() und Übergabe der Verbindungsparameter die Verbindung aufgebaut.

Jetzt ist die gesamte Programmlogik fertig und muss nur noch gestartet werden.
Zeile 49 und folgende sagen was passieren soll, wenn das Python-Skript aufgerufen wird.
Zeile 50 legt eine Variable client an, die den Client enthält. Dieser wird mit Übergabe der MQTT-Client-ID erstellt wird.
Zeile 51 - 54 ruft die oben programmierte Funktion connect auf und übergibt fest im Code geschriebene Verbindungs- und Zugangsdaten. Da der Code auf dem Pi laufen soll, ist als Host "localhost" angegeben. Als Port der Standardport 1883, da es nicht notwendig ist eine interne Verbindung mittels TLS zu verschlüsseln. Zuletzt gebe ich einen Benutzer an, der sowohl Lesezugriff auf das sensors/office1/#-Topic als auch Schreibzugriff auf das LEDstrip1/in-Topic hat.
Jetzt ist alles vorbereitet und der Python-Code wird mit client.loop_forever() in eine Endlosschleife geschickt, die sich darum kümmert, dass regelmäßig geschaut wird ob neue Nachrichten ankommen. Das heißt auch, wenn wir das Programm gleich starten läuft das durch bis zum nächsten System-Neustart oder bis du es manuell abbrichst.

Wenn du das alles hast, öffne ein Terminal (OS X) oder eine Powershell (Windows) und wechsele mit dem Kommando cd in den Ordner in dem dein Python-Skript liegt.

# Gib hier die IP-Adresse deines Pis an
IP_ADDRESS=10.0.0.34

# Kopiere die Datei auf den Pi
scp temp_to_light.py pi@${IP_ADDRESS}:/home/pi/

# Logge dich auf dem Pi ein
ssh pi@${IP_ADDRESS}

# Installiere das paho-mqtt Paket systemweit
sudo python3 -mpip install paho-mqtt

# Entweder: Starte das Pythonskript (Terminal muss offen bleiben)
# Abbrechen mit Strg + C
python3 temp_to_light.py

# Oder: Starte das Skript im Hintergrund (Ausloggen möglich
# Skript läuft weiter)
python3 temp_to_light.py &

# Bonus um das Skript, das im Hintergrund läuft, abzubrechen
pkill -e -f "python3 temp_to_light.py"

Das war es auch schon wenn jetzt alles läuft

  • MQTT-Broker (Raspberry Pi) läuft
  • ESP32 mit DHT11/22 Sensor hat Strom
  • LED-Streifen hat Strom
  • Python-Skript temp_to_light.py läuft auf dem Pi

dann reagiert dein Licht mit seiner Main-Color auf die Temperatur und mit der Animationsgeschwindigkeit auf die Luftfeuchtigkeit.

Zur Erinnerung: mit dem Payload /<AnimationsID zwischen 1 und 65> kannst du die Animation einstellen. Da aktuell nur die Main-Color gesetzt wird, bietet sich die Animation 5 (Color Wipe), die du auch im Video siehst, an.

Herzlichen Glückwunsch, wenn du die Serie bis hierhin verfolgt hast, hast du eine Menge über MQTT gelernt und bestenfalls keine Berührungsängste (mehr) mit Arduino. Im letzten Teil der Reihe möchte ich dir noch zeigen, wie du per RFID-Tag die Animation (und gegebenenfalls auch die Hauptfarben) steuern kannst. Damit hast du dann einen möglichst breiten Überblick was mit Elektronik (allen voran ESPs) und MQTT alles angestellt werden kann.

Wenn du darüberhinaus noch Fragen oder Wünsche hast, kannst du mich immer unter info@ per Email oder auf Twitter erreichen (meine DMs sind offen, du kannst mir also auch ohne öffentlichen Tweet schreiben).

Ich freue mich auf den Abschluss dieser Blogreihe und auf dein Feedback, was du damit gebastelt hast. Bis zum nächsten Mal und bleib neugierig.

Zu Teil 5 geht es hier entlang: Karte, Bitte! RFID zur Steuerung - MQTT Part V