[HBW] booter für HM Wired (OTA updates)

Begonnen von loetmeister, 01 August 2026, 14:29:39

Vorheriges Thema - Nächstes Thema

loetmeister

Hallo,

maxx3105 hat sich die Mühe gemacht einen boot loader für HM Wired (Homebrew) zu entwickeln.
Zitat von: maxx3105 am 28 Juli 2026, 13:15:15So ich habe die https://github.com/maxx3105/HBWired und die https://github.com/maxx3105/HBW-Booter nun auf den aktuellen Stand gebracht. Damit müssten die Änderungen nun leicht ersichtlich sein, die ich für den (Open)CCU betrieb gemacht habe.
Ein großes Danke dafür!  8)


Ich habe ein paar Tests gemacht, und mir einen Fork auf github erstellt. Erst mal hat es auf anhieb funktioniert, da aber ein weiteres Gerät mit am Bus hing, tauchten ein paar Probleme auf.
So reagierte der booter auf auf alle broadcasts. Auch habe ich das initiale timeout reduziert, da ein device reset (message 0x21 0x21) ebefalls über ein Watchdog reset arbeitet. Eventuell könnte man das ändern... jetzt dauert ein restart ~4 Sekunden länger, was auch kein Problem ist.
Meine Änderungen. Kann ein pull request erstellen, falls das ok ist...
https://github.com/maxx3105/HBW-Booter/compare/main...loetmeister:HBW-Booter:main#diff-2a3c7d892139b12b843b5534701749e1369e69be4fe47408b86c737246b8d577R416
Ich teste noch was weiter, wie es sich mit FHEM und mehr Geräten am Bus verträgt...


Im flash_tool.py ist bei mir auch was krumm... hatte in Zeile 125 das 'print' einfach rausgenommen, dann lief es durch.  ::)
Flashe .\HBW-CC-WW-SPktS.ino.hex: 19354 Bytes, 0x0000..0x4B99
z z u  (Booter-Einstieg) ...
  -> Booter aktiv (StartupReason)
p  (Blockgroesse) ...
Traceback (most recent call last):
  File "C:\EigenerKram\elektronik\projekte\HBWired\booter\flash_tool.py", line 125, in <module>
    if d[1][:1] == b'p': print(f"  -> Booter meldet Blockgroesse {d[1][1]}")
                                                                  ~~~~^^^
IndexError: index out of range


PS: Änderungen an HBWired schaue ich mir noch an... den booter start habe ich über "resetSystem" gemacht. (der code ist ja schon vorhanden)
        switch(frameData[0]){
            case 'u':                                                              // Update (Bootloader starten)
              pendingActions.resetSystem = true;  // don't reset immediately, send ACK first
              // der HBW-Booter erkennt WDRF und bleibt im Update-Modus
              break;
        }

Gruß,
Thomas

loetmeister

Hallo,

habe das flash_tool.py Skript um einen optionalen Adressparameter erweitert, um beliebige Geräte am Bus zu adressieren. Auch die Aufhebung des sleep Modus nach Erfolg und Abbruch ergänzt, sonst würde alles eingefroren bleiben.
In meinem Test setup sieht es soweit gut aus, am "echten" bus mit vielen anderen Geräten steht der Test noch aus. :)
flash_tool.py COM9 C:\projekte\HBWired\HBW-xyz\build\arduino.avr.nano\HBW-xyz.ino.hex 0x42001234
Benutze Geraeteadresse: 0x42001234
...

Einfacher als das flash_tool.py wäre es wenn man in FHEM, am HM485_LAN device eine Upload Option hätte. So wie man dort den discovery modus starten kann. (set hm485 discovery start)
HM485 device (mit HM485d für USB Dongle)
TYPE       HM485_LAN
InterfaceType HMW-SOFT-GW


Änderung in der HBWired lib habe ich erst mal in meinem dev branch gemacht.
enabled 'u' (update) command to test booter
https://github.com/loetmeister/HBWired/commit/d95d7ceff4b7202f32ef78f4119ba24470d1e9d6
Der Weg über den watchdog ist einerseits schön, da man relativ Plattform- / MCU unabhängig ist - andererseits ist das "update" Kommando nicht das einzige was den watchdog reset auslöst / auslösen könnte.
@maxx3105, was sprach gegen de Sprung zur BOOTSTART Adresse?

Gruß

maxx3105

Hallo Thomas,

super Tests, danke — beide Punkte waren echte Bugs, sind gefixt:

flash_tool.py: Die p-Blockgrößen-Zeile war doppelt kaputt. Die Booter-Antwort auf p ist ein ACK-Frame mit Payload [0x00, Blockgröße] — kein 'p' im ersten Byte. Die Prüfung d[1][:1]=='p' hat also nie die echte Antwort getroffen und ist bei einem kurzen Fremd-Frame (dein zweites Gerät am Bus) in den IndexError gelaufen. Jetzt auf ACK-Frame + Länge geprüft, dann greift auch die Blockgrößen-Anzeige.
Booter/Broadcasts: Du hast völlig recht — der Booter hat jeden Broadcast beantwortet. Ich lasse jetzt nur noch z/Z als Broadcast durch (die antworten bewusst nicht) und beantworte alles andere ausschließlich adressiert. Damit ist die Kollision mit anderen Geräten weg.
PR sehr gerne — oder wir gleichen kurz ab, ich habe die zwei oben schon lokal drin.

Zur BOOTSTART-Frage: Ich bin bewusst über den Watchdog gegangen, aus drei Gründen — (1) plattformunabhängig, die App braucht kein MCU-spezifisches BOOT_START und keine Sprung-/Stack-/IRQ-Akrobatik; (2) der echte Reset gibt einen definierten Registerzustand, ein direkter Sprung in die Boot-Section würde den ganzen App-Zustand mitschleppen; (3) der AskSin-Funk-OTA-Bootloader macht es genauso (wdt_enable).

Dein Einwand stimmt trotzdem: reines WDRF ist nicht eindeutig, der Device-Reset läuft ja auch über den WDT. Sauberste Lösung ohne den Plattformvorteil aufzugeben: ein Magic-Wort in .noinit zusätzlich zum WDRF. Das u-Kommando setzt es vor dem Reset, der Booter bleibt nur bei WDRF && Magic, der Device-Reset ist damit wieder eindeutig ,,App". Dein reduziertes Timeout kann als Sicherheitsnetz gerne drin bleiben.

Gruß Markus

loetmeister

Hallo Markus,

danke für die Details. Blockgroesse stimmt nun:
p  (Blockgroesse) ...
  -> Booter meldet Blockgroesse 64

Ich habe mal einen pull request mit noch ein paar kleinen Anpassungen erstellt:
https://github.com/maxx3105/HBW-Booter/pull/1

Ja, WDRF + ".noinit" variable klingt spannend. Wusste gar nicht das es so was im RAM gibt :)


Habe das erste Gerät am Bus per OTA aktualisiert. Hatte nur den Booter (also ohne app) mal ein paar Stunden laufen lassen und dann die App Datei übertragen. Hat alles ohne Probleme geklappt.
In FHEM ändert sich bei einem "getConfig" (oder FHEM restart) der D-deviceKey zu "genric" - wie erwartet, aber nach dem OTA update und einem neuen "getConfig" ist alles wieder da.

Es wird ja immer nur ein Gerät in den bootloader modus versetzt, bzw. startet einzelne Geräte. Ein interessantes Testszenario wäre noch wenn man viele Geräte nur mit booter am Bus hat, und alle zeitgleich eingeschaltet werden (Strom kommt auf den Bus) dann würden alle STARTUP_REASON und ANNOUNCE ohne Kollisionsabfrage senden. Wobei es kein retrasmit gibt und dann nach dem großen Geschrei auch schnell wider ruhe einkehrt..  ::)

Gruß,
Thomas

maxx3105

#4
Hallo,

PR ist gemerged. Danke dafür.

Ich habe Claude mal an eine Fhem-Upload-Erweiterung angesetzt.

ZitatWas am echten FHEM noch zu verifizieren ist (im Code als A/B/C markiert)
Diese drei kann ich ohne laufendes FHEM nicht abschließend klären — es sind die einzigen echten Unbekannten:
A) Broadcast: reicht target='FFFFFFFF', damit CMD_SEND mode 0x00 (kein ACK) sendet? → in 00_HM485_LAN.pm prüfbar.
B) Antwort-Weg: kommt die Booter-ACK (Frame 0x19 + Payload) bis HM485_ProcessResponse? u/w stehen nicht in validRequestTypes — evtl. Liste erweitern oder Hook eine Ebene früher.
C) Payload-Format der ACK-Antwort in FHEM ($msgData) — für p sollte '0040' ankommen.

Um das "Geschrei" etwas zu sortieren könnte ich einen kleinen adress-abgeleiteten Random-Delay vor dem Announce (wie HBWired-Geräte es machen) einführen, aber kritisch ist es nicht.

Dein #TODO: add validation of sender address beim g-Announce würde ins selbe Thema fallen (richtiges Gerät im Announce-Getümmel erkennen).

Edit. Habe die Erweiterung angehängt. Bitte testen.

loetmeister

Hi Markus,

danke, das sieht schon vielversprechend aus... glaube die Einbindung in das HM485 Perl Modul klemmt noch etwas. Oder ich habs irgendwo versemmelt... :)

Meine Schritte:
HM485_fwUpdate.pl, als .pm hier hin kopiert:
/opt/fhem/FHEM/lib/HM485# ls
ConfigurationManager.pm  Constants.pm  Device.pm  Devices  HM485d  HM485_fwUpdate.pm  PeeringManager.pm  Util.pm  XmlConverter.pm

10_HM485.pm an gepasst:
use lib::HM485::HM485_fwUpdate;
'fwUpdate' => 'textField',nach Zeile 575 eingefügt
https://github.com/kc-GitHub/FHEM-HM485/blob/40a88b890d6aee5963e9497493f438f5fd45f124/FHEM/10_HM485.pm#L575

} elsif ($cmd eq 'fwUpdate') {
            return HM485_fwu_Start($hash, $value);
#        }
nach Zeile 621
$value statt $a[2] und ohne schließende Klammer
https://github.com/kc-GitHub/FHEM-HM485/blob/40a88b890d6aee5963e9497493f438f5fd45f124/FHEM/10_HM485.pm#L621

foreach my $d (values %{$modules{HM485}{defptr}}) {
            next unless $d->{fwu} && $d->{IODev} && $d->{IODev} == $ioHash;
            HM485_fwu_OnResp($d, $msgData);
            return;
        }
nach Zeile 2005
https://github.com/kc-GitHub/FHEM-HM485/blob/40a88b890d6aee5963e9497493f438f5fd45f124/FHEM/10_HM485.pm#L2005


2026.08.12 22:13:39 2: HBW_CC_WW_SPktS_HBW7296375: fwUpdate: /home/mobile/HBW/HBW-CC-WW-SPktS.ino.UART_v0.14.hex -- 16520 Bytes, 0x0000..0x4087
2026.08.12 22:13:40 3: HBW_CC_WW_SPktS_HBW7296375: fwUpdate: retry 1 at state 2
2026.08.12 22:13:40 5: hm485: HM485_LAN_Write TX: 221
2026.08.12 22:13:40 5: SW: fd02dd4b
2026.08.12 22:13:40 5: hm485: HM485_LAN_parseIncommingCommand: MsgId: 221 Cmd: 97
2026.08.12 22:13:40 5: hm485: HM485_LAN_parseIncommingCommand: Alive: (221) 00 AliveStatus: 00
2026.08.12 22:13:40 3: HBW_CC_WW_SPktS_HBW7296375: fwUpdate: retry 2 at state 2
2026.08.12 22:13:41 3: HBW_CC_WW_SPktS_HBW7296375: fwUpdate: retry 3 at state 2
2026.08.12 22:13:41 1: HBW_CC_WW_SPktS_HBW7296375: fwUpdate FAILED (HBW_CC_WW_SPktS_HBW7296375): timeout at state 2
Die hex Datei wird gelesen, und fwUpdate aufgerufen.
Leider sehe ich kein z z / Z Z oder anderes auf dem Bus. CMD_SEND scheint noch nicht zu passen.


Gruß,
Thomas

maxx3105

Hi Thomas,

das Log zeigt genau die Ursache, und sie liegt auf meiner Sende-Seite, nicht an deinem Einbau. Dein set-Zweig (mit $value) und der Hook waren richtig gedacht. Zwei Sachen:

1. Warum nichts auf den Bus ging (der eigentliche Bug): Meine Sende-Helfer riefen IOWrite(**$ioHash**, ...) mit dem IO-Hash auf. FHEMs IOWrite will aber das Device-Hash — es sucht sich ->{IODev} selbst, exakt wie HM485_DoSendCommand: IOWrite($hash, HM485::CMD_SEND, {target,data}). Mit dem IO-Hash sucht IOWrite dessen ->{IODev} (existiert nicht) und wirft den Frame lautlos weg → ,,timeout at state 2". Das %params-Format und die Hex-Daten waren korrekt. In der angehängten .pl gefixt (_sendAcked + _sendBroadcast).
Du darfst diesen Dateianhang nicht ansehen.

2. Bitte den Hook eine Ebene höher setzen — statt in HM485_ProcessResponse in HM485_Parse, direkt nach

my $msgData = uc( unpack ('H*', substr($message, 4)));und vor if ($msgCmd == HM485::CMD_RESPONSE):

foreach my $d (values %{$modules{HM485}{defptr}}) {
    next unless $d->{fwu} && $d->{IODev} && $d->{IODev} == $ioHash;
    HM485::Util::Log3($ioHash, 3, 'fwUpdate RX: msgCmd='.$msgCmd.' msgData='.$msgData);
    HM485_fwu_OnResp($d, $msgData);
    return $ioHash->{NAME};
}
So fangen wir die Booter-Antwort ab, egal ob sie als CMD_RESPONSE oder CMD_EVENT reinkommt (in ProcessResponse verpasst du den Event-Fall), und $msgData ist der volle Payload.

Dann einmal testen: jetzt sollten z z und u wirklich auf dem Bus liegen und der Booter antworten. Schick mir die fwUpdate RX:-Zeilen aus dem Log (verbose 3 reicht) — daran justieren wir den letzten offenen Punkt, den Payload-Offset: OnResp erwartet die p-Antwort als 0040 (Blockgröße bei substr 2) und die r-Antwort als reine Flash-Bytes ab Offset 0. Stehen im $msgData noch führende Bytes (Sender-Adresse o.ä.), verschieben wir die zwei substr-Offsets entsprechend — ein Einzeiler je Stelle. CRC und der ganze Ablauf sind im Trockenlauf schon durch, das ist der letzte Handgriff.

Gruß