Es gibt Projekte, die entstehen aus einer spontanen Idee, und dann gibt es solche, die sich über Monate hinweg entwickeln. Unser Mini-Arcade-Automat gehört definitiv zur zweiten Kategorie. Alles begann an einem kalten Wintertag, als mein Sohn und ich uns an den Küchentisch setzten, um wieder einmal ein kleines Bastelprojekt zu starten. Damals ahnten wir noch nicht, dass aus einem einfachen Papiermodell irgendwann ein funktionierender Mini-Arcade-Automat werden würde. Doch genau das ist passiert – und in diesem Beitrag möchte ich euch Schritt für Schritt mitnehmen, wie wir dieses Projekt umgesetzt haben.
Die Inspiration: Ein Arcade-Automat aus Papier Die Idee, einen Mini Arcade-Automaten zu bauen, kam uns ganz spontan. Ich habe ja schon vor einigen Jahren einen Tabletop- bzw. Bartop Arcade Automaten gebaut, den mein Sohn immer wieder einmal in Beschlag nimmt und damit spielt. Irgendwann einmal hat er eine ganz kleine „Mini Arcade Machine“ (die man in Fernost für unter 10 Euros kaufen kann) bekommen. Die bereitet aber aufgrund der sehr kleinen Abmessungen und dem Bildschirm von knapp 5 cm nur kurz Vergnügen. Auch die Qualität der fix eingebauten Spiele ist etwas grenzwertig. Als er wieder einmal mit dem kleinen Teil spielte kam die Frage: „Können wir so etwas aus Papier basteln?“, fragte er. Natürlich konnten wir! Also suchten wir uns eine alte Verpackungsschachtel, die von einer Lieferung eines großen Onlineversenders übrig geblieben war, und begannen, die ersten Entwürfe zu zeichnen.
Schritt 1: Skizzen und Planung Mit einem Lineal, einem Bleistift zeichneten wir die Umrisse des Automaten direkt auf die Kartonage. Wir orientierten uns dabei an den Massen des kleinen Arcade-Automaten: ein leicht nach hinten geneigtes Display, ein breiter Sockel und eine Frontplatte mit Platz für Tasten und Hebel. Die Proportionen wurden einfach hochskaliert… Sohnemann musste abmessen und seine in der Schule erwobenen Multiplikationsfähigkeiten praktisch anwenden …
Schritt 2: Ausschneiden und Zusammenkleben Mit einer Schiene und einem Teppichmesser schnitten wir die einzelnen Teile aus. Das war gar nicht so einfach, denn der Karton war dicker, als wir gedacht hatten, und die Kanten sollten möglichst sauber sein. Nachdem alle Teile ausgeschnitten waren, klebten wir sie mit Schmelzkleber zusammen. Der Schmelzkleber war ideal, da er schnell abkühlt und eine stabile Verbindung schafft.
die ausgeschnittenen Kartonteileund so sind sie angeordnetmit Schmelzkleber werden die Teile aneinandergeklebt
Schritt 3: Bemalen und Dekorieren
Der fertige Papierautomat sah schon ganz gut aus, aber er war noch etwas langweilig. Also bemalten er ihn mit seinen Filzstiften en und verpasste ihm ein klassisches Design in Schwarz. Für die Tasten und Hebel bastelten er kleine Knöpfe aus Papier, die er auf die Frontplatte klebte. Das Ergebnis war ein kleiner, aber detailreicher Arcade-Automat aus Papier.
der fertig zusammengeklebte Papier Arcadedas fast fertig bemalte Papiermodell
Die Idee wächst: Vom Papiermodell zum fuktionstüchtigen Automaten Das Papiermodell war ein voller Erfolg, und wir hatten viel Spaß beim Basteln. Doch irgendwann kam mir der Gedanke: Was wäre, wenn wir das Ganze aus Holz bauen und mit echten Tasten, einem Bildschirm und einem kleinen Computer, zum Beispiel einen alten Raspberry PI ausstatten würden? So könnte er einen schönen Arcade Automaten haben und auch tatsächlich darauf spielen. Die schlechteren Tage der Sommerferien boten sich dafür an und so begann die Planung für den Bau eines echten Mini-Arcade-Automaten.
Materialwahl: MDF-Platten und Lasercutter Für den Bau des Automaten entschied ich mich für MDF-Platten mit einer Dicke von 6 mm. MDF ist ein ideales Material für solche Projekte: Es ist stabil, leicht zu bearbeiten und hat eine glatte Oberfläche, die sich gut lackieren lässt. Außerdem ist es relativ günstig, was bei einem Hobbyprojekt natürlich ein großer Vorteil ist.
Schritt 1: Digitalisieren der Skizzen Die Skizzen des Papierautomaten dienten als Vorlage für die Holzkonstruktion. Mit der Software Inkscape zeichnete ich die Entwürfe und passte sie an die Maße der MDF-Platten an. Dabei achtete ich darauf, dass die Proportionen stimmten und die einzelnen Teile später gut zusammenpassen würden. Die endgültigen Maße des Automaten sollten etwa 25 x 30 cm betragen – klein genug, um handlich zu sein, aber groß genug, um ein authentisches Arcade-Gefühl zu vermitteln.
die in den Lasercutter importierten inkscape files
Schritt 2: Zuschnitt mit dem Lasercutter Der nächste Schritt war der Zuschnitt der MDF-Platten. Hier kam ein Lasercutter zum Einsatz, der die Teile mit hoher Präzision ausschneiden konnte. Die Möglichkeit, Nuten in die Platten zu schneiden, war hier besonders praktisch: Diese Nuten ermöglichten es, die Teile später passgenau rechtwinklig zusammenzusetzen und zu verkleben.
der Lasercutter bei der Arbeit
Nachdem alle Teile ausgeschnitten waren, begann der Zusammenbau. Zunächst prüfte ich die Passform der einzelnen Teile. Dank der präzisen Arbeit des Lasercutters passte alles perfekt zusammen. Die Nuten erleichterten den Aufbau enorm, da sie die Platten in der richtigen Position hielten, während der Kleber trocknete.
alles passt zusammenauch das Bedienpanel wird bedienbar sein
Schritt 3: Lackierung Nachdem der „Rohbau“ fertig war, ging es an die Lackierung. Ich entschied mich für einen schwarzen Acryllack, der dem Automaten ein etwas professionelleres Finish verleihen sollte. Die glatte Oberfläche der MDF-Platten war ideal für die Lackierung, und nach zwei Schichten sah der Automat fast aus wie ein echtes Arcade-Gerät in klein.
Das Frontglas, das den LCD-Bildschirm schützt, besteht aus Acrylglas. Um den Rand der Schutzabdeckung gleichmäßig schwarz lackieren zu können, wird die Schutzfolie, die sich auf dem Acrylglas befindet, im Randbereich entfernt. Anschließend wird der freigelegte Randbereich des Acrylglases schwarz lackiert, um eine saubere und gleichmäßige Optik zu erzielen.
Ausschneiden der Schutzfolie des Acrylglases um den Rahmen zu maskieren
Das Frontglas, das den LCD-Bildschirm schützt, besteht aus Acrylglas. Um den Rand der Schutzabdeckung gleichmäßig schwarz lackieren zu können, wird die Schutzfolie, die sich auf dem Acrylglas befindet, im Randbereich entfernt. Anschließend wird der freigelegte Randbereich des Acrylglases schwarz lackiert, um eine saubere und gleichmäßige Optik zu erzielen. So nach und nach nahm das Gehäuse Formen an und es war an der Zeit die technischen Komponenten einzubauen.
der Aufbau nimm langsam Formen an
Das Herzstück des Innenlebens ist ein alter Raspberry PI 3 mit einem Retropie – SD Karten Image. Für die Bedienung und Steuerung entschied ich mich für farbige Drucktaster mit 16mm Durchmesser. Davon finden zehn Stück auf dem Bedienpanel Platz. Auf der linke und der rechten Seite ist je ein Taster platziert. Aus Bequemlichkeit und auch aus Kostengründen bastelte ich keinen eigenen Controller für die Tastenbedienung des Raspberry, sondern nahm einen fertigen USB-HID Controller, der nur ein paar Euro teuer war. Der Bildschirm ist ein 5 Zoll 800×600 Pixel LC-Display und um die PCM Soundausgabe des Raspberry auf einem kleinen Lautsprecher hörbar zu machen, kam ein fertiges „superlowcost“ Class D Verstärkerboard zum Einsatz.
die Innenkanten des Monitorrahmens müssen geschwärzt werden, damit es später keine Reflexionen in der Acrylscheibe gibt
Nachdem nun alle Gehäuseteile fertig lackiert waren und auch alle Komponenten für den Innenausbau vorbereitet waren, begann schlussendlich der Zusammenbau. Da alle Gehäuseteile verklebt sind, aber die Technik natürlich zugänglich bleiben soll, haben wir auf der Rückseite eine Revisionstüre eingeplant…
Tastenfeld mit Farbbestückung nach Juniors Wunschder HID Tasencontroller findet seinen Platzder Bildschirm mit dem Raspberry PI wird eingesetzthier sind alle Kompnenten fertig eingebaut
Jetzt fehlt nur mehr der Revisionsdeckel auf der Rückseite. Um die Abwärme des Raspberry Pi loszuwerden haben wir ein paar Lüftungsschlitze in den Deckel geschnitten. (genauer gesagt der Laser hat geschnitten 🙂 )
Deckel an der Rückseite
Zur Stromversorgung dient ein 5V / 20W Netzteil, das an einer hinten angebrachten Netzteilbuchse angesteckt wird. Es versorgt sämtliche Baugruppen (Raspberry, Audioverstärker und Monitor).
Die verbaute Technik hier nochmal in der Übersicht:
Raspberry Pi 3 mit 32GB SD Karte
5-Zoll-LCD-Bildschirm: Waveshare 5inch 800×480
HID-Controller: USB noname Joystickcontroller (Onlineversand)
Tasten: 16 mm Momentary Push Button (30Stk Set im Onlineversand)
Audioverstärker: DollaTek 3W DC 5V PAM8403 (Onlineversand)
Stromversorgung: 5V/20W-Netzteil
vom Papiermodell zum bespielbaren MiniArcade
Da mir für´s Ende des Blogbeitrags kein vernünftiger Text einfällt, hat die KI für mich nachgedacht und den folgenden Absatz erzeugt:
Nach Wochen harter Arbeit war es endlich soweit: Der Mini-Arcade-Automat war fertig. Mit seinem schwarzen Gehäuse, den farbigen Tasten und dem kleinen Bildschirm sieht er nicht nur aus wie ein echter Arcade-Automat, sondern spielt sich auch so. Mein Sohn war begeistert – und ich auch. Es ist ein Projekt, das uns nicht nur viel Spaß gemacht hat, sondern auch zeigt, wie aus einer einfachen Idee etwas Großartiges entstehen kann.
Fazit: Manchmal braucht es nicht viel, um etwas Besonderes zu schaffen: ein bisschen Karton, ein paar MDF-Platten und eine große Portion Begeisterung. Unser Mini-Arcade-Automat ist der beste Beweis dafür, dass Basteln nicht nur kreativ, sondern auch unglaublich erfüllend sein kann. Wer weiß, vielleicht ist das ja der Anfang einer ganzen Serie von Mini-Projekten? 😊
In dem Beitrag Pylontech PV-Akkustatus im HomeAssistant hatte ich das Projekt „Pylontech-Battery-Monitoring“ der folgenden GitHub Links etwas aufgehübscht und eine Platine gezeichnet, um das ganze Konstrukt etwas kompakter und professioneller aufzubereiten. https://github.com/irekzielinski/Pylontech-Battery-Monitoring https://github.com/hidaba/PylontechMonitoring
Im Homeassistant wurden damit, bzw. werden, sämtliche Batteriedaten der Pylontech Akku Module angezeigt. Super! Wenn ich aber einen Blick in die Geräteliste der, in meinen Wifi – Netzen angemeldeten Geräte ansehe, wird mir fast übel – es sind mittlerweile viel zu viele Funkgeräte, vor allem aus dem SmartHome Bereich, die sich die Kanalbandbreite teilen. So ist es mein aktueller Plan, einige der Smarthome Geräte ins verkabelte LAN-Netzwerk zu bringen.
Die selber gebastelten Geräte auf Basis der ESPs bieten sich dafür an. Das sind dann Geräte, wie das OpenDTU Interface, das EVU-Smarthomeinterface oder wie hier, die Schnittstelle von der seriellen Console der Pylontechakkus zum MQTT Server im Homeassistant.
Dazu habe ich einige Versuche mit Boards wie dem OLIMEX ESP32-PoE und dem WT32-ETH01 gemacht. Die Olimexplatine hätte den großen Vorteil, auch über PoE mit Energie versorgt werden zu können. Allerdings ist die Spannungsversorgung bei PoE Betrieb so „schlecht“, dass die benötigten Standards externer Boards nicht erfüllt werden. Hier kann ich zum Beispiel das Funkmodul NRF24L01 erwähnen. Damit habe ich einige Versuche gemacht und beschlossen, die PoE Funktionalität vorerst einmal außer Acht zu lassen. So entstand dann der Plan mit dem WT32-ETH01 auf dem ein ESP32 arbeitet, ein universelles Board zu designen, auf dem mehrere Schnittstellen vorhanden sind. Es sollte folgendes können:
mit OpenDTU und NRF24L01 mit den PV-Invertern kommunizieren
über die Pylontech Console via MQTT mit dem Smarthome System kommunizieren
eine optionale CAN Schnittstelle besitzen
neben der RS232 Schnittstelle auch über RS422/RS485 kommunizieren zu können
die Spannungsversorgung über 5V USB zu erhalten
und alles schön klein und kompakt in einem Gehäuse verpackt zu haben
So habe ich dazu eine Schaltung entworfen und eine Platine gezeichnet. Bei einer Fernost PCB Manufaktur ließ ich die Boards fertigen. Das Bestücken ist auch schnell erledigt.
Schaltplan des Universal Lan Interface
Das nachstehende Bild zeigt das Platinenlayout vor der Fertigung.
Das WT32-ETH01 Board besitzt keinen USB-Port zum Programmieren des Controllers. Es wird über einen externen USB-UART Adapter programmiert. Um den Programmiermodus zu aktivieren, muss auch ein IO Pin gegen GND geschaltet werden. Um das etwas zu vereinfachen, gibt es jetzt auf dem Board einen Jumper „PROG“. Wenn diese Brücke gesteckt ist, kann der der WT32 die Firmwarefiles empfangen. Einen Pinheader Steckplatz „TO-FTDI“ habe ich als Anschlussmöglichkeit für den USB-UART Adapter vorgesehen.
Die Platine ist nun so konzipiert, dass man damit unterschiedliche Geräte bedienen kann. Schliesst man ein NRF24L01 Modul an den Pinheader „NRF24L01+“ an und flasht das ESP32-OpenDTU Image auf den Controller, dann können damit die Wechselrichterdaten empfangen und über das LAN Netzwerk übertragen werden. Ein geeignetes IO-config jason-file für die Benutzung des WT32 habe ich erstellt.
Eine weitere Anwendung ist die Verwendung der Platine mit der seriellen Ausgabe der Batteriedaten der Pylontech PV Akkus. Die Akkus stellen einen „Console“ Port zur Verfügung der eine RS232 Schnittstelle darstellt. Über diese werden die Daten zum WT32 Controller übertragen und stehen dann über LAN im lokalen Netzwerk zur Verfügung.
Dazu habe ich das ESP8266 script von hidaba und irekzielinski für die ESP32 Controller angepasst. (siehe Code am Ende des Beitrags)
Ist der Code kompiliert und hochgeladen, dann sollte nach Anschluss aller Verbindungen unter der eingestellten IP-Adresse der Status der Pylontec Batterien zu sehen sein.
Ausführung der Platine mit Pylontech SetupAusführung mit OpenDTU und NRF24L01 Setup
Die im Bild dargestellten Gerätesetups sind mit einem Gehäuse ausgestattet. Die „.stl“ Dateien dazu habe ich mit FreeCad erstellt auf thingiverse veröffentlicht.
„Frisch gewischte Böden, ohne davor Staub zu saugen: Der Hartbodenreiniger FC7 Cordless beseitigt alle Arten von trockenem und feuchtem Alltagsschmutz in einem Schritt.“ (Originaltext kaercher.com)
Dieses Produktversprechen bekommt man auf der Webseite des Herstellers, wenn man sich für die elektrischen Hartbodenreiniger FC7 interessiert. Wenn dieses Versprechen aber einmal nicht mehr wahr gemacht wird, dann erfahre ich von der Existenz dieser Geräte. Denn dann werde ich gebeten, einmal nachzusehen, warum etwas nicht mehr so tut wie es soll. So auch in diesem Fall. Die Bürsten (Walzen – wie auch immer diese Teile bezeichnet werden) drehen sich nicht mehr, so die Problembeschreibung. Oder genauer gesagt, sie drehen sich nur manchmal, wenn das Bodenteil zum beweglichen Stiel eine bestimmte Position einnimmt. Und da der Stiel (in dem die ganze Elektronik, wie Akkus, BMS und Bedienelemente untergebracht sind) in einem weiten Spielraum beweglich ist, liegt die Vermutung nahe, dass hier ein Kabelbruch oder ähnliche Kontaktprobleme vorliegen.
Das ist jetzt nicht gerade ein komplexes Problem, aber vielleicht interessiert den einen oder anderen doch, wie man das Problem beheben mit mehr oder weniger Aufwand beheben kann.
Im ersten Schritt sind der Schmutzwasserbehälter und die vier Reinigungswalzen zu entfernen. Danach können die Schrauben der Antriebsabdeckung und der Batterieabdeckung gelöst und die Abdeckungen entfernt werden.
Schrauben der AntriebsabdeckungSchrauben der Batterie-/ Elektronikabdeckung
Sind die Abdeckungen gelöst, können sie entfernt werden. Unter der Batterieabdeckung ist die Platine mit dem BMS und der Steuerelektronik des Gerätes zu sehen. Darunter befinden sich die 18650er Li-Ion Zellen. Die Abgänge zum Bodenantrieb, zur Bedieneinheit im Griff, etc. sind gesteckt.
Platine mit BMS und Steuerung
Der achtpolige Stecker links unten im Bild ist zu lösen. Er verbindet den Bürstenantrieb mit der Elektronik. Von den acht Polen des Steckers sind sechs Pins belegt. Ein roter und ein schwarzer Draht als Zuleitung zum DC – Motor (ja, hier wurde nur ein DC-Bürstenmotor verbaut und kein Brushless …) weiters sind zwei braune Drähte zu den Stiften, die den Widerstandssensor für den Wasserstand im Schmutzwasserbehälter bilden, verlegt. Zwei blaue Drähte steuern das Magnetventil des Wasserzulaufs an.
Da der Fehler beim Antrieb des Motors liegt (je nach Lage des Stiels dreht sich der Motor, oder eben nicht), ist der Fehler möglicherweise in der Kabelverbindung von der Platine bis zum Motor zu suchen. Mit der Durchgangsprüfung des Multimeters war der Fehler schnell entdeckt. Die schwarze Leitung zum Motor war gebrochen.
gebrochene Leitung (schwarzer Draht zum DC Motor)
Die Bruchstelle befindet sich genau in dem Bereich, wo der Stiel in der Bodeneinheit beweglich befestigt ist. Genau hier werden der Kabelbaum und der Gummischlauch für die Wasserführung eingeführt. Das permanente Bewegen des Kabelbaumes führt dann längerfristig zwangsläufig zur Beschädigung und zum Bruch der Leitungen. Vor allem wenn man den Stiel in sehr flachen Winkeln benützt, um beispielsweise den Boden unter Kästen, Kommoden etc. damit reinigt.
Draht gelötet und mit Schrumpfschlauch isoliert
Die Reparatur habe ich hier mittels Zusammenlötens des Drahtes und Schützen mit einem Schrumpfschlauch durchgeführt. Den beschädigten Kabelschutzschlauch habe ich mit Isolierband umwickelt. Das sollte wieder einige Zeit halten. Da das Teil konstruktionsbedingt nicht bis in alle Ewigkeit halten wird, sollte bei der nächsten Reparatur der Kabelbaum komplett erneuert werden. (da der vermutlich nicht als Ersatzteil erhältlich ist, wird man wohl selber einen anfertigen müssen – dann aber gleich mit stabileren, hochflexiblen Drähten…)
Jetzt konnten die Komponenten wieder zusammengebaut werden. Die Rollen entsprechen den Farben grün / blau auf die Antriebsnaben aufsetzen und wieder alles zusammenschrauben.
Bei diesem Gerät sind offensichtlich gebrochene Kabelverbindungen und abgerissene Zahnriemen in der Motoreinheit die häufigsten Störfälle.
In diesem Beitrag beschäftige ich mich ein wenig mit der Restauration – oder eher -Instandsetzung eines Handheld Spiels das ich kürzlich zur Begutachtung erhalten habe. Es hat die Bezeichnung „Tricky Traps“. Das bedeutet so viel wie „tückische oder knifflige Fallen“
Das Spiel „Tricky Traps“ von Tomy ist ein mechanisches Geschicklichkeitsspiel, das ursprünglich in den 1970er Jahren veröffentlicht wurde. Es besteht aus einem labyrinthartigen Spielfeld, bei dem der Spieler eine kleine Metallkugel durch eine Reihe von Hindernissen und Fallen navigieren muss. Ziel des Spiels ist es, die Kugel erfolgreich durch das Labyrinth ins Ziel zu manövrieren, ohne dass sie in eine der vielen Fallen fällt. Es stehen fünf Kugeln zur Verfügung. Das Spiel läuft auf Zeit.
Tricky Traps
Spielmechanik:
Der Spieler startet das Spiel mit einem Drehknopf, der einen kleinen Elektromotor in Gang setzt. Dieser Motor treibt die „Fallen“ an und über ein Getriebe auch den Drehknopf selbst. So wird der „Timer“ realisiert. Denn hat der Drehknopf etwa eine dreiviertel Umdrehung hinter sich, so bleibt der Motor wieder stehen und das Spiel ist beendet. Dies wird durch eine Kontaktfeder gelöst, die von einem kleinen Steg unten am Drehknopf nach unten auf einen Gegenkontakt gedrückt wird.
Ist das Spiel gestartet so kann mit dem roten Knopf eine Kugel in die Bahn entlassen werden. Der weise Knopf unten ist der eigentliche und einzige Spielknopf. Er hebt die Kugel mit einem kleinen Zylinder an, sodass sie sich durch die verschiedenen Teile des Spielfelds bewegen kann. Das muss man mit dem richtigen Timing machen.
Auf dem Spielfeld befinden sich zahlreiche Hindernisse wie rotierende Scheiben, kleine Rampen, schmale Passagen und ein rotierender Magnet, die die Kugel stoppen oder in eine Falle fallen lassen können.
Das farbenfrohe Design ist typisch für die mechanischen Spiele der 70er und 80er Jahre. Es besteht aus Kunststoff, und die beweglichen Teile sind meist in leuchtenden Farben gehalten.
Die technischen Probleme, die bei solchen Spielen immer wieder auftreten, sind:
ausgelaufene Batterien, die meist Korrosion und Zerstörung der Kontakte verursachen
Spröder Kunststoff, der vor allem bei Zahnrädern auftritt, die auf Messingwellen aufgesteckt sind und so zu rutschen beginnen. Auch halten so Gehäuseschalten oft nicht mehr vernünftig zusammen.
Elektromotoren deren Bürsten abgenutzt sind, sodass sie nicht mehr drehen
Verharzte Fette und Öle, die bewegliche Teile schwergängig machen
Drähte und elektrische Verbindungen, sie korrodiert und gebrochen sind
Alle diese Punkte sind bei der Restauration immer wieder zu finden und müssen behoben werden. Dies lässt sich auch mehr oder weniger einfach realisieren. Ich beginne eigentlich nach einer vorsichtigen Öffnung und Begutachtung des Gerätes mit einer kompletten Demontage und Reinigung der Teile. Dann versuche ich gebrochene Kunststoffteile zu reparieren. Hier verwende ich soweit möglich verschiedene Klebstoffe. Manchmal ist es auch notwendig, ein Teil auch mit einem 3D Drucker nachzubauen. Dies setzt natürlich voraus, dass vom originalen Teil noch genug vorhanden ist, um es passgenau nachzukonstruieren. Die elektrischen Komponenten, sind bei diesen Geräten am einfachsten zu reparieren, da meist keine Elektronik mit irgendwelchen Bauteilen mit nicht mehr hergestellten ICs verbaut sind.
Dieser Beitrag hat diesmal so rein gar nichts mit Retro zu tun. Mein Kollege aus dem Studienzweig Multimedia hat sich mit dem Thema DeepFakes beschäftigt und mit viel Mühe einen tollen Videobeitrag erstellt. Hier der Link zum Video:
edit 7.11.24
Ich habe mittlerweile auch eine Interfaceplatine mit USB Typ B Buchse zur 5V Versorgung gezeichnet. (s. Layout unten). Denn so klein und fein die Micro USB Steckerchen auch sind, ich brauch‘ was robusteres.
neue Boardversion mit USB-Type B Buchse für die Stromversorgung
Da ich immer öfter nach den Fertigungsdaten gegfragt werde, stelle ich die Gerberdaten der Platinen zum Download zur Verfügung:
In dem Beitrag mit dem Titel: „EVU Smartmeter mit ESP32 auslesen und Daten per MQTT senden“ (link) habe ich beschrieben, wie sich die Smartmeter der EVUs über die Kundenschnittstelle auslesen lassen. Die Messdaten stehen dann als Topics über den mqtt Broker zur Verfügung und können in diversen Homeautomationen (HomeMatic, Homeassistant, etc.) weiterverarbeitet werden. Dazu benötigt man lediglich eine ESP32-Platine und ein paar wenige Kleinteile, um die Verbindung zum Smartmeter herstellen zu können. Als kleines Update habe ich den Aufbau (damals mit Stiftleisten auf Lochrasterplatine) mittlerweile ein wenig geschönt und eine Platine gefertigt.
Layout im Designtool
Der zugehörige Schaltplan entspricht im Wesentlichen auch der Skizze im damaligen Beitrag. Um ein wenig Komfort mit der neuen Platine zu erhalten, ist die Verbindung zur Kundenschnittstelle des Smartmeters über eine RJ-Buchse steckbar. Und auch die Spannungsversorgung habe ich über eine USB-Buchse realisiert.
Nach dem Bestücken und Aufstecken der ESP32 Platine bekam das Gerät noch ein kleines Gehäuse spendiert und verrichtet nun im E-Verteilerschrank seinen Dienst.
Die Hardware ist somit fertig und funktionstüchtig. Zum Thema Software habe ich mir auch überlegt, etwas zu ändern. Bis jetzt lief auf dem ESP ein Programm, das die Daten des Smartmeters entschlüsselt und dann per MQTT an die IP Adresse des Brokers gesendet hat. Da ich mittlerweile jedoch auch ein Anwender der ESPHome Integration in meiner HomeAssistant Umgebung bin, habe ich den ESP mit einem ESPHome Basisimage geflasht. Auf GitHub gibt es das Repository von Andre-Schuiki, auf dem er eine Version für ISKRA und SIEMENS Smartmeter für die Verwendung mit ESPHome veröffentlicht. Unter folgendem Link ist die Anleitung zur Installation zu finden: https://github.com/Andre-Schuiki/esphome_im350/tree/main/esp_home
Das Script für das ESPHome Graät sieht bei mir folgendermassen aus:
esphome:
name: kelagsmartmeter
friendly_name: KelagSmartmeter
libraries:
- "Crypto" # !IMPORTANT! we need this library for decryption!
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "da kommt der key rein des neu angelegten ESPHome Gerätes rein"
ota:
password: "das automatisch generierte ota passwort"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Kelagsmartmeter Fallback Hotspot"
password: "das automatisch generierte password"
captive_portal:
external_components:
- source:
type: local
path: custom_esphome
sensor:
- platform: siemens_im350
update_interval: 5s
trigger_pin: 26 # this pin goes to pin 2 of the customer interface and will be set to high before we try to read the data from the rx pin
rx_pin: 16 # this pin goes to pin 5 of the customer interface
tx_pin: 17 # not connected at the moment, i added it just in case we need it in the future..
decryption_key: "00AA01BB02CC03DD04EE05FF06AA07BB" # you get the key from your provider!
use_test_data: false # that was just for debugging, if you set it to true data are not read from serial and the test_data string is used
test_data: "7EA077CF022313BB45E6E700DB0849534B697460B6FA5F200005C8606F536D06C32A190761E80A97E895CECA358D0A0EFD7E9C47A005C0F65B810D37FB0DA2AD6AB95F7F372F2AB11560E2971B914A5F8BFF5E06D3AEFBCD95B244A373C5DBDA78592ED2C1731488D50C0EC295E9056B306F4394CDA7D0FC7E0000"
delay_before_reading_data: 1000 # this is needed because we have to wait for the interface to power up, you can try to lower this value but 1 sec was ok for me
max_wait_time_for_reading_data: 1100 # maximum time to read the 123 Bytes (just in case we get no data)
ntp_server: "pool.ntp.org" #if no ntp is specified pool.ntp.org is used
ntp_gmt_offset: 3600
ntp_daylight_offset: 3600
counter_reading_p_in:
name: reading_p_in
filters:
- lambda: return x / 1000;
unit_of_measurement: kWh
accuracy_decimals: 3
device_class: energy
counter_reading_p_out:
name: reading_p_out
filters:
- lambda: return x / 1000;
unit_of_measurement: kWh
accuracy_decimals: 3
device_class: energy
counter_reading_q_in:
name: reading_q_in
filters:
- lambda: return x / 1000;
unit_of_measurement: kvarh
device_class: energy
counter_reading_q_out:
name: reading_q_out
filters:
- lambda: return x / 1000;
unit_of_measurement: kvarh
device_class: energy
current_power_usage_in:
name: power_usage_in
filters:
- lambda: return x / 1000;
unit_of_measurement: kW
accuracy_decimals: 3
device_class: energy
current_power_usage_out:
name: power_usage_out
filters:
- lambda: return x / 1000;
unit_of_measurement: kW
accuracy_decimals: 3
device_class: energy
# Extra sensor to keep track of uptime
- platform: uptime
name: IM350_Uptime Sensor
switch:
- platform: restart
name: IM350_Restart
Seit ich mich mit Home Automatisierungen beschäftige soll natürlich so viel wie möglich optimiert, vereinfacht und unter den Aspekten der neuen Schlagworte „Green Electronics“, „Nachhaltigkeit“, „Energiesparend“ … usw. angepasst und realisiert werden. So schalten bei mir Geräte bei Nichtbenutzung oder Nichtbeachtung ab, Stand-by Energieverbrauch wird weitgehend vermieden und auch die menschliche Vergesslichkeit (Fenster offen gelassen im Winter, oder vergessen Licht aus zu schalten) verhindert die IOT – Technologie. Wie die Leser des Blogs mittlerweile schon wissen habe ich hier Systeme wie HomeMatic, NodeRed und seit einiger Zeit Homeassistant mit ESPHome, Zigbee2Mqtt usw. in Verwendung. Das Ziel ist natürlich auch, alles Systeme Cloudfrei zu halten. Ich will nicht, dass die Daten den Umweg über irgendwelche Server in Fernost nehmen um bei mir ein Licht ein und aus zu schalten. Also soll möglichst alles im Kreis meines eigenen Netzwerks stattfinden und nicht nach außen „telefonieren“ und auch funktionieren wenn ich die Datenleitung kappe.
Bei diversen Lieferanten gibts es seit langem, ein, für die Bequemlichkeit im elterlichen Ruheraum, äußert praktisches Gerät. Ich spreche da von einer platzsparenden Möglichkeit, die Flimmerkiste (heute auch Flat-TV genannt) im Raum unterzubringen. Ich nenne hier nur Bezeichnungen wie:
Speaka Professional TV-Deckenhalterung elektrisch motorisiert (1439178) oder MyWall HL46ML … etc. Manche von diesen Gräten sind mit einer Funkfernbedienung steuerbar, andere wiederum über die CloudApp von Tuya. Man kann die Tuya App zwar über die Tuya IOT Entwicklungsumgebung umgehen und diese Geräte über die Integration „TuyaLocal“ in seinen Homeassistant bringen – geht zwar – ist aber eher eine „NUR“- Lösung. Die ideale Lösung ist aus meiner Sicht, die Integration dieser Geräte ins ESPHome System. Am Beispiel der Speaka Professional TV Deckenhalterung zeige ich, wie diese mit einer kleinen Erweiterung ins ESPHome Netz und somit im Homeassistant integriert werden kann. Diese Ausführung des SpeaKa Teils hat keine Internet Anbindung und wird nur über eine Funkfernbedienung gesteuert.
TV Deckenhalter mit geöffneter Abdeckung
Mit ein wenig reverse Engineering haben wir (Kollege Werner und meiner einer) das bestehende Gerätewerk analysiert. Das System ist in etwa so aufgebaut:
Platine in der Deckenhalterung
Systemdiagramm
Das Systemdiagramm oben zeigt wie die Platine aufgebaut ist. Die Stromversorgung kommt von einem Steckernetzteil mit DC 24V Ausgang bei 1,5A. Auf der Platine erkennt man noch einen unbestückten Bereich, dessen Lötpads mit +3V3, GND und RX, TX Leitungen passend für einen ESP8266 beschaltet sind. Ebenso ist eine USB Buchse zu erkennen. Diese beiden Schnittstellen sind im Diagramm nicht berücksichtigt. Untersucht haben wir die RX/TX Leitungen, die von den unbestückten Lötpads (ESP8266) zum Microcontroller (1301 X 016B) geroutet sind. Doch hier waren keinerlei Signale zu messen. (Vermutlich ist die Schnittstelle in der geflashten Programmversion nicht aktiviert).
„Debug“ Drähte an den RX/TX und am RF-Chip
Dieser Weg bringt uns also nicht weiter. Im nächsten Schritt haben wir uns angesehen wo die Steuersignale der Funkfernbedienung herkommen, bzw. wie sie in weiterer Folge umgesetzt werden. Der RF-Empfänger Chip hat 16 Pins und leider keinerlei Beschriftung. Oder wurde sie entfernt. Die Versorgungsspannung des RF-Chips liegt an Pin1 und Pin16 an, Pin2 und Pin3 ist mit einem Quarz beschaltet und von Pin9 ist eine Leitung zum Microcontroller geroutet. Das muss also der Datenausgang sein. Mit Hilfe der Software „PulseView“ von Sigrok und einem Fernost Logicanalyzer haben wir diesen Ausgang mitgesnifft. Und siehe da, hier offenbarten sich Datenpakete mit einer Dauer von 10.3ms. Die Software PulseView konnte das Protokoll nach einigen Versuchen mit unterschiedlichen analysierten Datenraten als RS232 Protokoll erkennen. So war es dann ein leichtes die empfangenen und dekodierten Steuerbefehle zum Microcontroller zu protokollieren.
RF-Chip mit angeschlossener „Sniffer“ Leitung
Die Baudrate des RS232 Ports am RF-Chip Ausgangs beträgt 9600 bei 8N1. Es werden bei jedem gesendeten Befehl 10 Bytes in HEX empfangen. Hier die Liste der Kommandos: (fehlende Byte folgen…)
Befehl
Byte0
Byte1
Byte2
Byte3
Byte4
Byte5
Byte6
Byte7
Byte8
Byte9
UP
0xAA
0x06
0x04
0x25
0x03
0xD5
0x01
0x00
0x02
0x55
DOWN
0xAA
0x06
0x04
0x25
0x03
0xD5
0x00
0x10
0x11
0x55
LEFT
0xAA
0x06
0x04
0x25
0x03
0xD5
0x55
RIGHT
0xAA
0x06
0x04
0x25
0x03
0xD5
0x55
BUTTON1
0xAA
0x06
0x04
0x25
0x03
0xD5
0x55
BUTTON2
0xAA
0x06
0x04
0x25
0x03
0xD5
0x00
0x08
0x09
0x55
MEM1
0xAA
0x06
0x04
0x25
0x03
0xD5
0x55
MEM2
0xAA
0x06
0x04
0x25
0x03
0xD5
0x55
OK
0xAA
0x06
0x04
0x25
0x03
0xD5
0x00
0x40
0x41
0x55
SET
xx
xx
xx
xx
xx
xx
xx
xx
xx
xx
Nachdem mit dem Logikanalyzer das Datenprotokoll gefunden war, versuchten wir über ein Terminalprogramm und einen USB zu TTL232 Converter die Daten an den Microcontroller zu senden. Dazu wurde der RF-Chip entfernt. Er zog den Pegel im Ruhezustand auf VCC und verhinderte ein paralleles Betreiben der „RS232 Transmitter“.
RF-Chip entferntBoard ohne Chip mit Debugleitung
USB UART zum Senden der Befehle
Die Steuerbefehle aus oben dargestellter Tabelle konnten per Terminal Programm erfolgreich gesendet werden. Jetzt musste nur noch ein ESP32 Board diese Aufgabe übernehmen. Ein ESP32 NodeMCU Board aus dem Fundus wurde mit einem Basis ESPHome-Image bestückt und ins Homeassistant Netzwerk integriert. Dem ESPHome Knoten war jetzt nur noch beizubringen, über den TX Pin des ESP32 die Bytefolge bei entsprechendem Trigger im Homeassistant zu senden. Dazu wurde das ESP32 Board in auf der Platine befestigt und die VCC3V3, GND und TX Leitung zum PIN9 des ehemaligen RF Chip gelötet.
ESP32 am Board des Speaka Deckenhalters
Wieder im Deckenhalter eingebaut
In der ESPHome Webumgebung ist nun das folgende yaml Script hinzuzufügen.
esphome:
name: tvhalterung
friendly_name: TVHalterung
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "hier dein key beim Anlegen des device"
ota:
password: "hier dein ota password"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Tvhalterung Fallback Hotspot"
password: "hier wieder deins"
captive_portal:
uart:
tx_pin: 4
rx_pin: 5
baud_rate: 9600
# Example button configuration
button:
- platform: template
name: TV Halterung UP
id: tv_up
icon: "mdi:arrow-up-bold-outline"
on_press:
- logger.log: "Button pressed TV Up"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x01,0x00,0x02,0x55]
- platform: template
name: TV Halterung OK
id: tv_ok
icon: "mdi:stop-circle-outline"
on_press:
- logger.log: "Button pressed TV OK"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x40,0x41,0x55]
- platform: template
name: TV Halterung DOWN
id: tv_down
icon: "mdi:arrow-down-bold-outline"
on_press:
- logger.log: "Button pressed TV Down"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x10,0x11,0x55]
- platform: template
name: TV Halterung Button1
id: tv_button1
icon: "mdi:numeric-1-circle-outline"
on_press:
- logger.log: "Button pressed TV Button1"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]
- platform: template
name: TV Halterung Button2
id: tv_button2
icon: "mdi:numeric-2-circle-outline"
on_press:
- logger.log: "Button pressed TV Button2"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x08,0x09,0x55]
- platform: template
name: TV Halterung Left
id: tv_left
icon: "mdi:arrow-left-bold-outline"
on_press:
- logger.log: "Button pressed TV Left"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]
- platform: template
name: TV Halterung Right
id: tv_right
icon: "mdi:arrow-right-bold-outline"
on_press:
- logger.log: "Button pressed TV Right"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]
- platform: template
name: TV Halterung MEM1
id: tv_mem1
icon: "mdi:alpha-m-circle-outline"
on_press:
- logger.log: "Button pressed TV MEM1"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x01,0x02,0x55]
- platform: template
name: TV Halterung MEM2
id: tv_mem2
icon: "mdi:alpha-m-circle-outline"
on_press:
- logger.log: "Button pressed TV MEM2"
- uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x01,0x02,0x55]
Ist das yaml Script dann kompiliert und zum ESP hochgeladen, gibt es ein neues ESPHome Device mit dem Namen TV-Halter in der Homeassistant Umgebung. Hier sind nun die Tasten für die Steuerung als Entitäten gelistet. Hat alles gekplappt, sollte sich die TV-Halterung über den Homeassistant jetzt steuern lassen.
(Es sind noch nicht alles Steuerkommandos richtig implementiert – die korrekten Codes werden in der Tabelle noch ergänzt)
Update 26.11.2024:
Aufgrund vieler Anfragen nach der Platine und weil vielleicht nicht jeder daran interessiert ist, sie selber zu zeichnen, stelle ich die Gerberdaten zum Dowload zur Verfügung:
Wer eine Photovoltaik Anlage in seinem Eigenheim aufgebaut hat, verwendet vielleicht sogar einen Energiespeicher. In diesem Beispiel handelt es sich um eine Offgrid Anlage, die mit zwei Modulen des Herstellers Pylontech ausgestattet ist. Die Pylontech Akkus der Type US3000C haben eine Ausgangs Spannung von 48V. Die Nennkapazität beträgt 3500Wh. Die verbauten Zellen sind LiFePO4 Zellen und die nutzbare Kapazität ist laut Datenblatt mit 3374Wh angegeben. Die Akkus sind so gebaut, dass sie mit weiteren Pylontech-Akkus parallelgeschaltet werden können. Das intern verbaute BMS (BatterieManagementSystem) kommuniziert über eine sogenannte „Link“ Schnittstelle mit den anderen Pylontech Batteriemodulen. Ein als „Master“ konfigurierter Akku erledigt den Datenaustausch zum Wechselrichter. Hier stellt Pylontech den CAN- oder RS485 Bus als Schnittstelle zur Verfügung. Will man jedoch Informationen über die einzelnen Zellen (Spannungen, Ströme, Ladungen, Temperaturen etc.) haben, so gibt es an jedem Modul noch eine Schnittstelle mit der Bezeichnung „Console“. Das ist eine RS232 Schnittstelle über die man direkt mit dem BMS des Akkus kommunizieren kann. Dieser Port wird auch genutzt, um die Firmware des BMS zu aktualisieren. Ich rate aber DRINGEND davon ab, mit Firmwareupdates und Flashsoftware daran herum zu spielen. Das ist dem Hersteller oder dem Haftungsträger vorbehalten.
Da man über diese Schnittstelle aber auch einiges an Informationen über die im Akku verbauten Zellen erfährt, ist das ein interessanter Zugang. So hatte ich anfangs einen Laptop mit einem Terminal angeschlossen und konnte so die einzelnen Zellenspannungen und vor allem den evtl. unterschiedlichen Ladezustand der parallel geschalteten Module entdecken und monitoren. So dachte ich mir, wäre es doch eine gute Idee, wenn man diese Infos in seiner Hausautomatisierung zur Verfügung hat und dort visualisieren und für Steuerungszwecke nutzen kann.
Da für uns Freaks und Technikinteressierte Begriffe wie Homeassistant, Docker, Proxmox, HomeMatic, NodeRed usw. durchaus bekannt sind, dachte ich mir, diese Daten sollen auch im Homeassistant zu Entitäten werden. So war schnell wieder ein kleines neues Projekt geschaffen. Mein Plan: die Daten der Serien Schnittstelle auslesen und per MQTT an den Homeassistant senden.
Bevor ich mich nun aber mit dem Zerlegen der Datenstrings, die über den seriellen Port herauspurzeln beschäftige, bemühte ich zuerst einmal die Suchmaschinen. Vielleicht hat sich ja schon jemand anderes mit diesem Thema beschäftigt. Und genau so war es auch. Auf GitHub wurde ich unter dem Begriff „pylontec2mqtt“ fündig. Unter https://github.com/irekzielinski/Pylontech-Battery-Monitoring ist ein Projekt gehostet, das mittels ESP8266 die seriellen Daten vom Port abholt und per MQTT und Wifi zum Homeassistant Server sendet. Ein Fork mit einer Weiterentwicklung dieses Projektes ist unter https://github.com/hidaba/PylontechMonitoring zu finden.
Warum ich das Projekt trotz des einfachen Nachbaus hier im Blog veröffentliche? Ich habe die Schaltung ein wenig optimiert und in ein Layout gepackt und den Code etwas angepasst. Das Ergebnis möchte ich hier teilen. Mir war es in wichtig, einen vernünftigen Aufbau auf einer Platine zu haben, die mit einem USB A-B Kabel für die Spannungsversorgung und einem LAN-RJ45 Kabel für die Datenverbindung angesteckt wird. Dabei wollte ich eine „solide“ USB Steckverbindung verwenden (nicht die Fragilen Mini- oder Mikro USB Steckverbindungen)
Auf einer Lochrasterplatine und mit den üblichen Entwicklungsplatinen habe ich mir auf die Schnelle ein Funktionsmuster „zusammen gestrickt“ um darauf die Software anpassen zu können.
Funktionsmuster auf Lochraster
So habe ich zuerst aus den Skizzen im Git-Projekt einen Schaltplan erstellt. An der „Console“ Schnittstelle liegt ein „echter“ RS232 Pegel an, der über den MAX3232 IC auf eine 5V TTL umgesetzt wird. Mit dem BSS123 FET wird für die Signale RX und TX je ein Pegelwandler auf 3.3V realisiert.
pylontec2mqtt schematic
Diese 3.3V TTL Pegel verarbeitet der ESP8266 in Form des Wemos D1Mini oder WemosD1Pro Entwicklungsboard, das auf die Platine aufgesteckt wird. Die gesamte Konstruktion habe ich dann in ein kleines Kunststoffgehäuse gepackt, das bequem über die Lan und USB Kabel mit dem Pylontec und einer USB Spannungsquelle zu verbinden ist.
Layout im Designtool
Im Bild oben ist der Layout Entwurf dargestellt. Die Platine und die Lage der Bauteile wurden vor der Fertigung nochmals mit der Vorschau überprüft und dann beim Hersteller des Vertrauens bestellt.
Vorschau der Platine vor der Fertigung
Nach kaum zwei Wochen Wartezeit hielt ich dann die Leeren Platinen in Händen und konnte sie mit den Bauteilen bestücken.
fertig bestückte Platine
Im Bild oben ist die fertig bestückte Platine zu sehen. Hier fehlt nur mehr das Wemosboard mit dem ESP.
Vergleich zwischen Funktionsmuster und erstem „Fertigungsmodell“
Schlussendlich habe ich einen WemosD1 Pro aufgesteckt, da dieser die Möglichkeit bietet, eine externe WiFi Antenne anzuschließen und somit eine vernünftige Funkreichweite zu erhalten.
Nach dem Aufspielen der Software und der Inbetriebnahme ist der Webserver des Wemos unter der im Code angegebenen IP Adresse erreichbar. Hier kann auch gleich geprüft werden, ob der Pylontech Akku mit dem Wemos kommuniziert. Das Ergebnis sieht dann wie folgt aus.
Webseite des WEMOS
Hier ist zu erkennen, dass beide Akkumodule korrekt erkannt werden. Im nächsten Schritt wird überprüft, ob über das MQTT Protokol Nachrichten versandt werden. Die IP Adresse des MQTT Brokers ist auch im Code des Wemos festzulegen. Ich habe in meinem Aufbau den MQTT-Explorer im Homeassistant eingerichtet, um schnell und einfach die MQTT Funktionen prüfen zu können.
MQTT Explorer
Im Bild oben ist zu erkennen, dass auch die Daten über MQTT korrekt ankommen. Jetzt ist es nur mehr notwendig, im Homeassistant ein Sensor yaml file anzulegen, um die topics im als Entitäten zur Verfügung zu stellen. Dazu habe ich im configuration.yaml folgenden code hinzugefügt:
Auf der Homeassistant Website könnte die Visualisierung dann beispielsweise so aussehen:
Zu guter Letzt poste ich unten noch den angepassten Code. Die zum Kompilieren notwendigen Libraries und weiteren Infos sind den oben angeführten GitHub Links zu entnehmen.
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include <ESP8266WebServer.h>
#include <circular_log.h>
#include <ArduinoJson.h>
#include <NTPClient.h>
#include <ESP8266TimerInterrupt.h>
//+++ START CONFIGURATION +++
//IMPORTANT: Specify your WIFI settings:
#define WIFI_SSID "wifiname"
#define WIFI_PASS deinpasswort1234"
#define WIFI_HOSTNAME "mppsolar-pylontec"
//Uncomment for static ip configuration
#define STATIC_IP
IPAddress local_IP(192, 168, xxx, yyy);
IPAddress subnet(255, 255, 255, 0);
IPAddress gateway(192, 168, xxx, zzz);
IPAddress primaryDNS(192, 168, xxx, zzz);
//Uncomment for authentication page
//#define AUTHENTICATION
//set http Authentication
const char* www_username = "admin";
const char* www_password = "password";
//IMPORTANT: Uncomment this line if you want to enable MQTT (and fill correct MQTT_ values below):
#define ENABLE_MQTT
// Set offset time in seconds to adjust for your timezone, for example:
// GMT +1 = 3600
// GMT +1 = 7200
// GMT +8 = 28800
// GMT -1 = -3600
// GMT 0 = 0
#define GMT 3600
//NOTE 1: if you want to change what is pushed via MQTT - edit function: pushBatteryDataToMqtt.
//NOTE 2: MQTT_TOPIC_ROOT is where battery will push MQTT topics. For example "soc" will be pushed to: "home/grid_battery/soc"
#define MQTT_SERVER "192.168.xx.broker"
#define MQTT_PORT 1883
#define MQTT_USER ""
#define MQTT_PASSWORD ""
#define MQTT_TOPIC_ROOT "ingmarsretro/pylontec/" //this is where mqtt data will be pushed
#define MQTT_PUSH_FREQ_SEC 2 //maximum mqtt update frequency in seconds
//+++ END CONFIGURATION +++
#ifdef ENABLE_MQTT
#include <PubSubClient.h>
WiFiClient espClient;
PubSubClient mqttClient(espClient);
#endif //ENABLE_MQTT
//text response
char g_szRecvBuff[7000];
const long utcOffsetInSeconds = GMT;
char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds);
ESP8266WebServer server(80);
circular_log<7000> g_log;
bool ntpTimeReceived = false;
int g_baudRate = 0;
void Log(const char* msg)
{
g_log.Log(msg);
}
//Define Interrupt Timer to Calculate Power meter every second (kWh)
#define USING_TIM_DIV1 true // for shortest and most accurate timer
ESP8266Timer ITimer;
bool setInterval(unsigned long interval, timer_callback callback); // interval (in microseconds)
#define TIMER_INTERVAL_MS 1000
//Global Variables for the Power Meter - accessible from the calculating interrupt und from main
unsigned long powerIN = 0; //WS gone in to the BAttery
unsigned long powerOUT = 0; //WS gone out of the Battery
//Global Variables for the Power Meter - Überlauf
unsigned long powerINWh = 0; //WS gone in to the BAttery
unsigned long powerOUTWh = 0; //WS gone out of the Battery
void setup() {
memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff)); //clean variable
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);//high is off
// put your setup code here, to run once:
WiFi.mode(WIFI_STA);
WiFi.persistent(false); //our credentialss are hardcoded, so we don't need ESP saving those each boot (will save on flash wear)
WiFi.hostname(WIFI_HOSTNAME);
#ifdef STATIC_IP
WiFi.config(local_IP, gateway, subnet, primaryDNS);
#endif
WiFi.begin(WIFI_SSID, WIFI_PASS);
for(int ix=0; ix<10; ix++)
{
Log("Wait for WIFI Connection");
if(WiFi.status() == WL_CONNECTED)
{
break;
}
delay(1000);
}
ArduinoOTA.setHostname(WIFI_HOSTNAME);
ArduinoOTA.begin();
server.on("/", handleRoot);
server.on("/log", handleLog);
server.on("/req", handleReq);
server.on("/jsonOut", handleJsonOut);
server.on("/reboot", [](){
#ifdef AUTHENTICATION
if (!server.authenticate(www_username, www_password)) {
return server.requestAuthentication();
}
#endif
ESP.restart();
});
server.begin();
timeClient.begin();
#ifdef ENABLE_MQTT
mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
#endif
Log("Boot event");
}
void handleLog()
{
#ifdef AUTHENTICATION
if (!server.authenticate(www_username, www_password)) {
return server.requestAuthentication();
}
#endif
server.send(200, "text/html", g_log.c_str());
}
void switchBaud(int newRate)
{
if(g_baudRate == newRate)
{
return;
}
if(g_baudRate != 0)
{
Serial.flush();
delay(20);
Serial.end();
delay(20);
}
char szMsg[50];
snprintf(szMsg, sizeof(szMsg)-1, "New baud: %d", newRate);
Log(szMsg);
Serial.begin(newRate);
g_baudRate = newRate;
delay(20);
}
void waitForSerial()
{
for(int ix=0; ix<150;ix++)
{
if(Serial.available()) break;
delay(10);
}
}
int readFromSerial()
{
memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));
int recvBuffLen = 0;
bool foundTerminator = true;
waitForSerial();
while(Serial.available())
{
char szResponse[256] = "";
const int readNow = Serial.readBytesUntil('>', szResponse, sizeof(szResponse)-1); //all commands terminate with "$$\r\n\rpylon>" (no new line at the end)
if(readNow > 0 &&
szResponse[0] != '\0')
{
if(readNow + recvBuffLen + 1 >= (int)(sizeof(g_szRecvBuff)))
{
Log("WARNING: Read too much data on the console!");
break;
}
strcat(g_szRecvBuff, szResponse);
recvBuffLen += readNow;
if(strstr(g_szRecvBuff, "$$\r\n\rpylon"))
{
strcat(g_szRecvBuff, ">"); //readBytesUntil will skip this, so re-add
foundTerminator = true;
break; //found end of the string
}
if(strstr(g_szRecvBuff, "Press [Enter] to be continued,other key to exit"))
{
//we need to send new line character so battery continues the output
Serial.write("\r");
}
waitForSerial();
}
}
if(recvBuffLen > 0 )
{
if(foundTerminator == false)
{
Log("Failed to find pylon> terminator");
}
}
return recvBuffLen;
}
bool readFromSerialAndSendResponse()
{
const int recvBuffLen = readFromSerial();
if(recvBuffLen > 0)
{
server.sendContent(g_szRecvBuff);
return true;
}
return false;
}
bool sendCommandAndReadSerialResponse(const char* pszCommand)
{
switchBaud(115200);
if(pszCommand[0] != '\0')
{
Serial.write(pszCommand);
}
Serial.write("\n");
const int recvBuffLen = readFromSerial();
if(recvBuffLen > 0)
{
return true;
}
//wake up console and try again:
wakeUpConsole();
if(pszCommand[0] != '\0')
{
Serial.write(pszCommand);
}
Serial.write("\n");
return readFromSerial() > 0;
}
void handleReq()
{
#ifdef AUTHENTICATION
if (!server.authenticate(www_username, www_password)) {
return server.requestAuthentication();
}
#endif
bool respOK;
if(server.hasArg("code") == false)
{
respOK = sendCommandAndReadSerialResponse("");
}
else
{
respOK = sendCommandAndReadSerialResponse(server.arg("code").c_str());
}
handleRoot();
}
void handleJsonOut()
{
#ifdef AUTHENTICATION
if (!server.authenticate(www_username, www_password)) {
return server.requestAuthentication();
}
#endif
if(sendCommandAndReadSerialResponse("pwr") == false)
{
server.send(500, "text/plain", "Failed to get response to 'pwr' command");
return;
}
parsePwrResponse(g_szRecvBuff);
prepareJsonOutput(g_szRecvBuff, sizeof(g_szRecvBuff));
server.send(200, "application/json", g_szRecvBuff);
}
void handleRoot() {
#ifdef AUTHENTICATION
if (!server.authenticate(www_username, www_password)) {
return server.requestAuthentication();
}
#endif
timeClient.update(); //get ntp datetime
unsigned long days = 0, hours = 0, minutes = 0;
unsigned long val = os_getCurrentTimeSec();
days = val / (3600*24);
val -= days * (3600*24);
hours = val / 3600;
val -= hours * 3600;
minutes = val / 60;
val -= minutes*60;
time_t epochTime = timeClient.getEpochTime();
String formattedTime = timeClient.getFormattedTime();
//Get a time structure
struct tm *ptm = gmtime ((time_t *)&epochTime);
int currentMonth = ptm->tm_mon+1;
static char szTmp[9500] = "";
long timezone= GMT / 3600;
snprintf(szTmp, sizeof(szTmp)-1, "<html><b>Pylontech Battery</b><br>Time GMT: %s (%s %d)<br>Uptime: %02d:%02d:%02d.%02d<br><br>free heap: %u<br>Wifi RSSI: %d<BR>Wifi SSID: %s",
formattedTime, "GMT ", timezone,
(int)days, (int)hours, (int)minutes, (int)val,
ESP.getFreeHeap(), WiFi.RSSI(), WiFi.SSID().c_str());
strncat(szTmp, "<BR><a href='/log'>Runtime log</a><HR>", sizeof(szTmp)-1);
strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'> <a href='/req?code=pwr'>PWR</a> | <a href='/req?code=pwr%201'>Power 1</a> | <a href='/req?code=pwr%202'>Power 2</a> | <a href='/req?code=pwr%203'>Power 3</a> | <a href='/req?code=pwr%204'>Power 4</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);
//strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'><a href='/req?code=pwr'>Power</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);
strncat(szTmp, "<textarea rows='80' cols='180'>", sizeof(szTmp)-1);
//strncat(szTmp, "<textarea rows='45' cols='180'>", sizeof(szTmp)-1);
strncat(szTmp, g_szRecvBuff, sizeof(szTmp)-1);
strncat(szTmp, "</textarea></form>", sizeof(szTmp)-1);
strncat(szTmp, "</html>", sizeof(szTmp)-1);
//send page
server.send(200, "text/html", szTmp);
}
unsigned long os_getCurrentTimeSec()
{
static unsigned int wrapCnt = 0;
static unsigned long lastVal = 0;
unsigned long currentVal = millis();
if(currentVal < lastVal)
{
wrapCnt++;
}
lastVal = currentVal;
unsigned long seconds = currentVal/1000;
//millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter
return (wrapCnt*4294967) + seconds;
}
void wakeUpConsole()
{
switchBaud(1200);
//byte wakeUpBuff[] = {0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x38, 0x32, 0x43, 0x30, 0x30, 0x34, 0x38, 0x35, 0x32, 0x30, 0x46, 0x43, 0x43, 0x33, 0x0D};
//Serial.write(wakeUpBuff, sizeof(wakeUpBuff));
Serial.write("~20014682C0048520FCC3\r");
delay(1000);
byte newLineBuff[] = {0x0E, 0x0A};
switchBaud(115200);
for(int ix=0; ix<10; ix++)
{
Serial.write(newLineBuff, sizeof(newLineBuff));
delay(1000);
if(Serial.available())
{
while(Serial.available())
{
Serial.read();
}
break;
}
}
}
#define MAX_PYLON_BATTERIES 8
struct pylonBattery
{
bool isPresent;
long soc; //Coulomb in %
long voltage; //in mW
long current; //in mA, negative value is discharge
long tempr; //temp of case or BMS?
long cellTempLow;
long cellTempHigh;
long cellVoltLow;
long cellVoltHigh;
char baseState[9]; //Charge | Dischg | Idle
char voltageState[9]; //Normal
char currentState[9]; //Normal
char tempState[9]; //Normal
char time[20]; //2019-06-08 04:00:29
char b_v_st[9]; //Normal (battery voltage?)
char b_t_st[9]; //Normal (battery temperature?)
bool isCharging() const { return strcmp(baseState, "Charge") == 0; }
bool isDischarging() const { return strcmp(baseState, "Dischg") == 0; }
bool isIdle() const { return strcmp(baseState, "Idle") == 0; }
bool isBalancing() const { return strcmp(baseState, "Balance") == 0; }
bool isNormal() const
{
if(isCharging() == false &&
isDischarging() == false &&
isIdle() == false &&
isBalancing() == false)
{
return false; //base state looks wrong!
}
return strcmp(voltageState, "Normal") == 0 &&
strcmp(currentState, "Normal") == 0 &&
strcmp(tempState, "Normal") == 0 &&
strcmp(b_v_st, "Normal") == 0 &&
strcmp(b_t_st, "Normal") == 0 ;
}
};
struct batteryStack
{
int batteryCount;
int soc; //in %, if charging: average SOC, otherwise: lowest SOC
int temp; //in mC, if highest temp is > 15C, this will show the highest temp, otherwise the lowest
long currentDC; //mAh current going in or out of the battery
long avgVoltage; //in mV
char baseState[9]; //Charge | Dischg | Idle | Balance | Alarm!
pylonBattery batts[MAX_PYLON_BATTERIES];
bool isNormal() const
{
for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)
{
if(batts[ix].isPresent &&
batts[ix].isNormal() == false)
{
return false;
}
}
return true;
}
//in Wh
long getPowerDC() const
{
return (long)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));
}
// power in Wh in charge
float powerIN() const
{
if (currentDC > 0) {
return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));
} else {
return (float)(0);
}
}
// power in Wh in discharge
float powerOUT() const
{
if (currentDC < 0) {
return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0)*-1);
} else {
return (float)(0);
}
}
//Wh estimated current on AC side (taking into account Sofar ME3000SP losses)
long getEstPowerAc() const
{
double powerDC = (double)getPowerDC();
if(powerDC == 0)
{
return 0;
}
else if(powerDC < 0)
{
//we are discharging, on AC side we will see less power due to losses
if(powerDC < -1000)
{
return (long)(powerDC*0.94);
}
else if(powerDC < -600)
{
return (long)(powerDC*0.90);
}
else
{
return (long)(powerDC*0.87);
}
}
else
{
//we are charging, on AC side we will have more power due to losses
if(powerDC > 1000)
{
return (long)(powerDC*1.06);
}
else if(powerDC > 600)
{
return (long)(powerDC*1.1);
}
else
{
return (long)(powerDC*1.13);
}
}
}
};
batteryStack g_stack;
long extractInt(const char* pStr, int pos)
{
return atol(pStr+pos);
}
void extractStr(const char* pStr, int pos, char* strOut, int strOutSize)
{
strOut[strOutSize-1] = '\0';
strncpy(strOut, pStr+pos, strOutSize-1);
strOutSize--;
//trim right
while(strOutSize > 0)
{
if(isspace(strOut[strOutSize-1]))
{
strOut[strOutSize-1] = '\0';
}
else
{
break;
}
strOutSize--;
}
}
/* Output has mixed \r and \r\n
pwr
@
Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St
1 49735 -1440 22000 19000 19000 3315 3317 Dischg Normal Normal Normal 93% 2019-06-08 04:00:30 Normal Normal
....
8 - - - - - - - Absent - - - - - - -
Command completed successfully
$$
pylon
*/
bool parsePwrResponse(const char* pStr)
{
if(strstr(pStr, "Command completed successfully") == NULL)
{
return false;
}
int chargeCnt = 0;
int dischargeCnt = 0;
int idleCnt = 0;
int alarmCnt = 0;
int socAvg = 0;
int socLow = 0;
int tempHigh = 0;
int tempLow = 0;
memset(&g_stack, 0, sizeof(g_stack));
for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)
{
char szToFind[32] = "";
snprintf(szToFind, sizeof(szToFind)-1, "\r\r\n%d ", ix+1);
const char* pLineStart = strstr(pStr, szToFind);
if(pLineStart == NULL)
{
return false;
}
pLineStart += 3; //move past \r\r\n
extractStr(pLineStart, 55, g_stack.batts[ix].baseState, sizeof(g_stack.batts[ix].baseState));
if(strcmp(g_stack.batts[ix].baseState, "Absent") == 0)
{
g_stack.batts[ix].isPresent = false;
}
else
{
g_stack.batts[ix].isPresent = true;
extractStr(pLineStart, 64, g_stack.batts[ix].voltageState, sizeof(g_stack.batts[ix].voltageState));
extractStr(pLineStart, 73, g_stack.batts[ix].currentState, sizeof(g_stack.batts[ix].currentState));
extractStr(pLineStart, 82, g_stack.batts[ix].tempState, sizeof(g_stack.batts[ix].tempState));
extractStr(pLineStart, 100, g_stack.batts[ix].time, sizeof(g_stack.batts[ix].time));
extractStr(pLineStart, 121, g_stack.batts[ix].b_v_st, sizeof(g_stack.batts[ix].b_v_st));
extractStr(pLineStart, 130, g_stack.batts[ix].b_t_st, sizeof(g_stack.batts[ix].b_t_st));
g_stack.batts[ix].voltage = extractInt(pLineStart, 6);
g_stack.batts[ix].current = extractInt(pLineStart, 13);
g_stack.batts[ix].tempr = extractInt(pLineStart, 20);
g_stack.batts[ix].cellTempLow = extractInt(pLineStart, 27);
g_stack.batts[ix].cellTempHigh = extractInt(pLineStart, 34);
g_stack.batts[ix].cellVoltLow = extractInt(pLineStart, 41);
g_stack.batts[ix].cellVoltHigh = extractInt(pLineStart, 48);
g_stack.batts[ix].soc = extractInt(pLineStart, 91);
//////////////////////////////// Post-process ////////////////////////
g_stack.batteryCount++;
g_stack.currentDC += g_stack.batts[ix].current;
g_stack.avgVoltage += g_stack.batts[ix].voltage;
socAvg += g_stack.batts[ix].soc;
if(g_stack.batts[ix].isNormal() == false){ alarmCnt++; }
else if(g_stack.batts[ix].isCharging()){chargeCnt++;}
else if(g_stack.batts[ix].isDischarging()){dischargeCnt++;}
else if(g_stack.batts[ix].isIdle()){idleCnt++;}
else{ alarmCnt++; } //should not really happen!
if(g_stack.batteryCount == 1)
{
socLow = g_stack.batts[ix].soc;
tempLow = g_stack.batts[ix].cellTempLow;
tempHigh = g_stack.batts[ix].cellTempHigh;
}
else
{
if(socLow > g_stack.batts[ix].soc){socLow = g_stack.batts[ix].soc;}
if(tempHigh < g_stack.batts[ix].cellTempHigh){tempHigh = g_stack.batts[ix].cellTempHigh;}
if(tempLow > g_stack.batts[ix].cellTempLow){tempLow = g_stack.batts[ix].cellTempLow;}
}
}
}
//now update stack state:
g_stack.avgVoltage /= g_stack.batteryCount;
g_stack.soc = socLow;
if(tempHigh > 15000) //15C
{
g_stack.temp = tempHigh; //in the summer we highlight the warmest cell
}
else
{
g_stack.temp = tempLow; //in the winter we focus on coldest cell
}
if(alarmCnt > 0)
{
strcpy(g_stack.baseState, "Alarm!");
}
else if(chargeCnt == g_stack.batteryCount)
{
strcpy(g_stack.baseState, "Charge");
g_stack.soc = (int)(socAvg / g_stack.batteryCount);
}
else if(dischargeCnt == g_stack.batteryCount)
{
strcpy(g_stack.baseState, "Dischg");
}
else if(idleCnt == g_stack.batteryCount)
{
strcpy(g_stack.baseState, "Idle");
}
else
{
strcpy(g_stack.baseState, "Balance");
}
return true;
}
void prepareJsonOutput(char* pBuff, int buffSize)
{
memset(pBuff, 0, buffSize);
snprintf(pBuff, buffSize-1, "{\"soc\": %d, \"temp\": %d, \"currentDC\": %ld, \"avgVoltage\": %ld, \"baseState\": \"%s\", \"batteryCount\": %d, \"powerDC\": %ld, \"estPowerAC\": %ld, \"isNormal\": %s}", g_stack.soc,
g_stack.temp,
g_stack.currentDC,
g_stack.avgVoltage,
g_stack.baseState,
g_stack.batteryCount,
g_stack.getPowerDC(),
g_stack.getEstPowerAc(),
g_stack.isNormal() ? "true" : "false");
}
void loop() {
#ifdef ENABLE_MQTT
mqttLoop();
#endif
ArduinoOTA.handle();
server.handleClient();
//if there are bytes availbe on serial here - it's unexpected
//when we send a command to battery, we read whole response
//if we get anything here anyways - we will log it
int bytesAv = Serial.available();
if(bytesAv > 0)
{
if(bytesAv > 63)
{
bytesAv = 63;
}
char buff[64+4] = "RCV:";
if(Serial.readBytes(buff+4, bytesAv) > 0)
{
digitalWrite(LED_BUILTIN, LOW);
delay(5);
digitalWrite(LED_BUILTIN, HIGH);//high is off
Log(buff);
}
}
}
#ifdef ENABLE_MQTT
#define ABS_DIFF(a, b) (a > b ? a-b : b-a)
void mqtt_publish_f(const char* topic, float newValue, float oldValue, float minDiff, bool force)
{
char szTmp[16] = "";
snprintf(szTmp, 15, "%.2f", newValue);
if(force || ABS_DIFF(newValue, oldValue) > minDiff)
{
mqttClient.publish(topic, szTmp, false);
}
}
void mqtt_publish_i(const char* topic, int newValue, int oldValue, int minDiff, bool force)
{
char szTmp[16] = "";
snprintf(szTmp, 15, "%d", newValue);
if(force || ABS_DIFF(newValue, oldValue) > minDiff)
{
mqttClient.publish(topic, szTmp, false);
}
}
void mqtt_publish_s(const char* topic, const char* newValue, const char* oldValue, bool force)
{
if(force || strcmp(newValue, oldValue) != 0)
{
mqttClient.publish(topic, newValue, false);
}
}
void pushBatteryDataToMqtt(const batteryStack& lastSentData, bool forceUpdate /* if true - we will send all data regardless if it's the same */)
{
mqtt_publish_f(MQTT_TOPIC_ROOT "soc", g_stack.soc, lastSentData.soc, 0, forceUpdate);
mqtt_publish_f(MQTT_TOPIC_ROOT "temp", (float)g_stack.temp/1000.0, (float)lastSentData.temp/1000.0, 0.1, forceUpdate);
mqtt_publish_i(MQTT_TOPIC_ROOT "currentDC", g_stack.currentDC, lastSentData.currentDC, 1, forceUpdate);
mqtt_publish_i(MQTT_TOPIC_ROOT "estPowerAC", g_stack.getEstPowerAc(), lastSentData.getEstPowerAc(), 10, forceUpdate);
mqtt_publish_i(MQTT_TOPIC_ROOT "battery_count",g_stack.batteryCount, lastSentData.batteryCount, 0, forceUpdate);
mqtt_publish_s(MQTT_TOPIC_ROOT "base_state", g_stack.baseState, lastSentData.baseState , forceUpdate);
mqtt_publish_i(MQTT_TOPIC_ROOT "is_normal", g_stack.isNormal() ? 1:0, lastSentData.isNormal() ? 1:0, 0, forceUpdate);
mqtt_publish_i(MQTT_TOPIC_ROOT "getPowerDC", g_stack.getPowerDC(), lastSentData.getPowerDC(), 1, forceUpdate);
mqtt_publish_i(MQTT_TOPIC_ROOT "powerIN", g_stack.powerIN(), lastSentData.powerIN(), 1, forceUpdate);
mqtt_publish_i(MQTT_TOPIC_ROOT "powerOUT", g_stack.powerOUT(), lastSentData.powerOUT(), 1, forceUpdate);
// publishing details
for (int ix = 0; ix < g_stack.batteryCount; ix++) {
char ixBuff[50];
String ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/voltage";
ixBattStr.toCharArray(ixBuff, 50);
mqtt_publish_f(ixBuff, g_stack.batts[ix].voltage / 1000.0, lastSentData.batts[ix].voltage / 1000.0, 0, forceUpdate);
ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/current";
ixBattStr.toCharArray(ixBuff, 50);
mqtt_publish_f(ixBuff, g_stack.batts[ix].current / 1000.0, lastSentData.batts[ix].current / 1000.0, 0, forceUpdate);
ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/soc";
ixBattStr.toCharArray(ixBuff, 50);
mqtt_publish_i(ixBuff, g_stack.batts[ix].soc, lastSentData.batts[ix].soc, 0, forceUpdate);
ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/charging";
ixBattStr.toCharArray(ixBuff, 50);
mqtt_publish_i(ixBuff, g_stack.batts[ix].isCharging()?1:0, lastSentData.batts[ix].isCharging()?1:0, 0, forceUpdate);
ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/discharging";
ixBattStr.toCharArray(ixBuff, 50);
mqtt_publish_i(ixBuff, g_stack.batts[ix].isDischarging()?1:0, lastSentData.batts[ix].isDischarging()?1:0, 0, forceUpdate);
ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/idle";
ixBattStr.toCharArray(ixBuff, 50);
mqtt_publish_i(ixBuff, g_stack.batts[ix].isIdle()?1:0, lastSentData.batts[ix].isIdle()?1:0, 0, forceUpdate);
ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/state";
ixBattStr.toCharArray(ixBuff, 50);
mqtt_publish_s(ixBuff, g_stack.batts[ix].isIdle()?"Idle":g_stack.batts[ix].isCharging()?"Charging":g_stack.batts[ix].isDischarging()?"Discharging":"", lastSentData.batts[ix].isIdle()?"Idle":lastSentData.batts[ix].isCharging()?"Charging":lastSentData.batts[ix].isDischarging()?"Discharging":"", forceUpdate);
ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/temp";
ixBattStr.toCharArray(ixBuff, 50);
mqtt_publish_f(ixBuff, (float)g_stack.batts[ix].tempr/1000.0, (float)lastSentData.batts[ix].tempr/1000.0, 0.1, forceUpdate);
}
}
void mqttLoop()
{
//if we have problems with connecting to mqtt server, we will attempt to re-estabish connection each 1minute (not more than that)
static unsigned long g_lastConnectionAttempt = 0;
//first: let's make sure we are connected to mqtt
const char* topicLastWill = MQTT_TOPIC_ROOT "availability";
if (!mqttClient.connected() && (g_lastConnectionAttempt == 0 || os_getCurrentTimeSec() - g_lastConnectionAttempt > 60)) {
if(mqttClient.connect(WIFI_HOSTNAME, MQTT_USER, MQTT_PASSWORD, topicLastWill, 1, true, "offline"))
{
Log("Connected to MQTT server: " MQTT_SERVER);
mqttClient.publish(topicLastWill, "online", true);
}
else
{
Log("Failed to connect to MQTT server.");
}
g_lastConnectionAttempt = os_getCurrentTimeSec();
}
//next: read data from battery and send via MQTT (but only once per MQTT_PUSH_FREQ_SEC seconds)
static unsigned long g_lastDataSent = 0;
if(mqttClient.connected() &&
os_getCurrentTimeSec() - g_lastDataSent > MQTT_PUSH_FREQ_SEC &&
sendCommandAndReadSerialResponse("pwr") == true)
{
static batteryStack lastSentData; //this is the last state we sent to MQTT, used to prevent sending the same data over and over again
static unsigned int callCnt = 0;
parsePwrResponse(g_szRecvBuff);
bool forceUpdate = (callCnt % 20 == 0); //push all the data every 20th call
pushBatteryDataToMqtt(lastSentData, forceUpdate);
callCnt++;
g_lastDataSent = os_getCurrentTimeSec();
memcpy(&lastSentData, &g_stack, sizeof(batteryStack));
}
mqttClient.loop();
}
#endif //ENABLE_MQTT
Immer wieder werde ich angesprochen, mir fehlerbehaftete, kabellose Staubsauggeräte anzusehen. Dabei sind die modernen Geräte nach dem Zyklon Prinzip am häufigsten vertreten. Meist sind es die Akkus, deren BMS (Batterie-Management-System) dem Akku den „Garaus“ macht, indem der im BMS verbaute Microcontroller ein „disabled“ Flag setzt und den Akku unbrauchbar macht. Der Grund dafür ist meist ein defekt einer oder mehrerer Zellen. Genauer gesagt ist es die Balance der Zellen, die nicht mehr gegeben ist oder erreicht wird. Bei vielen Akkusystemen moderner kabelloser Geräte kommen Lithium-Ionen Zellen der Baugröße 18650 zum Einsatz. (zylindrische Zellen mit 18mm Durchmesser und 65mm Länge, einer Nominalspannung von 3.7V und einer Kapazität von durchschnittlich zwei Ah) Ein defekter oder ein, durch das BMS abgeschalteter Akku, lässt sich dann nicht mehr Laden, der Handstaubsauger schaltet nicht mehr ein, wenn der der Trigger gedrückt wird und es gibt je nach Bauart und Modell Blinksignale der im Akku verbauten Leuchtdioden. Zu diesen Fehlerbildern gibt es einige Ansätze, erfolgreiche Reparaturen durchzuführen. Zum einen können aufgrund des Alters des Gerätes Zellen defekt sein, die dann eine viel zu geringe oder gar keine Zellspannung mehr abgeben. Hier ist ein Tausch der einzelnen, betroffenen Zellen dann möglich – wenn auch nicht sinnvoll. Man kann natürlich auch alle Zellen erneuern – so man sich die Arbeit und den Umgang mit dem Punktschweißgerät und dem Nickelband zutraut. Ich hatte aber auch schon einige Geräte am Tisch, dessen Akkus auch keine Ausgangsspannung mehr freischalteten, obwohl die einzelnen Zellen und deren Leerlaufspannung absolut in Ordnung und die Balance untereinander gegeben war. Der einfachste Lösungsansatz war dabei eine genauere Untersuchung der BMS Platine. Ein einfacher Reset des verbauten Microcontrollers erweckte den Akku schon häufig wieder zum Leben. (Hier ist natürlich vorausgesetzt, dass man am Board ein wenig Reverse Engineering betreibt und sich zumindest die Type des Microcontrollers und dessen Beschaltung herauszeichnet) Bei Recherchen im Netz bin ich sogar auf ein GitHub Projekt gestoßen, bei dem jemand eine eigene Firmware für das BMS Board einiger Dyson Akkus entwickelt hat.
Wie im Titel des Beitrags beschrieben, muss es aber nicht immer an einem defekten Akku liegen, dass der kleine Zyklon nicht mehr tobt. In diesem Fall hat sich folgendes Verhalten gezeigt:
Beim Betätigen des Triggers blinkt die LED am Dyson DC34 und der Motor dreht nicht. Steckt man das Ladegerät an den Akku des Saugers an, so leuchtet die LED am Netzteil und es sieht alles nach einem, wie in der Bedienungsleitung beschriebenen Ladevorgang aus.
Dyson DC34 am Ladegerät – noch ist die LED an
Soweit so gut. Doch schon nach einigen Minuten erlischt die LED am Ladegerät und man bekommt signalisiert, dass der Akku vollgeladen ist. Dem ist aber nicht so. Wenn der Trigger betätigt wird, so passiert nichts außer, dass die LED am Dyson zu blinken beginnt. Der Motor bleibt still. Also war mein erster Verdacht – wie so oft – der Akku ist defekt. Also habe ich zuerst einmal den Akku demontiert und die Spannungen der 18650er Zellen gemessen. Die waren jedoch alle ok – also einem Bereich um die 3.5V und vor allem gab es auch unter den Zellen keine großen Abweichungen. Sie waren somit auch gut balanciert, aber eben leer. Im nächsten Schritt habe ich dann das BMS Board genauer betrachtet und den darauf befindlichen Mikrocontroller resettet. Ein erneuter Ladeversuch brachte aber keine Änderungen und LED am Ladegerät erlosch wieder nach einigen Minuten.
So war der nächste Schritt, den Akku mit einem Labornetzteil zu laden und dann messen wie sich das BMS und die Zellen verhalten. Das Modell DC34 ist eines der „älteren“ Geräte. Hier wurden auf der DC Seite noch zwei Spannungen benötigt. 16.75V DC und 24.35V DC werden über den dreipoligen Ladestecker geliefert. Da der Akku ja schon geöffnet war, war es ein Leichtes, die beiden Spannungen anzulegen. Und siehe da, das Laden der Zellen klappte. Nach einigen Minuten am Labornetzteil hatte der Dyson Akku wieder genug Energie um den Sauger zu betreiben. Somit konnte ich den Akku als Defekt ausschließen.
Der nächste Ansatz war nun, das Ladegerät zu untersuchen. Und um das Ergebnis vorweg zu nehmen – genau das war das Problem. Um hier die Ursache zu finden, entschloss ich mich, das Ladegerät zu öffnen. Aufgrund der Nachhaltigkeit der heutigen Konsumentenprodukte ist das Teil ganz einfach zu öffnen – lediglich zwei Kreuzschlitzschrauben halten die Gehäuseschalen zusammen und nach dem Lösen dieser, fallen die Teile einfach auseinander und die Platine liegt auf dem Tisch… – schön wär´s. Leider ist kein Hersteller daran interessiert, dass seine defekten Teile einfach zu reparieren sind. So ist das Gehäuse auch nicht verschraubt. Denn Schrauben sind ja auch teuer. Die Kunststoffteile sind natürlich verschweißt bzw. verklebt und nur mit einer Trennscheibe am Dremel zu lösen. Gesagt – getan.
Nach dem Öffnen und Freilegen der Netzteilplatine habe ich die DC-Ausgänge mit Lasten, die der aufgedruckten Nominalleistung entsprechen, belastet und gleichzeitig die Spannungen gemessen. Diese fallen unter Last um ein paar Millivolt ab, aber sind zu Anfang auch vorhanden. Dann passierte aber folgendes: Nach ein paar Minuten Betrieb fielen die Spannungen auf 0V ab und die LED leuchtete nicht mehr. Aha! Also kurz von der Netzspannung getrennt und wieder eingesteckt. Doch die LED blieb dunkel und es gab keine Ausgangsspannung. Erst nach einigen Minuten ohne AC-Versorgung fuhr das „Schaltnetzteil“ wieder hoch. Unter Last ließ sich das Verhalten immer nachvollziehen. Zwei bis fünf Minuten unter Last und das Netzteil schaltet ab. Bei einem Versuch ohne Ausgangsbelastung passierte lange nichts und die Spannungen standen stabil. Erst geschätzte dreißig Minuten später war wieder Ende. Nach einer genaueren Untersuchung ist mir dann aufgefallen, dass die primäre Ansteuerung des Wandlers auf einmal nicht mehr vorhanden war. Der Enable/Undervoltage Pin des Controller IC wurde aber im Ausschaltmoment über den Optokoppler Transistor nicht aktiviert und trotzdem war Schluss.
DC34 Ladegerät mit Lastwiderständen
Ein kurzes Berühren des Controller ICs mit dem Finger und das darauffolgende Wahrnehmen von unangenehmer Hitze und Schmerz eröffnete mir eine neue Fehlerursache. Das Teil wird thermisch zu heiß und es könnte ja eine „thermal-Protection“ geben. Bingo. Bei dem Controller IC handelt es sich um einen TNY278PN der TinySwitch Family von „power integrations“, einem IC der den Leistungs Mosfet und die Ansteuerelektronik (den PWM Generator) in einem Chip vereint hat. Und sieht man sich das Datenblatt an, dann ist folgendes zu lesen:
Auszug aus dem Datenblatt des TNY
Somit hat es etwas mit der Erwärmung des IC´s, bzw. dessen thermischer Belastung zu tun, die sich anscheinend nicht mehr im Sollbereich bewegt. Glücklicherweise schaltet der Chip durch seine eingebaute Sicherheitstechnik ab und nicht die Sicherung, die auslöst, wenn der Mosfet stirbt und dauerhaft niederohmig wird. Das Abschalten des Controller ICs auch bei Nichtbelastung war ein weiteres Indiz, dass der Chip nicht mehr in Ordnung ist. Also habe ich den IC erneuert und das Board wieder in Betrieb genommen. Die LED und die Ausgangsspannungen waren sofort wieder da. Nach 20 Minuten Dauerbetrieb mit angeschlossenen Lastwiderständen war immer noch alles OK. Auch die Temperatur des Controller IC war durchaus im erträglichen Bereich.
alt vs. neu
Also konnte ich davon ausgehen, dass das Ladegerät wieder ok war. Vor dem Verkleben der Gehäuseschalen bekam ein Gehäuseteil noch ein Tuning in Form von vier Lüftungslöchern. So gibt es zumindest die Möglichkeit, die thermische Abwärme im Netzteil abzuführen.
In einem alten Beitrag aus dem Jahr 2019 habe ich über Oszilloskope des Herstellers Keysight und deren Problem mit einem plötzlichen Ausfall berichtet. (siehe Link). Es ging damals darum, dass die Oszilloskope plötzlich ihren Dienst verweigerten – manchmal auch mit einem Knall und anschließendem Geruch nach „Strom“. Oder es passierte einfach gar nichts nach dem Einschalten. Der Grund dafür war und ist immer wieder der Ausfall des verbauten 12VDC Netzteils CCH0123F1-Z03A. Das Oszilloskop ist so konstruiert, dass das Netzteil bei ausgeschaltetem „Hauptschalter“ des Oszilloskops trotzdem am Netz hängt und im Standby-Modus betrieben wird. Der an der Frontseite des Gerätes befindliche Druckschalter schaltet das Netzteil dann in den PowerON Modus und die 12V Leistung ein. Wenn die Geräte in den Labors permanent an bestromten Steckdosen hängen, verwundert es auch nicht, dass die Geräte schneller altern, als die guten alten Kisten mit den Kathodenstrahlröhren. Die Teile, die der permanenten Stromversorgung durch thermische Dauerlast zu Opfer fallen, habe ich, sowie auch den Reparaturaufwand im damaligen Beitrag dargestellt. Von Seiten der Vertriebsfirmen ist auch ein Nachbestellen oder eine Ersatzlieferung von neuen Netzteilen nicht vorgesehen oder gewünscht. Wenn die Geräte innerhalb der Garantie- Gewährleistungszeit ausfallen sollten, ist der Austausch durch den Hersteller kein Problem. Fallen die Geräte aber erst aus, wenn sie schon ein paar Jahre im Labor oder der Werkstatt verweilen (dabei spielt es aber keine Rolle ob sie jeden Tag in Betrieb sind, oder eben nur angeschlossen und ausgeschaltet herumstehen), so läuft ein üblicher Reparatur- Serviceprozess beim Hersteller. Da sind dann auch die ordentlichen Tarife für den Service von Messtechnik zu bezahlen.
Im Bild oben: „neues Meanwell Powersupply“ unten: „origales Lineage Power“
Die Netzteile sind, wie im alten Bericht beschrieben, recht gut zu reparieren. Allerdings ist die Reparatur auch ziemlich zeitaufwendig. Schneller geht es natürlich, wenn ein neues Netzteil eingebaut wird. Die Vertriebsfirmen der Keysight Oszilloskope bieten leider keinen Ersatzteilsupport an und einen direkten Lieferanten des original verbauten Lineage Power Netzteils habe nicht finden können. Es gibt aber auch eine andere Alternative: Im Forum des EEV-Blogs haben einige User alternative Netzteile gefunden, die in die DSO-X Oszilloskope passen. Ein passendes Modell ist das RPSG-160-12 von Meanwell. Es handelt sich dabei um ein 12V 160W Powersupply. Die Bezeichnung „G“ in RPSG deutet darauf hin, dass auch ein 5V Standby-Supply vorhanden ist. Und genau diese Funktion benötigt auch das DSO-X. Denn wie schon zuvor beschrieben, ist der Frontschalter am Oszi nicht dazu gedacht, die Primärseite der Netzversorgung zu trennen, sondern lediglich eine Leitung am DC-Niedervoltstecker gegen Masse zu schalten. Diese Leitung steuert im Netzteil den „PowerON-Pin“.
Mechanisch passt das Meanwell beinahe auf die Montagehalterungen des DSO-X. „Beinahe“ bedeutet, dass der Abstand der Befestigungslöcher der Längsseite am Powersupply um etwa einen Millimeter weiter auseinander liegt, als die Befestigungsbolzen am Chassis des Oszilloskops. Das lässt sich aber mit einer kleinen Rundfeile oder einem 4mm Bohrer schnell anpassen. Jetzt kann das Meanwell Powersupply mit den originalen Torx Schrauben am Oszi Chassis befestigt werden. Die Steckverbindung für die AC-Versorgung von der OSZI-Platine zum Netzteil kann direkt vom alten Netzteil übernommen werden.
Pinout der Meanwell Steckerleisten
Die 12V Spannungsversogung am CN2 des Netzteils liegt an den Pins 1,2,3,4 (+12V) und 5,6,7,8 (GND) an. Die Verbindungsleitung zum Oszi ins entsprechend anzupassen.
DC12V Ausgänge an CN2
Im Bild unten sind die Pins der Steckerleiste am Oszi beschriftet dargestellt.
DC12V Eingang ins Oszilloskop
Ich habe die Drähte passend umgepinnt und den Stecker wie unten dargestellt mit dem Powersupply verbunden.
Die Hauptenergieversorgung zum Oszilloskop ist jetzt hergestellt. Es fehlt nur noch die „Einschaltleitung“ (PowerOn). Hierzu habe ich den 7. Pin (GND) und den 9.Pin(Switch) aus dem alten Stecker gelöst und direkt auf dem Standby Board des Netzteils angelötet. Der Draht am untersten Pin des Standby Boards ist das Signal „PowerOn“ und der darüber ist GND. Somit kann das Netzteil mit dem frontseitigen Einschalter am Oszilloskop hochgefahren werden.
„Steuerleitungen“ für das PowerOn des Netzteils
Gesamtansicht der Verkabelung
Nach einem kurzen Funktionstest und Überprüfen der Spannung (kann ggf. auch an dem Trimmpoti am Netzteil korrigiert werden) ist der Umbau abgeschlossen und der Zusammenbau kann wieder erfolgen.