Venus-Firmware: Vorzeichen-Bug, tote CAN-Nachrichten und der Watchdog
Aus der Analyse im Reverse-Engineering-Repository sind drei Befunde auch ohne Ghidra nachvollziehbar. Sie zeigen, was beim Lesen von Registern und beim Schreiben von Konfiguration in der Control-Firmware v150 tatsächlich passiert.
Register 32101 und der unsignierte Divisionsfehler
Register 32101 ist als Batteriestrom des BMS dokumentiert: int16, Einheit 0,1 A. Beim Entladen liefert es Werte, die zu keinem Strom passen. Im Leerlauf bei etwa −0,3 A kommt 39321 (0x9999) zurück, beim Entladen mit 600 W und −12,2 A kommt 39309 statt −12. Beim Laden mit 600 W stimmt es dagegen: erwartet 10, geliefert 10. Der Fehler trifft also nur Entladewerte.
Die Ursache liegt im gemeinsamen Lese-Serializer für FC03. Der Wert wird in eine unsignierte Variable geschrieben, vorzeichenerweitert aus int16, und dann mit dem Skalierungscode des Deskriptors umgerechnet. Multiplikation und Negation funktionieren im Zweierkomplement weiter, die Division durch 10 aber teilt −122 als 4294967174. Die unteren 16 Bit dieses Ergebnisses landen in der Antwort. Das Python-Äquivalent reproduziert beide beobachteten Werte genau:
def serialize(sram_int16):
v = sram_int16 & 0xFFFFFFFF # (uint)(short)x
v = v // 10 # unsigned division
return v & 0xFFFF # low 16 bits copied to the response
serialize(-3) # -> 39321
serialize(-122) # -> 39309Unabhängig bestätigt wurde der echte Strom über Register 34301 (Strom von Pack 4, ohne Skalierungscode) und die Leistungsanzeige per Bluetooth. In v150 kombiniert genau ein Register einen Vorzeichentyp mit einer Divisionsskala, nur 32101 ist betroffen. Der Fehler sitzt aber im Serializer und würde jedes künftige Register mit dieser Kombination treffen. Nutzbar ist 32101 beim Entladen nur näherungsweise (Strom ≈ (raw − 39321) A, 1 A Auflösung); der Strom je Pack (etwa 34301) ist korrekt vorzeichenbehaftet. Als Korrektur genügt eine vorzeichenbehaftete Variable. Dazu liegt ein englischer Fehlerbericht im Repository.
Register 46500 bis 46544: schreiben ohne Empfänger
Dieser Block besteht aus 15 Strukturen zu je 8 Byte, adressiert in Dreiergruppen (idx = (reg − 46500) / 3). Jeder Schreibzugriff hat zwei Nebenwirkungen: Er schreibt 8 Byte in den EEPROM ab 0x3700 + idx*8, ohne zu prüfen, ob sich der Wert geändert hat – der Block sollte also nicht zyklisch beschrieben werden –, und er sendet eine CAN-Nachricht an den Wechselrichter mit Funktionscode 0x30 + idx.
Die Micro-Firmware v116 kennt aber nur die Funktionscodes 0x01 bis 0x07, 0x10 bis 0x13, 0x16 und ab 0x50. 0x30 bis 0x3E fehlen im Dispatcher vollständig, die Nachrichten werden ohne Reaktion verworfen. Auch das BMS ist nicht der Empfänger: Sein Dispatcher prüft zuerst das Zielfeld der CAN-ID (Bits 12 bis 15), muss 3 sein, die Nachricht trägt 4. Insgesamt sendet der Control an das Ziel „Wechselrichter“ 23 Codes, für die es in Micro v116 keinen Empfänger gibt. Die Feldbedeutung des Blocks ist offen; denkbar bleibt eine in dieser Firmware-Paarung nicht fertig implementierte Funktion.
Die 30-Minuten-Resets und vier falsche Fährten
Der Mechanismus war aus dem Code bekannt: Liegt mehr als ein Telemetrie-Datensatz im Puffer, ist der 1800-s-Timer abgelaufen und das Statusregister 37001 gleich null, wird die Ethernet-Brücke zurückgesetzt – zwei bis drei Sekunden ohne Netz. Die Folgerung, den Upload zu beantworten, führte zum Offline Endpoint, und die Resets liefen trotzdem weiter. Die Fallstudie hält fest, warum vier plausible Erklärungen falsch waren:
- Die Zeitantwort war fehlerhaft: stimmte, ändert aber nichts.
- Ein Kaltstart räumt den Zähler ab: verschob den ersten Reset auf 2133 s, der Takt kehrte aber zurück.
- Node hält die Verbindung per Keep-Alive offen:
Connection: closeänderte nichts. - Die Empfangsschleife braucht CR LF am Ende: real im Code, aber auch mit CR-LF-Ende und chunked Framing blieben es vier Versuche im Abstand von 20 Sekunden.
Entschieden hat ein Kontrollexperiment: Ein Durchreichmodus gab die Zeitabfrage an die echte Cloud weiter und lieferte deren Antwort wörtlich zurück. Echte Antwort: eine Anfrage, akzeptiert. Selbst gebaute Antwort mit gleichem Rumpf: vier Anfragen und Aufgabe. Der Unterschied lag nur in den Kopfzeilen – Node hängt Keep-Alive: timeout=5 an und sortiert anders. Die Lehre aus dem Dokument: Jeder der vier Tests bestand darin, etwas zu ändern, und eine Änderung, die nicht hilft, beweist nichts. Es fehlte eine Kontrolle mit bekannter guter Gegenstelle und nur einer geänderten Variable. Nach der Korrektur lief der Upload im 300-s-Takt und es gab sieben Stunden keinen Reset.
Zu den Projekten
-
Marstek Offline Endpoint
Selbst gehosteter Container, der den Telemetrie-Upload eines Marstek Venus lokal beantwortet. Die 30-Minuten-Netzwerk-Resets der Firmware, die Modbus TCP und Home Assistant stören, bleiben damit aus.
-
Marstek Venus Firmware Reverse Engineering
Statische Analyse der Firmware von Marstek Venus D/E mit Ghidra: Control/EMS, Micro-Inverter/VNS und BMS, vollständige Modbus-TCP- und RS485-Registerkarte, Protokollnotizen und Python-Scan-Werkzeuge.
Unterstützen
Diese Tools entstehen in meiner Freizeit und bleiben kostenlos, offen und cloud-frei. Wenn dir eins davon einen Nachmittag gespart hat, freue ich mich über einen Kaffee oder ein Stück Pizza.
Kaffee oder PizzaWeitere Beiträge
- Rollladen-Steuerung pro Raum: Taster, Fenster, Kälte und Regen Eine Automation pro Raum, die Taster, Fensterkontakt, Kälte, Regen und die Zeit vor Sonnenaufgang zusammenführt. Zwei Merker-Helfer verhindern, dass sich die Auslöser gegenseitig überfahren.
- Stromausfall erkennen und Geräte wiederherstellen in Home Assistant Nach einem Stromausfall booten Lampen und Steckdosen in zufälligen Zuständen. Drei Erkennungswege, ein gemeinsames Script und ein Wiederherstellungsfenster bringen alles wieder in den gewünschten Zustand.
- Rauchmelder löst aus: Alle Rollläden fahren hoch (Home Assistant) Bei Rauchalarm sollen die Rollläden im ganzen Haus hochfahren, damit Fenster als Fluchtweg frei sind. Die Automation ist kurz – aber ein fehlendes „from“ im Trigger entscheidet darüber, ob sie im Ernstfall überhaupt feuert.