Archiv der Kategorie: Allgemeines & Neues

Hier poste ich alles, das keiner speziellen Kategorie entspricht

Wenn die Bürste nicht mehr dreht – oder: Reparatur des Kärcher FC7

Loading

„Frisch gewischte Böden, ohne davor Staub zu saugen: Der Hartbodenreiniger FC 7 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 Antriebsabdeckung
Schrauben 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.

 

EVU Smartmeter mit ESP32 und ESPhome auslesen und in Homeassistant verwenden

Loading

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:

ESP32_interface_2023-05-12

interface_usbB_2024-06-27


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  

 

TV-Deckenhalterung motorisiert über Homeassistant steuern

Loading

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 entfernt
Board 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)

Pylontech PV-Akkustatus im HomeAssistant

Loading

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

mqtt:
  sensor:
#Pylontec Akku Serial Readout (ESP32 192.168.xxx.yyy)
    - state_topic: "ingmarsretro/pylontec/ESP_WiFi_RSSI"
      name: "Pylontec_RSSI"
      unit_of_measurement: dB
      
    - state_topic: "ingmarsretro/pylontec/availability"
      name: "Pylontec_Status"
      
    - state_topic: "ingmarsretro/pylontec/currentDC"
      name: "DC-Strom"
      unit_of_measurement: "mA"
      
    - state_topic: "ingmarsretro/pylontec/getPowerDC"  
      name: "getPower DC"
      unit_of_measurement: "W"
      
    - state_topic: "ingmarsretro/pylontec/powerIN"  
      name: "Power IN"
      unit_of_measurement: "W"  
      
    - state_topic: "ingmarsretro/pylontec/estPowerAC"
      name: "Pylontec_estPowerAC"
      unit_of_measurement: Watt  
      
    - state_topic: "ingmarsretro/pylontec/soc"
      name: "Pylontec_SOC"
      unit_of_measurement: "%"  
      
    - state_topic: "ingmarsretro/pylontec/temp"
      name: "Pylontec_temperature"
      unit_of_measurement: "°C"
      
    - state_topic: "ingmarsretro/pylontec/battery_count"
      name: "Pylontec_BatteryCount"
      unit_of_measurement: "pcs"      
      
    - state_topic: "ingmarsretro/pylontec/base_state"
      name: "Pylontec_BaseState"
      
    - state_topic: "ingmarsretro/pylontec/is_normal"  
      name: "Pylontec_is_normal"

    - state_topic: "ingmarsretro/pylontec/powerOUT"
      name: "Pylontec_powerOUT"
      unit_of_measurement: Watt
      
# Pylontech battery module 0      
      
    - state_topic: "ingmarsretro/pylontec/0/current"
      name: "Pylontec_Battery0_current"
      unit_of_measurement: "A"

    - state_topic: "ingmarsretro/pylontec/0/voltage"
      name: "Pylontec_Battery0_voltage"
      unit_of_measurement: "V"
      
    - state_topic: "ingmarsretro/pylontec/0/soc"
      name: "Pylontec_Battery0_soc"
      unit_of_measurement: "%"
      
    - state_topic: "ingmarsretro/pylontec/0/charging"
      name: "Pylontec_Battery0_charging"
      
    - state_topic: "ingmarsretro/pylontec/0/discharging"
      name: "Pylontec_Battery0_discharging"
     
    - state_topic: "ingmarsretro/pylontec/0/idle"
      name: "Pylontec_Battery0_idle"
      
    - state_topic: "ingmarsretro/pylontec/0/state"
      name: "Pylontec_Battery0_state"
      
    - state_topic: "ingmarsretro/pylontec/0/temp"
      name: "Pylontec_Battery0_temp"
      unit_of_measurement: "°C"
      
# Pylontech battery module 1      
      
    - state_topic: "ingmarsretro/pylontec/1/current"
      name: "Pylontec_Battery1_current"
      unit_of_measurement: "A"

    - state_topic: "ingmarsretro/pylontec/1/voltage"
      name: "Pylontec_Battery1_voltage"
      unit_of_measurement: "V"
      
    - state_topic: "ingmarsretro/pylontec/1/soc"
      name: "Pylontec_Battery1_soc"
      unit_of_measurement: "%"
      
    - state_topic: "ingmarsretro/pylontec/1/charging"
      name: "Pylontec_Battery1_charging"
      
    - state_topic: "ingmarsretro/pylontec/1/discharging"
      name: "Pylontec_Battery1_discharging"
     
    - state_topic: "ingmarsretro/pylontec/1/idle"
      name: "Pylontec_Battery1_idle"
      
    - state_topic: "ingmarsretro/pylontec/1/state"
      name: "Pylontec_Battery1_state"
      
    - state_topic: "ingmarsretro/pylontec/1/temp"
      name: "Pylontec_Battery1_temp"
      unit_of_measurement: "°C"
      

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

 

Die Wärmepumpe (NEURA) in das Smarthome einbinden

Loading

Ein Smarthome ist heute keine Seltenheit mehr und sehr weit verbreitet. Es gibt unzählige Systeme am Markt, die das eigene Zuhause „Smart“ machen. Die digitalen Sprachassistenten von Google, Amazon und co. in Verbindung mit Smarten Glühlampen zählen zu den einfach und schnell zu installierenden Systemen. Aber es gibt auch komplexe Smart Home Systeme, bei denen in den Hausverteilern Aktoren für jede Lampe und Steckdosen verbaut sind. Die Fenster und Türen sind mit Meldekontakten ausgestattet und sichern das Eigenheim oder melden, wenn einmal auf das Schließen der Fenster nach dem Stoßlüften vergessen wird. Das diese Systeme bei vernünftiger Programmierung auch zur Energieoptimierung beitragen ist selbstverständlich. Auch ich betreibe Smarthome Komponenten unterschiedlichster Hersteller.

Dazu gehört seit Jahren das HomeMatic System, das sowohl kabelgebunden als auch über das Bidcos-Protokoll mit seinen Aktoren und Sensoren kommuniziert. Das HUE – System von Phillips spricht dabei über ZigBee mit seinen smarten Lampen und Steckdosen. Die Gateways dieser Systeme sind an ein LAN Netzwerk angeschlossen und jedes System bringt seinen eigenen Webserver mit, über den es dann zu steuern und einzustellen ist. Ein Wechselrichter von Photovoltaikanlagen kann seine Daten über unterschiedlichste Schnittstellen (RS485, CAN, RS232) zur Verfügung stellen. Um alle auf eine zentrale Darstellungsebene zu bringen, habe ich mich für das NodeRed System entschieden. Der Dazu notwendige NodeRed Server läuft auf einem Raspberry PI. (Auf der CCU3 mit dem Raspbian Image ist noch genug Platz um den NodeRed Server laufen zu lassen – der ist sogar als eigenes Plugin für die CCU verfügbar und wird „RedMatic“ genannt).  Mit dieser Konfiguration lässt sich fast alles im Bereich Homeautomation „erschlagen“. Mit ESP32 und Raspberry lassen sich über MQTT (Message Queueing Telemetry Transport) bequem Statusinformationen übertragen. Dies wende ich beispielsweise bei den kleinen Einspeise Wechselrichtern einer Balkon PV-Anlage an, als auch bei den PV-Wechselrichtern einer Offgrid-Anlage. Hier werden die Daten über unterschiedliche Bussysteme im Raspberry oder ESP32 empfangen und in das MQTT-Protokoll umgesetzt. Der MQTT Broker sammelt die Daten der einzelnen Geräte und über NodeRed lassen sie sich dann in eine Datenbank schreiben, im Browser oder am Smartphone visualisieren und auch einfach, je nach Bedarf, im HomeMatic System verarbeiten.

Beispiel eines Smarthomenetzwerks

Somit ist es möglich, nahezu alle Systeme miteinander Smart zu vernetzen und, für mich wichtig auf EINER Plattform zu visualisieren. Ein einziges System fehlte bisher noch. Das ist meine alte Neura Heizungswärmpepumpe. Die Firma Neura ist schon seit einigen Jahren nicht mehr existent und der von „b.i.t.“ entwickelte auf Webserver „webidalog“ wurde nie mehr aktualisiert. Die Wärmepumpe hat also einen Webserver auf einem kleinen mit Linux-Rechner onboard und baut die Webapplikation mit einer uralten Java Version. Für die Bedienung muss am PC eine Java Runtime installiert sein, die nur mit einigen Tricks auf einem aktuellen Windows Rechner läuft (Stichwort: Virtualisierung). Für die Bedienung über ein Smartphone ist eine html – Version mit eingeschränkter Funktionalität verfügbar. Mein Plan war es nun, eine Schnittstelle zu finden, mit der ich die Daten der Wärmepumpe zumindest einmal auslesen kann, um Vorlauf- Rücklauftemperaturen der Fußbodenheizung, Kesseltemperatur, etc. auch in meinem NodeRed System zur Verfügung habe. Da zu dem System aber so gut wie keine Dokumentation zu finden ist und ein Reverse-Engineering ein wenig kritisch ist, wenn das System weiter laufen soll, kam mir folgende Idee:

Mit einem „headles browser“ sollte es ja möglich sein, die html-Version der Neura WebDialog Website zu parsen und die relevanten Daten zu finden und über Variablen in MQTT-Topics zu verwandeln. Und hier muss ich einen besonderen Dank an meinen Kollegen Mario Wehr aussprechen, der mir die Softwarestrukur zum parsen der Website gebaut hat. Die Software ist in PHP geschrieben und läuft schlussendlich auf einem Raspberry PI. Hier sind lediglich eine php8-cli runtime und ein paar Module notwendig. Die Software funktioniert so, dass bei jedem Aufruf ein Login auf der Wärmepumpenwebsite ausgeführt wird, danach werden die Daten geparsed und zu MQTT-Broker gesendet. Das kontinuierliche Aufrufen des php-Skriptes habe ich dann einfach mit einem cronjob gelöst, der jede Minute ausgeführt wird.

 

>sudo crontab -e

und der job sieht dann so aus:

* * * * * sudo php /home/neura2mqtt/neura2mqtt.php -c

(wenn man sich die files  ins  /home/ verzeichnis legt…). Das Projekt habe ich auf github unter:  https://github.com/ingmarsretro/neura2mqtt veröffentlicht.

Neura Daten am NodeRed Dashboard

 

 

 

 

MIDI DB50XG – ein Interface für das Daughter Board

Loading

Beim Stöbern in einer Kiste mit meinen alten Bastelarbeiten ist das folgende Kästchen zum Vorschein gekommen.  Es stammt aus der Zeit als ich noch mit Amgia, aber auch schon mit PCs zu tun hatte – ich schätze so ca.  um 1996. Das Kästchen beschriftete ich mit „DB50XG MIDI – Wavetableprozessor“.

Das Fundstück aus der Kiste

Darin befindet sich eine Platine von Yamaha, die sich eben DB50XG nennt. Diese Platine war als Tochterplatine für PC-Soundkarten mit „Waveblaster“ Erweiterungsport konzipiert. Sie erweiterte die Soundkarten um einem polyphonen MIDI – Wavetable – Sampler. So konnte der General Midi Standard und der Yamaha XG Standard wiedergegen werden.  Heute macht sich darüber niemand mehr Gedanken. Wenn man damals mit einem PC aus Midi – Daten Sounds erzeugen wollte, dann war entweder eine externe Hardware notwendig, oder eben eine Soundkarte mit einem OnBoard Midi Synthesizer oder Wavetable Chipsatz. Der PC übernahm dann die Steuerung, das Senden und Empfangen der Midi Daten über eine Sequenzer Software. Heute werden die Midi Sounds direkt am PC generiert und die Samples und Tonmodelle in die Software eingebunden. Damals reichte die Leistung der PC-Hardware dazu nicht aus. Wenn sich jetzt jemand gerade fragt, worüber ich hier palavere – was ist Midi und wofür benötigt man das? – dann sei hier kurz gesagt: Midi ist die Abkürzung für „Musical Instrument Digital Interface“ – also eine digitale Schnittstelle – ein Datenprotokoll für Musikinstrumente. Es dient – grob erklärt – dazu, elektronische Musikinstrumente untereinander zu vernetzen und zu steuern. So kann zum Beispiel über ein einziges Keyboard eine Vielzahl von klangerzeugenden Geräten gesteuert werden. Wie der Midi Standard funktioniert, wie die Datenpakete aussehen und das elektrisch aussieht, werde ich hier nicht erläutern. Dazu findet man, wie immer, reichlich Informationen im Netz.

Im Inneren des Kästchens

Zurück zum selber gebastelten Kästchen. In die Plastikbox habe ich damals das DB50XG gepackt und vom „Waveblaster“-Port, einer 26poligen Buchsenleiste, die notwendigen Leitungen zur Inbetriebnahme der Midi Platine nach Außen geführt. Und das war ziemlich simpel. Die Platine benötigt eine Spannungsversorgung von +/-12V und +5V. Es gibt einen Midi-IN und einen Midi-OUT (Through) Pin, einen Reset-Pin und zwei Analog Audio Out Pins – je einen pro Kanal. Die untenstehende Tabelle zeigt die Pinzuordnung des Steckers:

Pin Nummer Zuordnung
1 Digital Masse
2 nicht verbunden
3 Digital Masse
4 nicht verbunden
5 Digital Masse
6 Versorgung +5V
7 Digital Masse
8 nicht verbunden
9 Digital Masse
10 Versorgung +5V
11 Digital Masse
12 nicht verbunden
13 nicht verbunden
14 Versorgung +5V
15 Analog Masse
16 nicht verbunden
17 Analog Masse
18 Versorgung + 12V
19 Analog Masse
20 Audio out rechts
21 Analog Masse
22 Versorgung -12V
23 Analog Masse
24 Audio out links
25 Analog Masse
26 Reset

Der ganze Aufbau war damals eher sehr spartanisch gestaltet. Die Stromversorgung musste über ein, oder mehrere externe Netzteile hergestellt werden. Es gab keine galvanische Signaltrennung mittels Optokoppler. Da musste ich mich auf den ordentlichen Aufbau des Midi-IO-Controller verlassen, den ich an den Amiga angeschlossen hatte. So durfte das natürlich nicht bleiben. Und das schöne DB50XG Board nicht mehr zu verwenden, oder dem Elektronikschrott zuzuführen, bringe ich nicht über´s Herz. Der Plan der daraus entstand, war, ein neues Interfaceboard zu entwickeln – oder basteln, das möglichst universell einsetzbar werden sollte. 

DB50XG

Diese Idee ist nun schon wieder einige Jahre her und immer wieder einmal habe ich ein wenig daran gearbeitet. Folgende Punkte, so habe ich mir ausgedacht, sollte das Interfaceboard erfüllen:

  • eine einfache Spannungsversorgung soll das Yamaha Board mit Energie versorgen. Idealer Weise soll ein USB-Port und optional ein Anschluss für ein Universalnetzteil vorhanden sein. Alle benötigten Spannungen sollen auf dem Interfaceboard aus den 5VDC generiert werden.
  • Das DB50XG soll, wie seinerzeit, auch als „Huckepack“ Platine aufgesteckt werden können
  • Das Midi-in Signal soll über die 5polige DIN Buchse und auch über einen Pinheader eingespeist werden können – natürlich schön entkoppelt (Damit kann auch ein Microcontroller wie Arduino und co. ganz ohne Aufwand angeschlossen werden)
  • Der Ton, also das Audiosignal soll pro Kanal über je eine Chinch-Buchse und auch als 3.5mm Klinkenbuchse und über einen Pinheader zur Abnahme bereitstehen.
  • Wortwiederholungen SOLLTEN vermieden werden, ist mir aber egal 🙂

Daraus entstand schlussendlich der folgende Schaltplan. Die 5VDC Versorgung der USB Quelle wird direkt zur 5V Versorgung des Midi Boards geführt. Die ebenfalls benötigten +12V/-12V erzeugt ein DC/DC Converter (TMR0522). Dieser wird eingangsseitig vom 5V Netz versorgt. Der optionale „Externe“ Spannungseingang gelangt an einen LM2596ADJ. Das ist ein Step-Down Voltage-Regulator der mit Eingangsspannungen bis zu 40V arbeiten kann. Die geregelte Ausgangsseite ist in vielen Bereichen verfügbar. Ich habe hier den ADJ (Adjustable) Typ in die Schaltung integriert, da ich davon einige Stück im Sortiment Kasten habe. Durch einen Jumper am Board ist, die Spannungsquelle wählbar.

Auf Basis dieses Schaltplanes habe ich ein Layout erstellt und es vorerst einmal im eigenen Ätzbad hergestellt. Heraus kam die folgende Platine, die als Testaufbau diente. Technisch funktionierte das Board einwandfrei, jedoch die Anordnung der Komponenten hat mir nicht gefallen. Den Step-Down Converter samt Spule hatte ich auf der Rückseite platziert. Auch war mir der Abstand zwischen den Anschlussbuchsen zu eng beieinander. Und wie man das als PCB Layouter so macht – man macht immer ein zweites Design. So auch dieses Mal.

Der Testaufbau mit bestücktem Midiboard ist im nachfolgenden Bild zu sehen. Das Midi-Signal als Testquelle kommt vom PC und wird durch einen USB-Midi Adapter aus Fernost generiert.

Also noch einmal vor den Rechner gesetzt und das Layout umgezeichnet. Heraus gekommen ist dann die folgende Version. Diese Ausführung habe ich dann bei einem Leiterplattenhersteller bestellt.

Die schlussendlich gefertigte Platine in bestücktem Zustand sieht dann so aus. Darunter ist sie  mit dem aufgesteckten DB50XG Board zu sehen.

 

Die Wetterkugel – oder das Goethe-Glas

Loading

Immer wieder halte ich Ausschau nach einfachen, interessanten Dingen. Dieses Mal hat mich ein Messgerät- oder eher „Anzeigegerät“ fasziniert, dessen Funktionsprinzip äußert einfach und doch sehr effektiv ist. Zudem ist es aus meiner Sicht auch noch ein Hingucker – Es handelt sich um das so genannte Goethe-Barometer. Die bekannteste Form ist wohl das an der Wand hängende, bauchige Glas mit einem Schnabel, ähnlich einer Gießkanne, in dem der Wasserstand den Luftdruck anzeigt. Eine etwas anders konstruierte Version dieses Glases habe ich im Netz gefunden…

Ein wenig zur Geschichte dieses Aufbaues:

Einem Herrn namens Evangelista Toricelli (1608-1647), einem italienischen Physiker und Mathematiker verdanken wir die Erkenntnis und den Nachweis, dass der Luftdruck Schwankungen unterliegt. Er baute 1643 das erste nach ihm benannte Barometer. 1644 entwickelte er das Quecksilberthermometer.

Ein kleiner Ausgleichsbereich im Anzeigerohr schützt vor Überlauf

Der deutsche Dichter Johann Wolfgang Göthe, beschäftigte sich auch mit den Naturwissenschaften. Er machte selbst viele naturwissenschaftliche Experimente und entwickelte später ein einfaches, aber wirkungsvolles Barometer auf den Grundlagen des Toricelli.

Die Funktionsweise:

Das Barometer zeigt schnell und präzise Luftveränderungen an. Bei steigendem Luftdruck fällt die Wassersäule im Anzeigerohr und bei fallendem Luftdruck steigt sie. Möglich ist dies durch die im Glas eingeschlossene Luft. Das Volumen der Luft bleibt bei gleichbleibender Temperatur immer gleich. Steigt oder sinkt der äußere Luftdruck, so wird die eingeschlossene Luft über die Wassersäule zusammengedrückt oder eben ausgedehnt. Da sich das Wasser nicht komprimieren lässt, ist es das ideale Medium um die Druckunterschiede sichtbar zu machen. Die Höhe der Wassersäule zeigt somit den Luftdruck an. Ist der Luftdruck bei schönem Wetter hoch, so ist der Außendruck gegenüber dem Druck der eingeschlossenen Luft höher und die Wassersäule sinkt, da die eingeschlossene Luft verdichtet wird. Bei niedrigem Luftdruck kann sie sich ausdehnen und der Stand der Wassersäule steigt.

die Höhe des Wasserstandes im Rohr zeigt den Luftdruck an

Dieses kurze Zeitraffervideo zeigt die Änderung des Wasserstandes bei Luftdruckänderung:

Geigerzähler – Bausatz aus Fernost

Loading

Immer wieder fasziniert mich das Thema Radioaktivität. Genauer gesagt ist es das Messen oder Detektieren dieser ionisierenden Strahlung, die durch den Zerfall und von Atomkernen unter Abgabe von Energie entsteht. Dabei unterscheidet man prinzipiell die aus der Bewegung der zerfallenden Teilchen (also Teilchenstrahlung) ausgesandte Energie (Alpha- und Beta- Teilchen) und der Strahlungsenergie, die als elektromagnetische Welle transportiert wird (Gammastrahlung und auch Röntgenstrahlung). Diese Strahlungsarten haben unterschiedliche Energiedichten und Reichweiten. Je nach Art sind sie mehr oder weniger einfach abzuschirmen. Alphastrahlung ist eine Teilchenstrahlung, die von Materie (Luft, Wasser) stark abgebremst wird und ein Blatt Papier gar nicht mehr durchdringt. Allerdings geben diese Teilchen auf ihrer sehr kurzen Distanz die Energie ab. Das ist besonders gefährlich, wenn diese Partikel eingeatmet werden, oder an den oberen Hautschichten strahlen. Gammastrahlung wiederum durchdringt wie eine Funkwelle Materie sehr leicht und lässt sich am wirkungsvollsten mit Blei abschirmen. Das auch diese Strahlungsart alles andere als ungefährlich ist, braucht man wohl nicht zu erwähnen.

Diese Strahlung kann man nicht sehen, riechen, schmecken oder sonst irgendwie direkt wahrnehmen, aber die Gefährlichkeit ist trotzdem vorhanden. Mit relativ einfachen Techniken kann man diese Zerfallsprozesse aber sichtbar, bzw. hörbar machen und zählen. 

Das bewerkstelligt man schon seit langem mit einem sogenannten Zählrohr oder dank der modernen Technik auch mittels Halbleiter. Ein P-N-Übergang wird in Sperrrichtung betrieben und unter Ausschluss von Licht (also abgedunkelt) der ganz kleine Sperrstrom gemessen. Trifft nun energiereiche Strahlung auf diesen P-N-Übergang dann wird der Stromfluss kurzzeitig erhöht und kann detektiert werden.

Immer wenn sich die Möglichkeit ergibt, sehr günstig zu einem Detektor zu kommen, greife ich natürlich zu. So auch dieses Mal. Einen einfachen Bausatz, basierend auf der Detektion mittels Zählrohrs musste ich mir ansehen. Der Bausatz stammt aus Fernost und besteht aus einer Basisplatine, einem aufgesteckten Arduino Nano und einem ebenfalls aufgesteckten LC-Display.

Alle für die Detektion notwendigen Komponenten befinden sich auf dem Mainboard. Dazu zählt unter anderem die Hochspannungserzeugung für das Zählrohr, die mittels einfacher Boost-Konverterschaltung, angetrieben von einem 555er, realisiert wird. Um das Zählrohr auf dem Mainboard zu befestigen hat der Designer dieses Boards einfache Glasrohrsicherungshalter gewählt. Die passen zwar nicht ganz exakt, lassen sich aber soweit ausdehnen, dass sie das Zählrohr gut festhalten. Das Zählrohr ist übrigens ein J305. Es ist ca. 90mm lang und hat einen Durchmesser von knapp einem Zentimeter.

Das Zählrohr arbeitet bei einer Anodenspannung von 350V bis 480V. Nachstehend habe ich die Spezifikationen aus dem Datenblatt aufgelistet:

  • Anodenspannung: 350 v bis 480 V
  • Typ: J305 Geiger-Zählrohr
  • Kathodenmaterial: Zinnoxid
  • Wandungsdichte: 50 ± 10 cg/cm²
  • Betriebstemperaturbereich: -40 °C bis 50 °C
  • Durchmesser: 10 mm (±0,5 mm)
  • Länge: 90 mm (±2 mm)
  • Eigenhintergrundstrahlung: 0,2 Impulse/s
  • Empfindlichkeit gegenüber γ-Strahlung: 0,1 MeV
  • Stromaufnahme: 0,015 mA bis 0,02 mA
  • Arbeitsspannung: 380 V bis 450 V
  • γ-Strahlung: 20mR/h ~ 120mR/h
  • β-Strahlung: 100 ~ 1800 Pulse/min.
  •  100 ~ 1800 Pulse/min.

Die Signaldetektion sowie die Aufbereitung des Signals erfolgt auch auf dem Mainboard. Die erkannten Impulse werden über einen kleinen Piezolautsprecher wiedergegeben. Um sie auch zählen zu können, muss man nicht mit einer Stoppuhr vorm Lautsprecher sitzen und im Minutenabstand die Pieptöne zählen – nein – das übernimmt ein Microcontroller, der wie heute üblich, aus einem fertigen Board besteht. Hier hat der Designer einen Arduino Nano (oder auch Nanonachbau) gewählt. Auf dem wiederum läuft ein Programmchen, dass das Zählen der Impulse übernimmt und auch gleich schön auf einem zweizeiligen LC-Display anzeigt und idealerweise auch noch in µSievert/h umrechnet. Um die Pulse dem Arduino zu übergeben, wird der Pegel des Signals auf TTL-Level gebracht und an den Interrupt-Eingang des Arduino geschaltet. Das LC-Display benutzt den I2C Ausgang des Arduino. Die Leitungen dafür werden lediglich von der Buchsenleiste, in die der Arduino gesteckt wird, über das Mainboard zur Buchsenleiste für das Display geführt. Um das ganze System mit Spannung zu versorgen, werden direkt die 5V vom USB-Anschluss des Arduino verwendet. Optional kann man die 5V auch über eine Steckerleiste am Mainboard anschließen.

Ist alles zusammengebaut und die USB-Versorgung angesteckt, dann gibt es zuerst einmal eine kurze Wartezeit in der die Hochspannung aufgebaut wird. Hier hat sich der Programmierer eine Animation ausgedacht, die am Display „Boot…“ anzeigt.

Und dann geht es auch schon los. Der Geigerzähler ist betriebsbereit und beginnt zu zählen. Als Test habe ich lediglich eine alte Uhr, deren Zeiger mit Radiumfarbe bemalt sind, zur Verfügung. Hier ist zumindest eine deutliche Änderung der Anzahl der detektierten Zählimpulse festzustellen, wenn man die Uhr in die Nähe des Zählrohrs bringt.

LEGO Mindstorms EV3 mit Matlab und Simulink

Loading

Wer mit Matlab und Simulink arbeitet oder lernt und einen LEGO EV3 Brick samt Sensoren und Aktoren besitzt, kann diese Systeme miteinander verknüpfen. Rund um den EV3 Brick gibt es natürlich eine Community und etliche Plattformen, über die man mit dem Brick kommunizieren kann. Doch dazu später.

Der Lego EV3 Brick ist im Wesentlichen ein Einplatinencomputer der mit einem LC-Display und vier Input- sowie vier Output-Ports (Schnittstellen für Sensoren und Aktoren) ausgestattet ist. Als Datenschnittstellen gibt es einen USB Port als HOST und einen MiniUSB Port als Interface zum PC und den diversen Softwaretools.

Will man mit dem Brick wireless kommunizieren, so ist dies über WiFi, beziehungsweise über Bluetooth möglich. Allerdings ist die Hardware zur Funkübertragung nicht im Brick verbaut und muss als USB Dongle in den Host-Port gesteckt werden.

Folgende USB Wifi Sticks sind lt. Internetrecherche mit dem EV3 und der originalen Legofirmware ab 1.08 kompatibel: (Quelle: Internet)

  • tp-link WL0084E
  • EDIMAX EW-7811Un mit dem Chipsatz RTL8188CUS
  • LogiLink WL0084W auf der Vertriebsseite (Conrad) angeblich mit dem Chipsatz Railink RT5370 jedoch das Herstellerdatenblatt gibt den Chipsatz RTL8188EUS an.

Im Test hat sich gezeigt, dass lediglich Dongles mit dem Realtek RTL8188CUS Chip im embedded Linux des EV3 Driver mäßig implementiert sind. Der Nachfolger von Realtek der RTL8188EUS funktioniert hier nicht.  Und die meisten Hersteller von Wifi USB Dongles (Stand 2021) vertreiben ihre Sticks mit dem 8188EUS Chip, sei dies TPLink, Edimax, LogiLink und wie sie alle heißen. Erkennbar ist das bei manchen an der Typenbezeichnung V2, oder 7811UN V2 (bei Edimax) oder V.02, V.03 bei TPLink. Ist diese Kennzeichnung beim Stick oder dessen Verpackung dabei dann wird das nix. Zumindest nicht, wenn mit der originalen Legofirmware gearbeitet werden will.

Vergleich edimax 7811un und 7811un v2
edimax 7188un mit RTL8188CUS chip
edimax 7188un V2 mit RTL8188EUS chip

Nun weiter zum EV3 Brick:

Auf diesem Einplatinencomputer läuft ein auf Linux basierendes embedded Betriebssystem das auf einem internen Flash- Speicher abgelegt ist. Dieses Betriebssystem wird in Form einer Firmware von LEGO zur Verfügung gestellt und ist zur Zeit in der Version 1.10 verfügbar. Diese Firmware ist beinhaltet sämtliche Partitionen der Laufwerke und deren Inhalt und wird im Rahmen eines Updates in einem „.bin“ File verpackt in den Flash geladen. Mit den Softwaretools „binwalk“ ist es unter Linux möglich, die Struktur der Partitionen aus dem Firmware File wiederherzustellen, zu ändern und wieder als „.bin“ Firmware Image auf die Hardware hochzuladen.

Es gibt aber auch Custom Firmware und Betriebssysteme die per SD-Card gebootet werden können. Welche Treiber hier mitgeliefert werden ist auf den Seiten der entsprechenden Custom OS Hersteller nachzulesen.

Folgende Betriebssysteme habe ich für den EV3 auf die Schnelle gefunden:

  • ev3dev (https://www.ev3dev.org/)
  • leJOS (https://sourceforge.net/projects/lejos/files/lejos-EV3/)

Man kann den EV3 mit folgenden (und ich nenne hier auch nur die gängigen die auch mir eingefallen sind) Tools bzw. Programmiersprachen programmieren:

  • LEGO EV3 Originalsoftware
  • NI Labview bist Labview Fall2016 ? (Unterstützt bzw. EV3 Plugins implementiert)
  • Matlab und Simulink (Harware Supportpackage für EV3 notwendig) link
  • MicroPython EV3 link

Fakt ist jedoch: der in die originale Lego EV3 Firmware (1.08H , 1.09D) integrierte Treiber für WLAN-Sticks unterstützt nur den Chipsatz RTL8188CUS. Und den benötigt man, wenn man über SIMULINK im „Monitor und Tune“ Modus mit dem Brick arbeiten will. Denn interessanter Weise klappt das NUR über eine WiFi Verbindung und NICHT über einen direkten USB Link zwischen PC und EV3.

Es ist auch NUR mit der Firmware 1.08H möglich mit Simulink zu kommunizieren da hier TELNET und SSH als Kommunikationsprotokoll genutzt werden. Und nur in der 1.08H ist eine ungeschützte Verbindung der Weg zum Erfolg. Ab der Version 1.09D ist ein Login notwendig.

Da die Firmware Versionen mittlerweile schwer zu finden sind, stelle ich hier einen Link zum Download herein.

Download EV3 Firmware 1.08H
Download EV3 Firmware 1.09D

Was benötigt man nun, um über Simulink mit dem EV3 sprechen zu können ?

  • zunächst muß Matlab und Simulink installiert sein
  • dann muss man das Support Package EV3 für Matlab und EV3 für Simulink herunterladen und installieren
  • auf dem EV3 Brick solltedie originale Firmware 1.08H vorhanden sein. Ist dies nicht der Fall kann die Version über den Firmwaredownloader in der orginalen EV3 PC Software auf das Gerät übertragen werden.
  • im EV3 Brick muß ein USB Wifi Dongle mit dem „CUS“ Chipsatz stecken
  • der PC muß idealerweise im selben Netz hängen wie auch das Wifi-Funknetz mit dem sich der Brick verbindet. Ein DHCP Server muß die IP Adressen für die Funkgeräte verteilen
  • jetzt kann der Brick gestartet werden. Danach muß er mit dem WLAN Netz verbunden werden (es sollten WPA2 Passwörter ohne Sonderzeichen festgelegt sein, da der EV3 Brick diese nicht zur Verfügung stellt)
  • ist der EV3 mit dem Netz verbunden, dann ist unter dem Menu „Brick Info“ die aktuelle IP Adresse des Bricks und die Brick ID zu notieren. Diese Infos werden gleich im Simulink Setup benötigt
  • nun zu Simulink: dort ist im Menu „configuration Parameters“ unter „Hardware Implementation -> Hardware board“ Mindstorms EV3 zu wählen 
  • das Setup wird im selben Menu unter „Hardware board settings“ -> Target hardware resources -> Groups -> Host to Target Connection festgelegt. Hier ist „Ethernet“ als Connection Type einzustellen. Die vorher ermittele IP Adresse und ID des Brick sind hier auch einzugeben.Das war´s dann auch schon. Nun sollte es möglich sein eure Simulink Modelle mit der EV3 Hardware zu nutzen und auch die Tune Infos während der Simulation am PC zu sehen …