Das sprechende Multimeter - Teil 5 - AZ-Delivery

Unser TDMM hat in den letzten vier Blogbeiträgen (Teil 1, Teil 2, Teil 3, Teil 4) eine ganze Menge dazugelernt. Heute wollen wir das TDMM ins Netz holen. Wir haben zwar ein schönes Display im Gerät und können uns Messergebnisse auch ansagen lassen. Oft möchte man aber nicht nur eine Messung einmalig durchführen, sondern auch Messungen speichern, die später ausgewertet werden, oder die Werte auf dem Mobiltelefon anschauen.

Das TDMM im Netz

Es gibt einige Möglichkeiten, wie man Daten des TDMM auf einem Web-Client darstellen kann, auf einem Laptop, einem Mobiltelefon, oder einem anderen Endgerät. Betrachten wir hier zwei Varianten:

Darstellung der Daten auf einer Website

Unser D1 Mini kann gut als Webserver genutzt werden. Er stellt die Messwerte auf einer Website dar, auf die sich der Benutzer z.B. mit seinem Mobiltelefon, Tablet oder PC einloggt.

Diese Lösung ist recht einfach mit diversen Libraries zu realisieren. Die Anzeige kann man grafisch gestalten,  z.B. indem man Messinstrumente darstellt, oder den Messwerteverlauf als Grafik.

Ein Nachteil dieser Version: Die Daten werden auf der Website dargestellt, aber nicht für die spätere Weiterverwendung gespeichert. Um eine Website zu erstellen, die auf den verschiedenen Endgeräten wirklich gut ausschaut, ist einiger Aufwand nötig.

Je nach Aufgabenstellung, ist diese Möglichkeit mehr oder weniger sinnvoll. Es geht auch anders:

MQTT - etwas mehr Aufwand - einige Vorteile

Im professionellen Umfeld wird man zuerst die Daten übertragen und sich danach um deren Darstellung kümmern. Eine solche Möglichkeit bietet das interessante Protokoll MQTT, das viele von ihnen sicherlich schon von anderen Anwendungen kennen - es wird häufig benutzt.

Das Grundkonzept sieht so aus:

MQTT ist rund um einen „Broker“ aufgebaut, der Nachrichten empfängt
und verteilt. Bekannte Systeme sind MOSQUITTO oder ECLIPSE.

Nachrichten werden für ein „Topic“ publiziert, das hierarchisch aufgebaut ist.
Ein Topic bei uns ist tdmm/spannung. Wenn das TDMM dafür
einen Spannungswert liefert, gibt der MQTT-Broker diesen Wert an alle Clients weiter, die das Topic abonniert haben. Zum Publizieren wird die „publish“-Anweisung verwendet. Für das Abo eines Topic verwendet der Client die Funktion „subscribe“.

Das TDMM schickt die Daten an den Broker, solange es dort eingeloggt ist. Es überprüft nicht, ob der Wert auch dort angekommen ist und ob er von den Clients übernommen wurde. Man kann bei MQTT die QoS (Quality of service) jedoch individuell definieren, je nachdem wie zuverlässig die Datenübertragung sein soll. Wir verwenden die einfachste Version QoS = 0 („send and forget“).

Es gibt ausgezeichnete Beschreibungen zu MQTT mit allen Details dieses Protokolls. Die Website des Projektes gibt erschöpfend Auskunft:
mqtt.org Für den Anfang ist das möglicherweise etwas viel. Von den Websites in deutscher Sprache möchte ich blog.doubleslash.de nennen, die mir empfehlenswert erscheint. Auch Wikipedia liefert brauchbare Informationen.

MQTT auf dem TDMM

MQTT erlaubt es Ihnen, die Daten vom TDMM auf ein webfähiges Endgerät zu übertragen und auf die vielfältigste Weise darzustellen. Statt mit JAVA und anderen Tools eine Darstellung für ihr Endgerät zu programmieren, stellen Sie bei diesem Konzept mit einem MQTT-Tool die Ergebnisse dar. Es verbindet sich auch mit dem MQTT-Broker und sendet ein „subscribe“ auf ein „Topic“ oder eine Topic-Gruppe. Drei Beispiele stelle ich ihnen hier vor:   Darstellung von TDMM-Daten auf einem Mobiltelefon   Darstellung in Home Assistant, der damit zu einem „Lab Assistant“ wird   Übernahme der TDMM-Daten durch ein Python-Skript Bevor wir MQTT-Daten einlesen, sind zwei Aufgaben zu erledigen:   Einrichten des MQTT-Brokers   Sketch des TDMM so erweitern, dass es sich beim Broker einloggt und Daten sendet Für die erste Aufgabe verweise ich auf die vielen Anleitungen im Netz. Mein MQTT-Broker ist Teil der lokalen Home Assistant - Installation und lief nach ein paar Mausklicks. Aber auch das Setup eines Brokers „from the scratch“ ist kein Hexenwerk. Wir gehen im Folgenden davon aus, dass ein MQTT-Broker verfügbar ist.

Nun bleibt noch die etwas anspruchsvollere Aufgabe: Den Sketch des TDMM erweitern. Er wächst damit auf fast 300 Zeilen an. Um den Code besser lesbar zu machen, wurde er teilweise neu mit Funktionen wie current() strukturiert.

 

// TDMM - the talking digital multimeter - German speaking DMM
// Michael Klein & Bernd Schaedler 2024
//  
// Thanks to Rob.Tillaart for: ADS1X15.h
// Thanks to Gerald Lechner for: Talking_Display.h
// Based on WEMOS D1 mini V3 - use board manager LOLIN(WEMOS) D1 mini Pro
#include <ESP8266WiFi.h> //WiFi Lib
#include <PubSubClient.h> //MQTT Lib
#include <ADS1X15.h>
#include <U8g2lib.h>
#include <SoftwareSerial.h>
#include <Talking_Display.h>
#define DFPLAYER_RX 12 //RX Anschluss für die Kommunikation mit dem DFPlayer 
#define DFPLAYER_TX 13 //TX Anschluss für die Kommunikation mit dem DFPlayer 
#define BUSY 14 //D5 Busy Leitung vom DFPlayer

const char* ssid = "Name des Netzwerks";
const char* password = "Mein WLAN Passwort";
const char* mqtt_server = "192.168.0.112";

IPAddress local_IP(192, 168, 0, 50); // Set static IP address
IPAddress subnet(255, 255, 255, 0);
IPAddress gateway(192, 168, 0, 1); // Set your Gateway IP address

WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;

SoftwareSerial ss(DFPLAYER_RX,DFPLAYER_TX); // RX, TX
Talking_Display<SoftwareSerial> td(ss,BUSY);

U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE); u8g2_uint_t offset;
u8g2_uint_t width; 
int32_t val_00 =0; int32_t val_01 =0; int32_t val_02=0; int32_t val_03=0;
float dcurrent = 0; float dcurrenty=0; String dcurrentx = ""; float mid = 2.344;
int32_t vres_00 =0; int32_t vres_01 =0; float resval=0; float resvolts_01 =0;

bool stateD0 = digitalRead(16); bool stateD3 = digitalRead(0); 
bool stateD4 = digitalRead(2); 
 
ADS1115 ADS(0x48);

void setup() {
Serial.begin(19200);

setup_wifi(); 
client.setServer(mqtt_server, 1883);

td.begin();
  ss.setTimeout(2000);
  td.setVolume(27);
  Serial.println("Sprachausgabe bereit");
  td.setEnglish(false);
  td.setWordTimeout(2); 
  td.registerOnError(tdError);

pinMode(16,INPUT); // D0 for "speak"-Key 
pinMode(2, INPUT_PULLUP); // D3 for "say-ampere" switch
pinMode(0, INPUT_PULLUP); // D4 for "say-volt" switch
pinMode(15, OUTPUT); //D8 for relay

Serial.println(__FILE__);
Serial.print("ADS1X15_LIB_VERSION: "); Serial.println(ADS1X15_LIB_VERSION);

Wire.begin(); // D1 mini V3 connect SDA > D2 (Pin 4) SCL > D1 (Pin 5)
ADS.begin();

  if (!ADS.begin()) {
      Serial.println("address ADS1115 or 0x48 not found");
    }
    if (!ADS.isConnected()) {
      Serial.println("address 0x48 not found");  
    }
  Serial.println("AD1115 is properly connected.");
  
  u8g2.begin();
  u8g2.enableUTF8Print();
  startScreen(); // call for initial screen  

  current();
  if (dcurrent < 0.00){
    Serial.println("Calibration in progress");
    while (dcurrent<0.00) {
      mid=mid - 0.001;
      delay(100);
      current(); Serial.print(mid); Serial.print("  "); Serial.println(dcurrent);
    }
  }
  if (dcurrent > 0.00){
    Serial.println("Calibration in progress");
    while (dcurrent>0.00) {
      mid=mid + 0.001;
      delay(100);
      current(); 
      Serial.print(mid); Serial.print("  "); Serial.println(dcurrent);
    }
  }
} // end of setup

void loop() {

  if (!client.connected()) { //wenn die Verbindung abbricht, Neuaufbau
  Serial.println("bin in der reconnect-schleife");delay(1000);
  reconnect();
  }
  client.loop();

  ADS.setGain(0);
  val_01=0;
  for (int x=0; x<10; x++){ // measure voltage
    val_00 = ADS.readADC_Differential_0_1();  
    val_01=val_01+val_00;
    delay(2); // delay 2ms
  }
  val_01=val_01/10; // mean value of voltage
  float volts_01 = ADS.toVoltage(val_01)*22.87; // multiplier 
  String voltsString = String(volts_01);
  Serial.print("U: "); Serial.print(voltsString); Serial.print(" Volt");
  client.publish("mqttuser/tdmm/spannung", String(volts_01).c_str());
  
  current(); // current measurement subroutine

  Serial.print("\t"); Serial.print("I: "); Serial.print(String(dcurrent)); Serial.print(" A");
  client.publish("mqttuser/tdmm/strom", String(dcurrent).c_str());
  client.publish("mqttuser/tdmm/leistung", String(abs(dcurrent*volts_01)).c_str()); 
  client.publish("mqttuser/tdmm/widerstand", String(resval).c_str());
  
  u8g2.clearBuffer(); // show values on OLED 
  u8g2.setFont(u8g2_font_ncenB12_tf);
  u8g2.setCursor(0, 20);
  u8g2.print("U: ");u8g2.print(voltsString);u8g2.print(" V");
  u8g2.setCursor(3, 35);
  u8g2.print("I:  ");u8g2.print(dcurrent);u8g2.print(" A");
  u8g2.setCursor(2, 50);
  u8g2.print("P: ");u8g2.print(abs(dcurrent*volts_01));u8g2.print(" W");
  u8g2.sendBuffer();
  Serial.print("\t"); Serial.print("P: "); Serial.print(abs(dcurrent*volts_01));Serial.println(" W");
  
  stateD0=digitalRead(16); stateD3=digitalRead(2); stateD4=digitalRead(0); 
  if (stateD0 == LOW && stateD3 == LOW) { 
    Serial.println("Volts speaking activated");// volt- & speak-key pressed?
    td.sayFloat(volts_01,2);
    delay(1500);
    td.say(363);
  } 
  if (stateD0 == LOW && stateD4 == LOW) {  // amp- & speak-key pressed?
    Serial.println("Ampere speaking activated");
    delay(1000);
    td.sayFloat(dcurrent,2);
    delay(1500);
    td.say(364);
  }
  stateD0 = HIGH; // set state back to HIGH
  
  if (analogRead(0)>400){ // "resistor-switch set?"
    resistor();
    }
  td.loop();
  delay(1500);
  
} // end of loop

void startScreen() {
  u8g2.clearBuffer(); 
  u8g2.setFont(u8g2_font_ncenB08_tr); // set fontsize
  u8g2.setFontPosTop(); // set position
  const char* title = "-- TDMM --";
  const char* subtitle = "art-of-electronics.blog";
  int xTitle = (128 - u8g2.getUTF8Width(title)) / 2; // title position + center
  int yTitle = 10; // vertical  position of title
  int xSubtitle = (128 - u8g2.getUTF8Width(subtitle)) / 2; // subtitle + center
  int ySubtitle = 40; // subtitle vertical position
  u8g2.setDrawColor(1); // textcolor (1 = white)
  u8g2.drawStr(xTitle, yTitle, title);
  u8g2.drawStr(xSubtitle, ySubtitle, subtitle);
  u8g2.sendBuffer(); // send display data
  td.say(362); // Start tune
  delay(3500);
}

void current(){
  val_02=0; dcurrent=0; dcurrenty=0; // measure current
  for (int y=0; y<10; y++){
    val_02 = ADS.readADC(2); 
    float volts_02 = ADS.toVoltage(val_02);  
    dcurrenty = (volts_02 - mid)*5.6482; // multiplier for 0.185 mV/A
    dcurrent=dcurrent+dcurrenty; 
  }
  dcurrent=dcurrent/10; // mean value of current
  String dcurrentx = String(dcurrent);
}

void resistor() {
  float mesres=470;
  vres_01 = ADS.readADC(3); 
  resvolts_01 = ADS.toVoltage(vres_01);
  if (resvolts_01 >= 3.2) {
	    ADS.setGain(0); digitalWrite(15, HIGH); mesres=4700; delay(500);
	    }
	    else if (resvolts_01 < 3.2 && resvolts_01 >= 1.37) {
	    ADS.setGain(1); 
	    }
	    else if (resvolts_01 < 1.37 && resvolts_01 >= 0.68) {
	    ADS.setGain(2); 
      }
      else if (resvolts_01 < 0.68 && resvolts_01 >= 0.34) {
	    ADS.setGain(4); 
      }
      else if (resvolts_01 < 0.34 && resvolts_01 >= 0.17) {
	    ADS.setGain(8); 
      }
	    else {
	    ADS.setGain(16);
	    }
  vres_01=0; resvolts_01=0; float voltsum=0;
  for (int x=0; x<10; x++){ // measure voltage at divider
    vres_01 = ADS.readADC(3);  
    resvolts_01 = ADS.toVoltage(vres_01);
    voltsum=voltsum+resvolts_01;
    delay(2); // delay 2ms
  }
  resvolts_01=voltsum/10; // mean value of voltage
  resval= resvolts_01*mesres/(3.32-resvolts_01);
  if (resval <0){
    resval=999999;
  }
  digitalWrite(15, LOW); // relay off
  String ohmString = String(resval);
  u8g2.setFont(u8g2_font_cu12_t_symbols);
  u8g2.setCursor(0, 0);
  u8g2.print("R: ");u8g2.print(ohmString);
  u8g2.print(" \u2126");
  u8g2.sendBuffer();
  u8g2.setFont(u8g2_font_ncenB12_tf);
  Serial.print("R: "); Serial.print(int(resval)); Serial.println(" Ohm  ");
  if (digitalRead(16) == LOW){
    td.sayInt(int(resval)); 
    delay(1000);
    td.say(365);
  }
}

void tdError(String msg) {
  Serial.println(msg);
}

void setup_wifi() {

  // Configures static IP address
  if (!WiFi.config(local_IP, gateway, subnet)) {
    Serial.println("static IP failed to configure");
           }

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println("Hallo - hier ist das TDMM");
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println(""); 
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    //if(client.connect("mqttuser")) {
    if (client.connect("homeassistant20","mqttuser","mein_MQTT_passwort")) {
    Serial.println("connected");
    } 
    else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

Die neuen Fähigkeiten des TDMM erfordern zwei weitere Libraries:  <ESP8266WiFi.h> die klassische WiFi-Bibliothek der 8266-Prozessorfamilie, sowie die nicht ganz so bekannte MQTT-Library <PubSubClient.h>. Diese Library übernimmt für uns das Publizieren der Daten und die Verbindung mit dem MQTT-Broker.

Die ersten 15 Zeilen bleiben bis auf die beiden neuen includes für diese Libraries unverändert. In den Zeilen 17 … 19 folgen die Credentials ihres Wlan sowie die IP-Adresse oder der Name des MQTT-Brokers. Wer mit dynamischen IP-Adressen arbeitet, setzt dort den Namen ein. Werden statische IP-Adressen für den Server eingesetzt, steht hier seine statische IP.

In den Zeilen 25 … 27 bilden wir die notwendigen Instanzen. Danach läuft das Programm so weiter, wie Sie es schon kennen.

im setup() wird danach die Wifi-Verbindung initiiert, sowie unser MQTT-Server auf dem Port 1883. Auf diesem Port läuft der Betrieb unverschlüsselt. Es ist auch verschlüsselter Betrieb möglich.

Nullstellung für Strommessung automatisieren - noch eine Verbesserung

In setup() Zeile 80 wird die Funktion current() aufgerufen. Die Strommessung wurde in diese Funktion ausgelagert. Hier wird sie einmal aufgerufen, um den Null-Ruhestrom zu messen. Die nachfolgenden Zeilen dienen der Nullstellung der Strommessung. Dabei wird schrittweise der Faktor „mid“ angepasst, der jetzt die Nullstellung der Hall-Spannung übernimmt. Das funktioniert recht gut. Sie sollten vor dem Start des TDMM die beiden Buchsen der Strommessung für diesen Abgleich kurzschließen.

Die Strommessung mit dem Hall-Element ist einigermaßen empfindlich. Für die praktische Anwendung kann ich empfehlen, das TDMM auf dem Labortisch zu platzieren, dann erst einzuschalten und danach möglichst nicht mehr zu verschieben - wenn Magnetfelder in der Nähe sind. Und das kann jedes Magnetfeld eines Transformators, Magneten ... was auch immer sein. Ein Neodym-Magnet reicht.
Messung in der loop()
Nachdem wir alles eingerichtet haben, gibt es in der loop() nicht mehr viel neuen Code. Gleich zu Beginn wird überprüft, ob die Verbindung zum MQTT-Broker steht. Falls nicht, geht das System in die reconnect-Schleife und stellt die Verbindung wieder her. Bei jedem Schleifendurchlauf erfolgt der Aufruf von client.loop();.

Nun brauchen wir nur noch nach jeder Messung die Daten ans Display zu schicken und mit client.publish( ….) an das richtige Topic des MQTT-Brokers. Mehr ist nicht mehr nötig.

Darstellung der Messwerte auf einem Mobiltelefon

Es gibt etliche MQTT-Apps, viele davon kostenfrei, die MQTT-Topics lesen und darstellen können. Meist können sie noch viel mehr, z.B. auch Daten an den Broker senden, um damit Vorgänge zu steuern, Objekte ein- und auszuschalten, Werte für eine Einstellung übergeben usw.

In unserem Fall wird eine iPhone-App verwendet und zwar „IoT MQTT Panel“. Diese App findet sich im AppStore. Sie ist kostenfrei und funktioniert ordentlich. Es gibt eine Vielzahl von Möglichkeiten, Informationen, Werte, rein binäre Daten und mehr darzustellen. Ähnliches gibt es für Android-Handys.

Für dieses Beispiel habe ich ausgewählt:

   Grafikdarstellung des Topics „mqttuser/tdmm/spannung“ ganz oben. Die App skaliert die Darstellung automatisch. Der Spannungssprung auf 5,2 V und zurück wird sauber dargestellt - sonst würden Sie auf der Grafik nur einen Strich bei 5,8 V sehen. Die X-Achse zeigt die Zeit der Messungen.

   In der Mitte die Darstellung des gleichen Topics mit der Funktion „Gauge“, die ein Messgerät mit Zeiger darstellt. Auch dafür sind weitere Detail-Einstellungen möglich.

   Ganz unten das Topic „mqttuser/tdmm/widerstand als reines Textfeld. Sie sehen zwischen den Klemmen am TDMM einen Widerstand von 4.7 kΩ.  Die Stellen hinter dem Komma sind irrelevant.

Man sieht auf den Foto, dass die Darstellung exakt den Werten des TDMM entspricht.

Darstellung im Home Assistant

Viele Leser werden Home Assistant kennen, eine weit verbreitete Softwarelösung zur Heimautomatisierung. Persönlich halte ich dieses Projekt für großartig, besonders deswegen, weil es beliebig ausbaufähig ist. Es können sogar selbst entwickelte Python-Skripts integriert werden.

In diesem Fall habe ich den HA zum „Lab Assistant“ gemacht. Dazu muss der HA zunächst von den MQTT-Entitäten Kenntnis bekommen, was man mit dem File-Editor bewerkstelligt.

Anschließend braucht der HA einen Neustart. Danach stehen die Entitäten zur Verfügung und können überall eingesetzt werden. Sie werden sich erinnern, dass MOSQUITTO auf dem HA läuft und zwar als sog. „Add-On“. Laden, konfigurieren - fertig. Eine komfortable Lösung ohne Firlefanz.

Sobald diese Schritte abgeschlossen sind, kann man ein neues Dashboard für die Messergebnisse einrichten. Ich habe das mit mehreren Messmöglichkeiten des TDMM gleichzeitig gemacht, auch um nochmals die schöne Eigenschaft des TDMM hervorzuheben: Alle Messungen erfolgen gleichzeitig. Wer HA kennt, braucht keine Infos mehr dazu, wie Dashboards aussehen können. Mein Beispiel sieht so aus:

Sie sehen in der linken Spalte die reine Wertedarstellung. In der Mitte habe ich Spannung und Strom mit der „Gauge“-Funktion dargestellt. Ganz rechts steht oben der Widerstandswert in einer Einzeldarstellung, darunter die Leistung als Grafik.

HA bietet noch viel mehr Elemente zur Darstellung. Es gibt PlugIns mit komfortablen Grafiken, teils mit Mittelwertbildung, Standardabweichung u.v.a.m. Hier sehen Sie nur einen Blick auf die Fülle der Möglichkeiten.

Übernahme der Daten durch ein Python-Skript

Viele von uns arbeiten sicherlich auch mit Python. In der professionellen Messtechnik werden häufig Messprogramme geschrieben bzw. Messungen automatisiert. Hierfür braucht man nicht unbedingt zu LabView greifen oder einem anderen, komplexen Framework. Python kann ohne Umwege direkt eingesetzt werden. Dazu wird die Library paho.mqtt.client genutzt.

Hier ein einfaches Skript ohne Schnörkel, das Werte des TDMM abholt und darstellt:

DOWNLOAD PYTHON SCRIPT

Die Kommentare sollten ausreichend verdeutlichen, was die einzelnen Funktionsaufrufe bewirken.
Selbstverständlich passen Sie die Credentials Ihrem jeweiligen Netzwerk an.

Als Ergebnis liefert das Skript bei jedem neuen „publish“ auf das Topic  mqttuser/tdmm/spannung
den Wert  auf dem Bildschirm.

Wenn man diese Werte z.B. in einer POSTGRES-Datenbank speichern will, sind jetzt nur noch wenige Schritte erforderlich, um eine ganze Messreihe komfortabel zu speichern. Danach wertet man sie aus, errechnet ggfls. statistische Größen, Mittelwert, Standardabweichung etc.

Fazit

Wenn es mir gelungen ist, den Nutzen von MQTT als Protokoll für unseren Zweck zu vermitteln und die einfache Handhabung etwas zu verdeutlichen, ist für heute das Ziel erreicht.

Nun wünsche ich Ihnen für die praktische Umsetzung ein klein wenig Geduld und ganz viel Spaß mit dem TDMM und seinen Möglichkeiten.

Ausblick 

Der kommende Teil 6, der vorerst letzte Teil dieser Blogartikelreihe, führt Sie zu Anwendungen des TDMM mit erweiterter Programmierung und Steuerung. Sie werden sehen, wie ein so kompaktes, kostengünstiges Gerät auch Anwendungen bewältigt, die fast schon „professionell“ genannt werden dürfen.

Bis dahin
Ihr Michael Klein

DisplaysEsp-8266Projekte für anfängerSensorenSmart home

Laisser un commentaire

Tous les commentaires sont modérés avant d'être publiés

Articles de blog recommandés

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery