Es sind dann doch einige Kabel geworden, und nach ein paar Versuchsrunden wurden diese auch funktionsfähig an die verschiedenen Ports des ESP32 angeschlossen.
Was soll die Platine nun in Summe tun? Primär mal als Frontend arbeiten, ohne viel eigene Steuerungslogik. Das soll die iobroker Instanz im Hintergrund per Skript übernehmen und die entsprechenden Kommandos per MQTT übermitteln. Der ESP32 wertet diese aus und steuert die entsprechenden Anzeigen, sofern er eine Bewegung über den PIR Melder entdeckt hat. Letzteres macht er allerdings dann doch autark per geflashtem Code. Hintergrund ist schlicht und ergreifend der Ansatz, das Backlight des Displays zu schonen und natürlich auch Strom zu sparen. Das TFT Display bekommt ebenfalls per MQTT über 6 Zeilen verteilt den anzuzeigenden Text, jeweils als Überschrift/Status-Pärchen, übermittelt. So kann z.B. unter dem Titel "Fenster" die Anzahl derselbigen noch offen stehenden angezeigt werden. Auch kann per Trigger einer der auf der microSD-Karte abgelegten MP3-Dateien abgespielt werden. Damit ist ein akustischer Hinweis, dass noch Fenster offen stehen, nun auch im Flur möglich. Und schlussendlich steht ein NFC Reader zur Verfügung, über den man sich an und abmelden kann.
Wie man die Arduino IDE installiert, die notwendigen Bibliotheken einbindet und den Code auf den ESP32 flasht ist an vielen Stellen im Internet in gut nachvollziehbaren Tutorials beschrieben. Persönlich habe ich mich per Buch eingelesen und fand das Makerbuch zum Arduino ganz hilfreich, wenn es auch nicht im Kern auf den ESP abzielt, sondern wie der Titel schon verrät auf den Arduino.
Der Aufbau der Schaltung ist in diversen Etappen zu schaffen:
- Installation und Inbetriebnahme Arduino IDE
- Aufbau der Schaltung rund um den ESP32 mit allen Peripherieelementen
- Flashen des Codes auf den ESP32
- ggfs. Einbau in ein Gehäuse (ich habe vor hier einen Bilderrahmen zu verwenden)
- Einbinden und Anpassen der Scripte in iobroker
- Test und finale Inbetriebnahme
ESP32 Code
/* * Dashboard * * Displaying information provided by MQTT from an iobroker/Homematic instance * Dashboard acts as a client, whilst business logic is located in iobroker * This is due to more flexibility as well as security considerations * * Copyright (c) 2023 Martin Arend * This software is published under GNU General Public License version 3 or later * www.gnu.org/licenses/gpl-3.0.txt * */ // Open Tasks: // ToDo: clean all input via MQTT (Hex and Strings) // including libraries #include <Adafruit_NeoPixel.h> // NeoPixel library #include <WiFi.h> // WiFi library #include <PubSubClient.h> // MQTT library #include <Adafruit_GFX.h> // Core graphics library #include <Adafruit_ST7735.h> // Hardware-specific library #include <SPI.h> // SPI library #include <Fonts/FreeSans9pt7b.h> // include font (weight: normal) for TFT #include <Fonts/FreeSansBold9pt7b.h> // include font (weight: bold) for TFT #include <SoftwareSerial.h> // include Serial library for MP3 Player #include <DFRobotDFPlayerMini.h> // include MP3player lib #include <MFRC522.h> // include RF-ID lib // define Clients WiFiClient espClient; // WiFi Client PubSubClient client(espClient); // MQTT Client // define arrays to store MQTT payloads for LEDs, TFT, RF-UID and String Programmstate String payloadLED[8]; String Headline[3]; String TftText[3]; String PSTATE; char message_buff[100]; // define setup for LED bar (Neopixel) #define PIN 12 #define NUMPIXELS 8 #define BRIGHTNESS 20 Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800); // define setup for PIR #define PIRpin 14 // define setup for WiFi connection const char* ssid = "SSID DEINES WLANS"; const char* password = "DEIN WLAN PASSWORT"; // define MQTT Broker Credentials const char* MQTT_BROKER = "IP.DEINES.MQTT.BROKERS"; // Broker IP Adresse const char* MQTT_USER = "USER"; // Broker User const char* MQTT_PASSWORD = "MQTT USER PASSWORT"; // Broker Passwort // define setup for TFT display #define TFT_CS 5 #define TFT_RST 4 #define TFT_DC 2 #define TFT_MOSI 26 // Data out - 23 #define TFT_SCLK 25 // Clock out - 18 #define TFT_BACKLIGHT 27 // TFT Backlight Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST); // define color set for TFT display const uint16_t Black = 0x0000; const uint16_t Blue = 0x001F; const uint16_t Red = 0xF800; const uint16_t Green = 0x07E0; const uint16_t White = 0xFFFF; uint16_t Text_Color = White; uint16_t Background_Color = Black; // define display timings long now = millis(); long timeDisplayOff = now; const int HYSTERESIS = 100000; // time in ms after display switches off again // define MP3player int volume = 15; // volume 0 - 30 #define MP3_TX 22 #define MP3_RX 21 SoftwareSerial MP3Player(MP3_RX, MP3_TX); // RX, TX DFRobotDFPlayerMini dfPlayer; // define RF-ID #define SS_PIN 16 #define RST_PIN 17 MFRC522 rfid(SS_PIN, RST_PIN); // *************************************************************************** // Interrupt routine for PIR void IRAM_ATTR detectsMovement() { Serial.print(millis()); Serial.println("ms - Motion detected"); timeDisplayOff = millis() + HYSTERESIS; Serial.println("Switch off at " + String(timeDisplayOff) + "ms"); } // *************************************************************************** // Setup routine // *************************************************************************** void setup() { // setup Serial Serial.begin(115200); SPI.begin(); //init SPI bus Serial.println("Booting..."); // setup Statemachine PSTATE = "BOOT"; // setup NeoPixel pixels.begin(); // This initializes the NeoPixel library. pixels.setBrightness(BRIGHTNESS); // sets brightness for the NeoPixel pixels.show(); // initialize pixel to all off // setup TFT Display pinMode(TFT_BACKLIGHT, OUTPUT); // define Pin as output to control TFT backlight digitalWrite(TFT_BACKLIGHT, HIGH); // switch TFT backlight on tft.initR(INITR_BLACKTAB); // init TFT driver tft.setFont(&FreeSans9pt7b); // load font for display tft.fillScreen(Background_Color); delay(500); tft.setCursor(25, 42); tft.setTextSize(1); tft.setTextColor(Blue); tft.println("miniSOFT"); tft.setTextColor(Text_Color); tft.setCursor(15, 62); tft.println("SMARTdash"); tft.setFont(); tft.setCursor(30, 100); tft.print("Booting "); // wait for PIR, play animation Serial.println("Wait for Motion Sensor."); // animation for (int i = 0; i < 8; i++) { for (int j = 0; j < 25; j++) { pixels.setPixelColor(i, pixels.Color(j*10,j*10,j*10)); pixels.show(); delay(14); } } // end animation Serial.println("Motion Sensor setup finished."); tft.fillRect(3, 90, 120, 140, Background_Color); tft.setCursor(30, 100); tft.print("PIR ready "); // setup PIR pinMode(PIRpin, INPUT_PULLUP); // Set motionSensor pin as interrupt, assign interrupt function and set RISING mode attachInterrupt(digitalPinToInterrupt(PIRpin), detectsMovement, RISING); // setup MP3 Player Serial.println("Setup MP3 Player."); tft.fillRect(3, 90, 120, 140, Background_Color); tft.setCursor(30, 100); tft.print("Init MP3 "); MP3Player.begin(9600); if (!dfPlayer.begin(MP3Player)) { //use softwareSerial to communicate with mp3 Serial.println(F("Unable to begin:")); Serial.println(F("1. Please recheck the connection!")); Serial.println(F("2. Please insert the SD card!")); } else { Serial.println(F("MP3 Player online.")); } dfPlayer.volume(volume); //sets volume for MP3 Player dfPlayer.play(1); //play the first mp3 as startup sound // setup RF-ID Serial.println("Setup RF-ID."); tft.fillRect(3, 90, 120, 140, Background_Color); tft.setCursor(30, 100); tft.print("Init RF-ID "); rfid.PCD_Init(); //init MFRC522 // switch all LEDs blue to indicate searching for WiFi for (int i = 0; i < 8; i++) { pixels.setPixelColor(i, pixels.Color(0,0,255)); pixels.show(); } // setup WiFi and MQTT initWiFi(); client.setServer(MQTT_BROKER, 1883); // initialisation all done - switch all LEDs off for (int i = 0; i < 8; i++) { pixels.setPixelColor(i, pixels.Color(0,0,0)); pixels.show(); } tft.fillRect(3, 90, 120, 140, Background_Color); tft.setCursor(30, 100); tft.print("WiFi ready"); // setup MQTT callback client.setCallback(callback); tft.fillRect(3, 90, 120, 140, Background_Color); tft.setCursor(30, 100); tft.print("Ready"); delay(1000); // clear TFT and load font tft.fillScreen(Background_Color); tft.setFont(&FreeSans9pt7b); // setup Statemachine to listen to MQTT messages PSTATE = "MQTT_READ"; digitalWrite(TFT_BACKLIGHT, LOW); // switch TFT backlight off } // *************************************************************************** // Main routine // *************************************************************************** void loop() { // MQTT if (!client.connected()) { while (!client.connected()) { client.connect("ESPDashboard", MQTT_USER, MQTT_PASSWORD); client.subscribe("dashboard/LED1"); client.subscribe("dashboard/LED2"); client.subscribe("dashboard/LED3"); client.subscribe("dashboard/LED4"); client.subscribe("dashboard/LED5"); client.subscribe("dashboard/LED6"); client.subscribe("dashboard/LED7"); client.subscribe("dashboard/LED8"); client.subscribe("dashboard/Headline1"); client.subscribe("dashboard/Text1"); client.subscribe("dashboard/Headline2"); client.subscribe("dashboard/Text2"); client.subscribe("dashboard/Headline3"); client.subscribe("dashboard/Text3"); client.subscribe("dashboard/MP3trigger"); client.subscribe("dashboard/RFID"); delay(1000); } } client.loop(); if (PSTATE = "MQTT_READ") { // activate display only on detected motion if(millis() < timeDisplayOff) { // display + LED on for (int i = 0; i < 8; i++) { hexrgb(payloadLED[i], i); } digitalWrite(TFT_BACKLIGHT, HIGH); // switch TFT backlight on dfPlayer.volume(volume); } else { // no motion, no trigger and/or hysteresis expired for (int i = 0; i < 8; i++) { pixels.setPixelColor(i, pixels.Color(0,0,0)); pixels.show(); } digitalWrite(TFT_BACKLIGHT, LOW); // switch TFT backlight off //dfPlayer.volume(0); // switches off the speaker if display is off } } if (rfid.PICC_IsNewCardPresent()) { // new tag is available if (rfid.PICC_ReadCardSerial()) { // NUID has been read // publish that a new card is detected via MQTT client.publish("dashboard/RFuid", "new card"); // read out new RF-UID MFRC522::PICC_Type piccType = rfid.PICC_GetType(rfid.uid.sak); Serial.print("RFID/NFC Tag Type: "); Serial.println(rfid.PICC_GetTypeName(piccType)); // read UID in the hex format Serial.print("UID:"); String rfidUid = ""; for (byte i = 0; i < rfid.uid.size; i++) { rfidUid += String(rfid.uid.uidByte[i] < 0x10 ? "0" : ""); rfidUid += String(rfid.uid.uidByte[i], HEX); } Serial.println(rfidUid); // publish UID via MQTT rfidUid.toCharArray(message_buff, rfidUid.length() + 1); client.publish("dashboard/RFuid", message_buff); rfid.PICC_HaltA(); // halt PICC rfid.PCD_StopCrypto1(); // stop encryption on PCD } } } // *************************************************************************** void hexrgb(String msg, int LEDnum) { // conversion HEX to RGB long rgb = strtol(&msg[1], NULL, 16); byte r = rgb>>16; byte g = rgb>>8; byte b = rgb; pixels.setPixelColor(LEDnum, pixels.Color(r,g,b)); pixels.show(); } void callback(char* topic, byte* payload, unsigned int length) { // decode MQTT payload String msg = ""; for (byte i = 0; i < length; i++) { char tmp = char(payload[i]); msg += tmp; } // store MQTTpayload per LED if(strcmp(topic, "dashboard/LED1") == 0) { payloadLED[0] = msg; Serial.println("LED1: " + payloadLED[0]); } else if(strcmp(topic, "dashboard/LED2") == 0) { payloadLED[1] = msg; Serial.println("LED2: " + payloadLED[1]); } else if(strcmp(topic, "dashboard/LED3") == 0) { payloadLED[2] = msg; Serial.println("LED3: " + payloadLED[2]); } else if(strcmp(topic, "dashboard/LED4") == 0) { payloadLED[3] = msg; Serial.println("LED4: " + payloadLED[3]); } else if(strcmp(topic, "dashboard/LED5") == 0) { payloadLED[4] = msg; Serial.println("LED5: " + payloadLED[4]); } else if(strcmp(topic, "dashboard/LED6") == 0) { payloadLED[5] = msg; Serial.println("LED6: " + payloadLED[5]); } else if(strcmp(topic, "dashboard/LED7") == 0) { payloadLED[6] = msg; Serial.println("LED7: " + payloadLED[6]); } else if(strcmp(topic, "dashboard/LED8") == 0) { payloadLED[7] = msg; Serial.println("LED8: " + payloadLED[7]); } else if(strcmp(topic, "dashboard/Headline1") == 0) { if (msg != Headline[0]) { tft.setFont(&FreeSansBold9pt7b); tft.setTextColor(Background_Color); tft.setCursor(10, 25); tft.print(Headline[0]); tft.setCursor(10, 25); tft.setTextColor(Blue); tft.print(msg); Headline[0] = msg; Serial.println("Headline1: " + Headline[0]); } } else if(strcmp(topic, "dashboard/Text1") == 0) { if (msg != TftText[0]) { tft.setFont(&FreeSans9pt7b); tft.setTextColor(Background_Color); tft.setCursor(10, 45); tft.print(TftText[0]); tft.setCursor(10, 45); tft.setTextColor(Text_Color); tft.print(msg); TftText[0] = msg; Serial.println("Text1: " + TftText[0]); } } else if(strcmp(topic, "dashboard/Headline2") == 0) { if (msg != Headline[1]) { tft.setFont(&FreeSansBold9pt7b); tft.setTextColor(Background_Color); tft.setCursor(10, 75); tft.print(Headline[1]); tft.setCursor(10, 75); tft.setTextColor(Blue); tft.print(msg); Headline[1] = msg; Serial.println("Headline2: " + Headline[1]); } } else if(strcmp(topic, "dashboard/Text2") == 0) { if (msg != TftText[1]) { tft.setFont(&FreeSans9pt7b); tft.setTextColor(Background_Color); tft.setCursor(10, 95); tft.print(TftText[1]); tft.setCursor(10, 95); tft.setTextColor(Text_Color); tft.print(msg); TftText[1] = msg; Serial.println("Text2: " + TftText[1]); } } else if(strcmp(topic, "dashboard/Headline3") == 0) { if (msg != Headline[2]) { tft.setFont(&FreeSansBold9pt7b); tft.setTextColor(Background_Color); tft.setCursor(10, 125); tft.print(Headline[2]); tft.setCursor(10, 125); tft.setTextColor(Blue); tft.print(msg); Headline[2] = msg; Serial.println("Headline3: " + Headline[2]); } } else if(strcmp(topic, "dashboard/Text3") == 0) { if (msg != TftText[2]) { tft.setFont(&FreeSans9pt7b); tft.setTextColor(Background_Color); tft.setCursor(10, 145); tft.print(TftText[2]); tft.setCursor(10, 145); tft.setTextColor(Text_Color); tft.print(msg); TftText[2] = msg; Serial.println("Text3: " + TftText[2]); } } else if(strcmp(topic, "dashboard/MP3trigger") == 0) { if (msg != "off") { Serial.println("MP3: " + msg); dfPlayer.volume(volume); //sets volume for MP3 Player dfPlayer.play(msg.toInt()); } } } void initWiFi() { // init WIFI connection with provided credentials WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); }
Wie immer sei an dieser Stelle der Hinweis erlaubt, dass ich Autodidakt bin. Der Code hat noch einiges Optimierungspotential und kann sicher an der ein oder anderen Stelle eleganter als jetzt ausfallen. Für den Moment tut er aber was er soll.
Die Statemachine habe ich für eine zukünftige Erweiterung angelegt. Sie wird aber noch nicht wirklich genutzt und kann daher ignoriert werden.
Wichtig ist, die eigenen Credentials für das WLAN und den MQTT Broker, der bei mir Tei der iobroker Instanz ist, anzugegeben. Sonst wird es nicht funktionieren.
Nach dem Start initialisiert der ESP32 alle Elemente der Peripherie und zeigt während dem Bootvorgang auf dem Display auch die einzelnen Schritte die er durchläuft an. Der Neopixel-Stick wird ebenfalls in die Anzeige der Startup-Sequenz eingebunden und animiert z.B. die Wartezeiten bei der Initialisierung und schaltet die LEDs auf blau, während auf die Netzwerkverbindung gewartet wird. Auch erfolgt eine erste Tonausgabe, ein Startup-Sound, indem die erste auf der microSD Karte liegende MP3 Datei abgespielt wird.
Ist alles initialisiert, hört der ESP32 per MQTT auf eingehende Nachrichten und wertet diese aus:
- Unter dashboard/LED1 bis /LED8 werden Hexwerte für die Farbsteuerung der LEDs erwartet. #000000 schaltet die LED aus, #FFFFFF auf weiß und z.B. #FF0000 auf rot.
- dashboard/Headline1 bis /Headline3 und die zugehörigen dashboard/Text1 bis /Text3 nehmen Texte im Stringformat zur Anzeige auf.
- dashboard/MP3trigger erwartet eine Integer Zahl um die dazugehörige auf der microSD Karte liegende MP3 Datei abzuspielen
- und schließlich sendet der ESP32 an den MQTT-Broker die UID der aufgelegten NFC Karte unter dashboard/RFID.
Nochmal der Hinweis: der Code beinhaltet zum jetzigen Zeitpunkt keinerlei Fehlerkorrekturen. Auch werden eingehende MQTT Nachrichten nicht auf syntaktische Korrektheit oder Zulässigkeit geprüft. Das wird zu einem späteren Zeitpunkt noch erfolgen.
Ansonsten habe ich versucht, den Code an den entscheidenden Stellen zu kommentieren.
Aufbau des ESP32
ESP32 | TFT Display | |||
G27 | GPIO27 | 27 | 8 | LEDA |
5V | V5 | 2 | VCC | |
GND | GND | 1 | GND | |
SP | VSPI_CLK | 25 | 3 | SCK |
SN | VSPI_MOSI | 26 | 4 | SDA |
G4 | GPIO4 | 4 | 5 | RES |
G2 | GPIO2 | 2 | 6 | RS |
G5 | VSPI_CS | 5 | 7 | CS |
ESP32 | MP3 Modul | |||
5V | V5 | 1 | VCC | |
- | - | 6 | SPK1 Speaker | |
- | - | 8 | SPK2 Speaker | |
GND | GND | 7 | GND | |
G22 | GPIO22 | 22 | 3 | TX |
G21 | GPIO21 | 21 | 2 | RX |
ESP32 | RF-ID | |||
G16 | GPIO16 | 16 | 1 | SDA |
G18 | GPIO18 | 18 | 2 | SCK |
G23 | GPIO23 | 23 | 3 | MOSI |
G19 | GPIO19 | 19 | 4 | MISO |
- | - | 5 | IRQ | |
GND | GND | 6 | GND | |
G17 | GPIO17 | 17 | 7 | RST |
3V3 | 3V3 | 8 | 3.3V |
Der Zusammenbau erfolgte bei mir mit einer Streifenlochrasterplatine. Wichtig ist zu beachten, dass ihr eine ausreichend dimensionierte Stromquelle verwendet. Ich versorge den ESP32 über ein handelsübliches USB Ladegerät mit entsprechendem Stecker. Aber bei den "nur" 500mA die eine Computer-USB Buchse liefert hing sich bei mir regelmäßig der MP3 Player auf, wenn gleichzeitig das Backlight des TFT Displays und die LEDs eingeschaltet waren. Erst eine Versorgung mit > 1A über die USB Schnittstelle führten zu einem stabilen Betrieb!
Denkt auch daran, den PIR Melder Euren Wünschen entsprechend einzustellen. An den beiden Potis lassen sich Ansprechzeit und -reichweite per Schraubendreher nach eigenem Gusto manuell einstellen.
iobroker
Auch für den guten iobroker gibt es deutlich bessere Tutorials im Netz als ich sie schreiben könnte. Inklusive der Installation eines MQTT Brokers. Zu Debug und Entwicklungszwecken habe ich parallel noch die MQTTBox im Einsatz. Mit diesem Tool kann ich im SmartHome den entsprechenden Datenverkehr mithören und schnell herausfinden ob und ggfs auch was klemmt.
Was nun noch fehlt, sind die passenden Blockly Scripte zur Steuerung des Dashboards.
Hier ein Beispielscript, dass die offenen Fenster im Erdgeschoss zählt.
Es bedient sich der Aufzählung Fenster, sprich alle Sensoren sind entsprechend in diese Kategorie einsortiert. Falls sich nun ein Zustand einer oder mehrerer der in dieser Liste aufgenommenen Sensoren ändert, triggert das Script. Es wird der Status Sensor für Sensor über die Schleife gezählt und in der Variablen gespeichert. Danach wird per MQTT das entsprechende Kommando (LED rot oder LED grün) an das Dashboard geschickt.
Ich habe zusätzlich noch das "steuere"-Kommando mit eingebaut und aktualisiere damit in der iobroker internen Zustandsverwaltung die jeweilige Farbe. Das hat den Hintergrund, dass bei einem Reset oder gewollten Neustart des Dashboards der zuletzt gültige Wert gleich wieder subscribed und damit sofort angezeigt werden kann.
Wie man sieht, ist die LED3 in diesem Beispielscript dem Fensterstatus im Erdgeschoss zugeordnet. Analog kann man nun weitere Status per Blockly Script aufsetzen und die weiteren LEDs ansteuern. Profis können das sicher alles in einem Script zusammenfassen. Ich habe pro LED ein eigenes angelegt. Ideen habt ihr sicher genug.
Wünsche viel Erfolg beim Nachbauen und der Gestaltung Eures persönlichen Dashboards.