Firmware ohne Changelog: Änderungen zwischen Versionen mit 15 Zeilen Python finden
Für die Stromversorgung meines Fahrrads gab es drei Firmware-Versionen, aber keine Liste der Änderungen. Ich habe aus jedem Image die lesbaren Zeichenketten gezogen und die Mengen verglichen. So wurde sichtbar, dass 0.1.2 genau den Bereich neu regelt, der mir beim Laden Probleme machte.
$ git log --graph Der Weg zur Lösung
-
Problem
Kein Changelog
Ladeabbrüche während der Fahrt, drei Firmware-Versionen, aber keine Liste der Änderungen.
-
Versuch
Manifeste verglichen
Größe, Datum und Zephyr-Revision zeigen nur, dass sich viel geändert hat.
-
Erkenntnis
Klartext im Image
Zephyr-Images enthalten Logmeldungen und Hilfetexte lesbar.
-
Lösung
Mengenvergleich der Texte
Der Vergleich zeigt: 0.1.2 bringt eine tempoabhängige Schutzschaltung am Dynamo.
Canyons ECLIPS (nRF52840, Zephyr, MCUboot) gibt es in drei Firmware-Versionen: 0.1.0, 0.1.1 und 0.1.2. Ein Changelog gibt es nicht. Mein Rad brach das Laden über den Dynamo während der Fahrt immer wieder ab und lud erst nach einem Halt weiter. Ich wollte wissen, ob ein Update daran etwas ändert, bevor ich es aufspiele.
Das Problem
Die Update-Pakete sind normale DFU-Zips mit manifest.json und app_update.bin. Das Manifest verrät nur Größe, Datum und Zephyr-Revision. Das Image selbst ist Maschinencode, und Disassemblieren ist aufwendig. Zephyr-Firmware enthält aber viele Logmeldungen und Shell-Hilfetexte im Klartext. Genau die verraten, welche Funktionen dazukommen.
Die Lösung
Pro Version das Zip nach fw/version/ entpacken, dann die lesbaren Zeichenketten als Mengen vergleichen:
import sys, re
def s(p):
return set(m.decode() for m in re.findall(rb"[ -~]{5,}", open(p,"rb").read()))
d = sys.argv[1]
a, b, c = (s(f"{d}/{v}/app_update.bin") for v in ("0.1.0","0.1.1","0.1.2"))
noise = re.compile(r"^[^a-zA-Z]*$|^.{0,6}$")
for name, x, y in (("0.1.0 -> 0.1.1", a, b), ("0.1.1 -> 0.1.2", b, c)):
print(f"\n===== {name} =====")
print("--- NEU ---")
for t in sorted(y - x):
if not noise.match(t): print("+", t)
print("--- WEG ---")
for t in sorted(x - y):
if not noise.match(t): print("-", t)
python -I fwdiff.py fw > firmware-diff.txt
Das Ergebnis waren gut 230 Zeilen. Ausschnitt für 0.1.1 → 0.1.2:
+ Prt: Disabling protection circuit (spin-up from rest, SoC=%d%%)
+ Prt: Re-enabling protection circuit after %ds at speed %d
+ Prt: Speed dropped below 15 km/h, resetting hold timer
+ Prt: Speed exceeded override threshold (%d km/h). Enabling protection circuit!
+ Prt: Sudden acceleration spike detected (%d km/h). Enabling protection circuit!
+ Shutdown pending [%u/%u]:- OCV: %d mV; SoC: %u%%
+ Timeout waiting for MAX17261 to be ready after model refresh
+ TS_COLD detected
Und für 0.1.0 → 0.1.1 unter anderem:
+ Dynamo Charging completed, disabling charger
+ Dynamo charging failed, attempting USB fallback
+ Switched from dynamo to USB charging
+ Front Light ON (POR + VBat >= threshold)
+ Advertising Google FMDN
Was man daraus lesen kann
- 0.1.1 baut die Laderegelung um: eigener Zustand „Laden“, Umschalten zwischen Dynamo und USB. Dazu kommen Find-My-Ortung und ein automatisches Frontlicht nach Akkuspannung.
- 0.1.2 steuert die Überspannungs-Schutzschaltung am Dynamo nach Tempo: frei nach dem Anfahren aus dem Stand, nach einer Haltezeit bei Tempo wieder an, Rücksetzen unter 15 km/h, Sofort-Schutz bei Tempo-Spitzen. Das deckte sich auffällig mit meinem Fehlerbild: Laden startet nur nach einem Halt, und im Moment des Abbruchs meldet das Gerät unplausible Tempowerte.
- Dazu kommen ein Akku-Schutz über die Ruhespannung (OCV), ein „model refresh“ für den Ladestands-Chip MAX17261 und Temperaturstufen.
Ob das Update mein Ladeproblem wirklich löst, sagen die Texte nicht. Sie zeigten mir aber, dass sich der Versuch lohnt und an welcher Stelle Canyon gearbeitet hat.
Stolperfallen
- Die Texte zeigen, was neu ist, aber nicht die Zahlen dahinter, etwa Schwellen und Haltezeiten. Dafür muss man doch disassemblieren.
- Mindestlänge und Rauschfilter sind ein Kompromiss. Sind sie zu lasch, ertrinkt der Vergleich in zufälligen Byte-Folgen aus dem Maschinencode.
- Formatstrings wie
Chg: %d, %dverraten nebenbei, welche Felder eine Statusantwort enthält. Das hilft auch bei eigenen Auswertungen. - Bei verschlüsselter Firmware funktioniert das nicht. Diese Pakete sind signiert, aber nicht verschlüsselt.