"""OTA-Update ueber HTTP, ausgeloest per MQTT (siehe mqtt.py, ``cmd/ota``). Laedt die Projektdateien von einem einfachen HTTP-Server (z.B. ``python3 -m http.server`` im Projektverzeichnis) und startet danach neu. Bewusst ohne Fremdbibliothek (kein ``urequests``) -- ein minimaler HTTP-GET ueber ``socket`` reicht und passt zum Rest des Projekts (vgl. mount_client.py). **Grundsatz: eine fehlgeschlagene OTA darf das Geraet nicht unbrauchbar machen.** Jede Datei wird deshalb zuerst unter einem temporaeren Namen (``.ota``) geladen; erst wenn *alle* Downloads gelingen, werden sie auf die endgueltigen Namen umbenannt und das Geraet neu gestartet. Bricht ein Download ab, bleiben die bisherigen Dateien unveraendert liegen, die temporaeren Reste werden aufgeraeumt -- die Anzeige laeuft mit der alten Version weiter. """ import socket # Dieselbe Liste wie deploy.sh FILES -- was auf dem Geraet gebraucht wird. # settings.json, wifi_config.py und mqtt_config.py gehoeren bewusst nicht # dazu: Laufzeitzustand bzw. Zugangsdaten, siehe deploy.sh. FILES = ( "ticks.py", "migra.py", "config.py", "settings.py", "transport.py", "display.py", "coords.py", "lx200.py", "mount_client.py", "brightness.py", "displayloop.py", "wifi.py", "mqtt.py", "run_esp32.py", "probe.py", "ota.py", "main.py", ) class OtaError(Exception): """Download oder Uebernahme fehlgeschlagen.""" def _get(url, timeout=10.0): """Minimaler HTTP-GET (nur ``http://``). Liefert den Body als bytes.""" if not url.startswith("http://"): raise OtaError("nur http:// unterstuetzt: %r" % url) rest = url[len("http://"):] if "/" in rest: host_port, pfad = rest.split("/", 1) pfad = "/" + pfad else: host_port, pfad = rest, "/" if ":" in host_port: host, port_s = host_port.split(":", 1) port = int(port_s) else: host, port = host_port, 80 try: addr = socket.getaddrinfo(host, port)[0][-1] except (OSError, IndexError) as e: raise OtaError("Adresse %s:%d nicht aufloesbar: %s" % (host, port, e)) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: try: sock.connect(addr) req = ("GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n" % (pfad, host)) sock.send(req.encode()) except OSError as e: raise OtaError("Verbindung zu %s:%d fehlgeschlagen: %s" % (host, port, e)) buf = b"" while True: try: chunk = sock.recv(512) except OSError as e: raise OtaError("Empfang gestoert: %s" % e) if not chunk: break buf += chunk finally: sock.close() kopf, trenner, body = buf.partition(b"\r\n\r\n") if not trenner: raise OtaError("unvollstaendige Antwort fuer %s" % pfad) zeile = kopf.split(b"\r\n", 1)[0] try: status = int(zeile.split(b" ")[1]) except (IndexError, ValueError): raise OtaError("unerwartete Antwort: %r" % zeile) if status != 200: raise OtaError("HTTP %d fuer %s" % (status, pfad)) return body def _aufraeumen(geladen): import os for _, tmp in geladen: try: os.remove(tmp) except OSError: pass def update(base_url, files=None, log=None, feed=None, getter=_get): """Alle Dateien laden, bei vollem Erfolg umbenennen und neu starten. base_url z.B. "http://192.168.178.50:8000" -- Basis-URL, unter der die Projektdateien liegen (ein einfacher HTTP-Fileserver im Projektverzeichnis reicht). files Vorgabe FILES; zum Testen ueberschreibbar. log Ausgabefunktion, Vorgabe print. feed optional: Watchdog-Funktion, vor jeder Datei gerufen -- ein Update ueber viele Dateien darf den Watchdog nicht ausloesen (siehe run_esp32.make_watchdog). getter zum Testen austauschbar; Vorgabe der echte HTTP-GET. Wirft OtaError, wenn irgendein Download fehlschlaegt -- **vor** dem Umbenennen, sodass die bisherigen Dateien unangetastet bleiben. Bei Erfolg kehrt die Funktion unter MicroPython nicht zurueck (machine.reset()); unter CPython (Tests) tut sie es doch, ohne neu zu starten. """ log = log or (lambda *a: None) if files is None: files = FILES base_url = base_url.rstrip("/") geladen = [] try: for name in files: if feed is not None: feed() log("OTA: lade %s ..." % name) body = getter("%s/%s" % (base_url, name)) if not body: raise OtaError("%s ist leer" % name) tmp = name + ".ota" with open(tmp, "wb") as f: f.write(body) geladen.append((name, tmp)) except OtaError as e: log("OTA: fehlgeschlagen (%s) -- bisherige Dateien bleiben unveraendert" % e) _aufraeumen(geladen) raise except OSError as e: log("OTA: Dateizugriff gestoert (%s) -- bisherige Dateien bleiben " "unveraendert" % e) _aufraeumen(geladen) raise OtaError("Dateizugriff gestoert: %s" % e) log("OTA: %d Dateien geladen, uebernehme ..." % len(geladen)) import os for name, tmp in geladen: os.rename(tmp, name) log("OTA: fertig, starte neu ...") try: import machine machine.reset() except ImportError: pass # CPython: nur zum Testen, kein echter Neustart