Freifunk-Rebooter

Aus Hobbyelektronik.org

Ausgangssituation

In unseren Flüchtlingsunterkünften stehen mehrere Xiaomi Mi Router 4A Gigabit Edition, die mit der Freifunk-München-Firmware bespielt sind. Wer Freifunk nicht kennt, kann und sollte am besten sich in der Wikipedia informieren.

Was am Anfang noch recht gut funktionierte, wurde mit der Zeit immer instabiler. Ein Cronjob zum nächtlichen Neustart brachte zwar Besserung, aber nach ein paar Tagen gingen die Router in eine Bootloop.

Um den Fehler besser zu verstehen, wurde zeitweise ein Raspberry Pi zum Protokollieren abgestellt. Als jemand, der von Linux nur rudimentäres Wissen besitzt, sahen die Meldungen so aus, als könne der SoC einen der WLAN-Chips nicht mehr initialisieren und schoss sich ins Nirwana. Blöd: ein Reboot richtet das nicht, Dokumentation für die Chips hängt wohl hinter der NDA-Mauer und einen hard-reset scheint es nicht zu geben.

Nach einem kurzen Austausch im Chat der Freifunker gab es ein Ticket bei OpenWRT und nach einigen paar Tagen auch eine Experimental-Firmware, die den Fehler beheben sollte. Sollte.

Ein paar Tage später stand bei Grafana für die Knoten wieder "no data", dafür gab es Klagen aus der Unterkunft. Einer der Router steht hinter für die dauerhaft verschlossener Tür, der andere in einem Zimmer dessen Bewohner auch mal ein paar Tage nicht da sind (und absperren).

Lösungsansätze

Kurz den/die entsprechenden LS-Schalter zu werfen ist leider auch keine Option. Das genauere Warum habe ich nicht weiter hinterfragt, das muss man einfach als gegeben annehmen.

Dabei ist Strom trennen einfach wie effektiv. Eine Zeitschaltuhr würde helfen, die billigeren haben aber recht grobe Zeitraster und dementsprechend lange Ausfallzeiten. Auch kann es natürlich gleich nach einem Zwangsneustart gleich wieder zu Problemen kommen und die Wartezeit zum nächsten Neustart wird unnötig lang.

Zwischenstecker mit Tasmota o. ä. würden zwar das Problem mit der Ausschaltzeit lösen, hätten aber wenig Chancen gegen Problem 2. Zwar könnten diese checken, ob das WLAN wegfällt, die Unterkunft wird allerdings durch mehrere Freifunk-Router (mit gleicher SSID) versorgt. Unnötig viel Komplexität.

Die Idee

Man sieht es den Routern von außen an, wenn sie abschmieren – im Betrieb leuchtet die Power-LED permanent blau, bei einem Crash bzw. Bootloop blinkt sie orange vor sich hin. Mit ein bisschen Mustererkennung erkennt man sogar die Phasen.

Warum also nicht einen kleinen Mikrocontroller auf die Status-LED(s) schauen lassen und das "have you tried to turn it off and on again"-Spiel spielen lassen?

Datensammeln

Am Anfang war die Beobachtung: In einem kleinen WhatsApp-Video bekam ich das unheilvolle orange blinken von einem Vereinsmitglied; ein paar Sekunden sind jedoch noch nicht die ganze Geschichte, deshalb holte ich mir einen der Router nach Hause und nahm ein Video von einem Kaltstart des Gerätes auf und wartete ein bisschen.

Ein paar Tage am Netz und siehe da: ohne Zutun von außen blinkt die Power-LED orange. Also nochmal die Kamera raus, Aufnahme und einige Minuten laufen lassen.

Als Referenz gab es dann noch ein Video von einem erfolgreichen Start:

Datei:Ff rebooter xiaomi start.mp4

Mit den Videos kann man sich die Mäusedisco nun wieder und wieder ansehen, Bild für Bild durchgehen und die Blinkzyklen aufschreiben, oder man ist faul und fragt ChatGPT, wie man mit Python die Farben von Pixeln in Videos ermittelt. Ok, das ist wirklich keine rocket science (und einfach nur in der Doku von OpenCV zu stöbern hätte gereicht), aber schon erstaunlich, was Wortstatistik auf Steroiden leisten kann.

Videoauswertung

Für den ersten Eindruck ist die Herangehensweise eher unakademisch - das Beispiel vom Chatbot wird angepasst, damit an zwei Punkten - Power- und Link-LED - die Farben ermittelt werden, diese werden als Zellenhintergrund für eine HTML-Tabelle ausgegeben. Einfach wie fatal.

Um nicht nur einen Pixel zu sampeln, wird jeder Frame um den Faktor 16 dezimiert - statt 1280x720 wird also ein Bild der Größe 80x45 verwendet. Das macht den Prozess nicht schneller, dafür werden Rauschen und Überstrahlungseffekte etwas reduziert.

import cv2
video = cv2.VideoCapture("S1040005.MP4")

frame_decimation = 16
frame_skip = 1
probe_points = [[970, 173], [1039, 168]]

framecnt = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
print(framecnt)

print("<style> body { background-color: black; } td { width: 20px; height: 1px; } </style>")
print("<table style=\"border-collapse: collapse;\">")

for frame_number in range(0, framecnt, frame_skip):
    video.set(1, frame_number)

    # Read the frame
    success, frame = video.read()
    newsize = (int(frame.shape[1] / frame_decimation), int(frame.shape[0] / frame_decimation))
    resized = cv2.resize(frame, newsize, interpolation = cv2.INTER_AREA)

    # Get the color of the pixel at the given location
    cols = []
    for point in probe_points:
        (b, g, r) = resized[int(point[1]/frame_decimation), int(point[0] / frame_decimation)]
        cols.append(f"#{r:02X}{g:02X}{b:02X}")
    
    #print("\t".join(cols))
    print("<tr>" + "".join(map(lambda x : f"<td style=\"background-color: {x}\"> </td>", cols)) + "</tr>")

print("</table>")
video.release()

Bei Videomaterial mit 25 fps (was zugegebenermaßen etwas wenig ist), sieht ein etwas längerer Ausschnitt des Videos oben wie folgt aus:

Links, wie im Video oben, die Power-LED, rechts die Link-LED. Wie aus dem Quellcode ersichtlich sein sollte, entspricht bei frame_skip = 1 jedes Frame ein Pixel in y-Richtung bei 25 fps. "Misst" man die Pixel vom ersten Lebenszeichen der linken LED bis sie blau wird, "vergehen" 1840 Pixel und somit 73,6 s - nicht ganz. Weil HTML ist das was man eingibt nicht unbedingt das, was angezeigt wird. Laut Konsole ist jeder oben angegebene Pixel 2px in der Darstellung, somit beträgt die Boot-Zeit 36,8 s.

Der Bootloop ist nicht spektakulärer, er ist lediglich die immerwährende Wiederholung des "orangen" Parts.

Umsetzung

Hardware

Der erste Gedanke war, die Hardware minimalinvasiv aufzubauen: Nachdem es Dauerlicht nur gibt, wenn die Kiste läuft, ist die Farbe irrelevant. Blinkt etwas länger, ist etwas im Argen. Also warum nicht einfach einen Fototransistor anflanschen, die Helligkeit auswerten und wenn längeres Blinken herrscht denk Anker werfen?

Es scheitert wie immer an der Realität. Die ersten Versuche waren aussichtsreich, mit Umgebungslicht und nicht exakt platziertem Fototransistor war die Freude aber recht schnell wieder vorbei.

Für die ersten Versuche auf dem Breadboard hat es zumindest gereicht.

Um nicht extra Teile bestellen zu müssen, wurde das eingeplant, was herum liegt, Herzstück ist ein (völlig überdimensionierter) ATmega8A, der Signale zweier Fototransistoren im SMD-Package, aber auch in der THT-Version in Form von BPW40 entgegennimmt.

Um beide LEDs am Router erfassen zu können, sind diese im Abstand von 15 mm zueinander platziert.

Auf der anderen Seite des Mikrocontrollers hängen p-Kanal FETs sowohl im SO-8- als auch SOT-23-Gehäuse, über die die Versorgung des Routers geschaltet wird.

Um evtl. doch noch einen UART mit abweichender Spannung (und Domäne) anbinden zu können, noch zwei einfache Levelshifter dazu und ab auf eine Leiterkarte damit.

Noch zwei Status-LEDs dazu und fertig ist der Lack.

Recht viel Optionales, aber eine zweiten Leiterkarten-Spin zu fahren: #gerkeinbock

Leiterkarten-Spin? Ja, mittlerweile bin ich echt zu faul, etwas auf Lochraster aufzubauen - zumal so gut wie alle Bauteile im SMD-Package daherkommen.

Der zusammengeklöppelte Schaltplan, Layout und der Aufbau sehen wie folgt aus:

BOM & Nachbau

Nachdem die Hardware schnell-schnell zusammengeklöppelt wurde, ist aktuell keine BOM und genauere Details vorgesehen. Wer sie dennoch möchte, kann sie gerne aus den EAGLE-Daten erzeugen oder mich anschreiben.

Firmware

Die Software hat im Prinzip nur eine Aufgabe: Blinkt die Power LED länger als x Sekunden, wird die Stromversorgung für y Sekunden unterbrochen.

"Genau das, nur etwas komplizierter!"

Zunächst gilt die Frage zu klären: Wann blinkt was, wann ist es statisch an, wann aus?

ADC

Vorher muss aber erst noch geklärt werden: Wann ist eine LED überhaupt an? Mit den vorhin erwähnten Fototransistoren muss man das Umgebungslicht filtern. Ist die LED länger statisch und das Licht "außenrum" ändert sich, oder es verrutscht etwas leicht, verpasst man vielleicht, dass sich etwas ändert.

Kann man filtern, kann man aber auch von Grund auf vermeiden. auch wenn die Elektronik dafür vorgesehen ist: man muss die optischen Sensoren nicht verwenden, sondern kann auch einfach auf die Ansteuerung der LEDs gehen. Diese erfolgt jedoch nicht mit boardähnlichen 5 V, sondern mit 3,3 V, was für die Eingänge etwas knapp werden kann. Also kommt doch der ADC zum Einsatz, wenn auch etwas vereinfacht: zwei Schwellenwerte, dazwischen eine Hysterese, fertig.

Einen Schritt zurück: Der Analog zu Digital-Wandler durchläuft, nachdem das "Subsystem" getriggert wurde alle ausgewählten Kanäle, was etwa jede Millisekunde geschieht.

Schwellenwertvergleich und ab mit einem true oder false in die LED-Detektion.

LED-Detektion

Diese schaut, ob sich was am Zustand des Einganges ändert und zählt die entsprechende Zeit. Wechselt der Zustand nach "0" (false) wird erst einmal blinkend angenommen, bleibt der Zustand für länger als LEDDET_TIMEOUT (750 ms) konstant, wird "statisch aus" respektive "an" angenommen.

Das Ganze wird für die rote und blaue LED der "Power-LED" gemacht...

Zustandsautomat

... und im "Target"-Zustandsautomaten verwurstet, der zunächst auf Papier entstand und nach der Implementierung auf dem PC nachgezeichnet wurde:

Nach dem Einschalten geht die Maschine zunächst in den Off-Zustand - hauptsächlich um die Realität abzubilden. Beim Startup wird die Stromversorgung für den Router eingeschaltet und die grüne LED blinkt vor sich hin.

Sobald für länger als 2 Sekunden die orange LED des Routers erloschen ist und die blaue leuchtet, wird angenommen dass der Router läuft. Ist das nach 90 Sekunden am Strom nicht der Fall, wird ein Crash angenommen und ein harter Reboot eingeleitet (mehr dazu später).

Im Zustand Running gibt es zwei Varianten für die eigenen Status-LEDs: In jedem Fall leuchtet die grüne LED dauerhaft, wurde vorher ein Crash "erkannt", blitzt zusätzlich die rote LED auf.

Sollte nun am Router die orange LED an gehen oder blinken und die die blaue LED nicht an sein, gibt es einen Crashverdacht; allerdings erst, wenn dies für über 2 Sekunden mit "Waterlevel" der Fall ist. Mit Waterlevel ist gemeint, dass im "Verdachtszustand" ein Zähler hochgezählt wird. Ist der "Verdacht" vorbei, wird dieser Zähler nicht auf Null zurückgesetzt, sondern lediglich verringert. So kann ein instabiler Zustand etwas besser erkannt werden.

Erhärtet sich der Crashverdacht, wird ein interner Zähler für die Verdachtsfälle hochgezählt, erreicht dieser die magische Anzahl 3 oder bleibt der Zustand für über 90 Sekunden, wird ein Neustart eingeleitet. "Fängt" sich der Router wieder, sprich: die blaue LED leuchtet konstant und die orange ist aus, wechselt der Zustand nach 5 Sekunden wieder nach Running. Allerdings hier ohne Waterlevel. Lieber einen harten Neustart als im Limbus zwischen Running und Crashed hängen bleiben. Da beim erhärteten Crashverdacht vermutlich nix mehr geht, darf nun die rote Status-LED blinken, die grüne bleibt dunkel.

Beim Durchführen des Reboots wird die Stromversorgung zum Router unterbrochen und die rote Status-LED blinkt schnell. Nach 5 Sekunden beginnt der Lebenszyklus des Routers von Vorne (Off) und er darf wieder starten.

Wozu der Aufwand?

Der Router startet zwar recht schnell (etwa 40 Sekunden reine Bootzeit), benötigt im dümmsten Fall aber 1-2 Minuten, bis er wieder mit dem Freifunk-Netz verbunden ist. Daher sollen unnötige Reboots vermieden werden.

Warum aber so lange Wartezeiten, bevor der Strom abgedreht wird?

Auch hier: Vorsicht. Es gibt auch legitime Gründe, warum der Router einen "Crash" hinlegt. Dieser lässt sich nicht von einem (gewollten) Neustart unterscheiden, sei es ein Admin aus der Ferne oder ein Firmware-Update. Gerade letzteres sollte man nicht durch einen harten Neustart unterbrechen. Nach einem Test dauert der erste Start nach einem Update unter 90 Sekunden, mal hoffen, dass sich das auch so im Feld verhält - schließlich sitzen die Bewohner nicht gerne ohne Internet und ich stehe ungern vor verschlossenen Türen, wenn es etwas zu tun gibt.

Damit nicht der Mikrocontroller zum Problemfall wird, ist der interne Watchdog permanent aktiviert und wird (mehr oder weniger sinnvoll) im Mainloop zurückgesetzt.

Einbau

Dafür, dass nach erstem Plan ein 3D-Druck-Gehäuse auf den Router geklebt werden sollte, hat sich ein fast idealer Platz im Gehäuse selbst gefunden - und auch für die Integration hatte Xiaomi ein Herz für Bastler. Oder zumindest beschlossen, dass ein 0-Ohm-Widerstand (R752) als Sicherung reicht. Mit dem nicht genutzten Verpolschutz (D12) gibt es auch ein Massepad direkt nebenan:

An dieser Stelle sei angemerkt, dass man sicherlich auch den Enable/Shutdown-Pin des/der Regler hätte verwenden können, bei der unbekannten Beschaltung und den Widerständen im 0201-Gehäuse wawr es mir allerdings zu fummelig, dort einzugreifen.

Den LEDs sieht man es mit scharfem Auge schon an, welche die orange und welche die blaue sein muss (wenn auch nicht unbedingt im Foto): Blaue LEDs haben (wie weiße) üblicherweise 2 Bond-Drähte, der ideale Abgriffpunkt ist zwischen Vorwiderstand und Kondensator:

Auf- und Eingebaut sieht es dann wie folgt aus:

Im Foto fehlt noch der obligatorische Heißkleber. Der Kondensator des Implantates musste noch ein bisschen gebogen werden, damit er nicht mit dem Gehäusedeckel kollidiert.

Die abgeschnittenen Drähte sind ein Überbleibsel der oben erwähnten Logging-Aktivitäten (und wurden noch gestutzt).

Die verbaute supergrüne LED ist trotz niedrigem Strom recht hell - sie ist, obwohl nach unten gerichtet, durch das Gehäuse leicht sichtbar und projiziert einen deutlichen Lichtkeil unter den gelöcherten Gehäuseboden. Kann man auch als Feature betrachten.

Das Ergebnis

Die ersten Tests mit per Konsole abgesetzten Reboots und einem erzwungenen Update waren erfolgreich - bei der Gelegenheit wurde auch gleich die Debug-Ausgaben eines Lebenszyklus (leider ohne Timestamps) aufgezeichnet:

Built on: Apr  2 2023 21:47:12
Target: off -> off
Target: off -> startup
LED or: blinking
LED bl: blinking
LED bl: off
LED or: on
LED or: blinking
LED or: off
LED or: blinking
LED or: off
LED bl: on
Target: startup finished
Target: startup -> running
Target: running, saw 0 crashes
LED bl: blinking
LED bl: off
LED or: on
Target: running -> crashed
LED or: blinking
LED or: off
LED or: blinking
LED or: off
LED bl: on
Target: recovered from crash state, saw 1 crashes
Target: crashed -> running
Target: running, saw 1 crashes
LED bl: blinking
LED bl: off
LED or: on
Target: running -> crashed
LED or: blinking
LED or: off
LED or: blinking
LED or: off
LED bl: on
Target: recovered from crash state, saw 2 crashes
Target: crashed -> running
Target: running, saw 2 crashes
LED bl: blinking
LED bl: off
LED or: on
Target: running -> crashed
Target: saw 3 crashes, reboot
Target: crashed -> reboot
LED bl: on
Target: reboot -> off
Target: off -> startup
LED or: blinking
LED bl: blinking
LED bl: off
LED or: on
LED or: blinking
LED or: off
LED or: blinking
LED or: off
LED bl: on
Target: startup finished
Target: startup -> running
Target: running, saw 0 crashes

Patient 1 ist zum Zeitpunkt der Erstellung dieses Artikels seit 7 Tagen im Einsatz, legte erst einmal einen Lauf von knapp 5 Tagen hin und brauchte dann in etwa in etwa alle 12-17 Stunden einen Tritt. Router 2 ist erst seit kurzer Zeit wieder am Netz und hat kurz nach dem Start mutmaßlich einen Crash auf dem anderen Router verursacht - zumindest gab es eine verdächtige Korrelation zwischen Einschalten von Router 2 und einem Crash von Router 1.

Bei beiden Geräten wurden die Cronjobs für den nächtlichen Reboot deaktiviert.

Andere Faktoren die Abstürze begünstigen sind mir aktuell nicht bekannt - bei beiden Geräten habe ich vor der Wiederinbetriebnahme ein Update erzwungen, bei einem auch manuell Caches geleert. Vielleicht bringt das was, vielleicht auch nicht.

Auch gilt es noch WLAN-Mesh auf zumindest einem der Geräte zu deaktivieren, zumal beide über Kabel angebunden sind und Komplexität sicherlich nicht zur Stabilität beitragen.

Ausblick

Die Schaltung kann verkleinert und optimiert werden, alles Unnötige entfernt werden. Hintergedanke für den Levelshifter an der UART-Schnittstelle war, über die Menge an Ausgaben (oder Keywords) einen Absturz zu erkennen oder auch Rückmeldungen z. B. per HTTP-Request auf die Konsole zu injizieren. Geschadet hat das Vorsehen sicher nicht, dank der bisherigen Zuverlässigkeit der aktuellen Erkennungsmethode und Statistik-Tools von ffmuc ist das aber nicht nötig.

Wenn die Geräte etwas länger wieder im Betrieb sind, wird es ein Update geben.

Generell halte ich einen externen Watchdog für Geräte, auf die man keinen permanenten Zugriff hat, für essenziell. Besser wäre natürlich, wenn es gar nicht bräuchte - was ich nicht als Kritik an Freifunk München/Gluon/OpenWRT meine. Die Projekte leben überwiegend von freiwilligen, die in ihrer Freizeit und Community mehr bewegen als manch kommerzielle Anbieter.

Downloads