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
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:

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:
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