From 540ce5b1eb77230016ca35beca9450f95d6e5e79 Mon Sep 17 00:00:00 2001 From: cjjohn <72096478+Grovy311@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:06:53 +0200 Subject: [PATCH] Fix crash when desktop config JSON is deleted: self-heal from board server.py and desktop_viewer.py read-only mode required the desktop config JSONs to exist and crashed/showed "Config-Datei fehlt" if one was deleted, even though the board already holds the config durably in NVM. Add versapad_combined.fetch_from_board()/load_or_fetch(): a missing combined JSON is now transparently rebuilt from the board via serial and cached back to disk, falling back to the legacy per-profile JSONs (or a clear error) only when the board is unreachable. --- AGENTS.md | 14 +++++++++++++ desktop_viewer.py | 30 +++++++++++++++++++++++---- server.py | 21 +++++++++++++------ versapad_combined.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fdfc96a..0604c07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,20 @@ Dokumentation und Verifikation unten für die Größeneinschätzung). vorhanden, und fällt sonst auf die klassischen `versapad_config{1,2,3}.json` zurück — beide Ansichten müssen dieselbe Quelle zeigen, sonst wirkt eine Bearbeitung "verschwunden". +- **Bug behoben 2026-08-08:** `server.py` (`vp.load_profile()`) und + `desktop_viewer.py` (`_current_profile_view()`) lasen im Nur-Lese-Modus + hart von den Desktop-JSONs -- fehlten sie (z.B. User loescht sie), gab es + eine ungefangene `FileNotFoundError` bzw. "Config-Datei fehlt"-Anzeige, + obwohl das Board die Config laengst dauerhaft im NVM haelt. Fix: neue + `versapad_combined.fetch_from_board()`/`load_or_fetch()` -- fehlt die + Kombi-JSON, wird sie automatisch per Serial vom Board neu aufgebaut und + als Cache gespeichert (self-healing), nur bei unerreichbarem Board (Port + belegt/kein Board) bleibt der Fallback auf die alten Einzel-JSONs bzw. + eine Klartext-Fehlermeldung. In `desktop_viewer.py` nur versucht, wenn + Live-Sync aus ist (sonst haelt der Serial-Hintergrundthread den + COM-Port -- zwei gleichzeitige Zugriffe auf denselben `self.ser` waeren + eine Race Condition). Die Desktop-JSONs sind damit reiner Lesecache, kein + Pflegeaufwand mehr fuers Board-Backup. - **Bug behoben 2026-08-07:** `_on_toggle_editing()` initialisierte `self.combined` beim ersten Aktivieren des Programmiermodus mit `vcomb.default_combined()` — das seedet ALLE 3 Profile aus den alten diff --git a/desktop_viewer.py b/desktop_viewer.py index 008b2c7..926337b 100644 --- a/desktop_viewer.py +++ b/desktop_viewer.py @@ -526,8 +526,13 @@ class VersaPadViewer(tk.Tk): Bevorzugt die kombinierte Datei (versapad_config_all.json), falls vorhanden -- so zeigen im Programmiermodus gespeicherte Aenderungen sich auch hier, statt dass die alten Einzel-JSONs weiter durchscheinen. - Faellt zurueck auf die klassischen versapad_config{1,2,3}.json, wenn - es noch keine kombinierte Datei gibt.""" + Fehlt sie (z.B. versehentlich geloescht) und ist Live-Sync gerade aus + (COM-Port frei), wird sie automatisch per Serial vom Board neu + aufgebaut und als neuer Cache gespeichert -- das Board ist die + eigentliche Quelle der Wahrheit, kein Datei-Handling von Hand mehr + noetig. Nur wenn das nicht klappt (Board nicht erreichbar, Live-Sync + haelt den Port), weicht es zuletzt auf die klassischen + versapad_config{1,2,3}.json aus.""" if os.path.exists(vcomb.DEFAULT_PATH): try: data = vcomb.load_file(vcomb.DEFAULT_PATH) @@ -538,8 +543,25 @@ class VersaPadViewer(tk.Tk): }) return cfg, f"Quelle: {vcomb.DEFAULT_PATH}" except (KeyError, IndexError, ValueError): - pass # kaputte/unvollstaendige Datei -- auf Einzel-JSONs ausweichen - return vp.load_profile(self.profile), f"Quelle: {vp.CONFIG_PATHS[self.profile]}" + pass # kaputte/unvollstaendige Datei -- weiter unten ausweichen + elif not self.live_sync.get(): + try: + data = vcomb.fetch_from_board(link=self._link) + vcomb.save_file(data, vcomb.DEFAULT_PATH) + raw = data["profiles"][self.profile] + cfg = vp.annotate_profile({ + "buttons": [dict(b) for b in raw["buttons"]], + "encoders": [dict(e) for e in raw["encoders"]], + }) + return cfg, "Quelle: Board (neu vom Geraet geladen)" + except RuntimeError: + pass # Board nicht erreichbar -- weiter unten ausweichen + try: + return vp.load_profile(self.profile), f"Quelle: {vp.CONFIG_PATHS[self.profile]}" + except FileNotFoundError as e: + hint = (" (Live-Sync ist an -- COM-Port belegt, zum automatischen " + "Neuladen vom Board erst ausschalten)") if self.live_sync.get() else "" + raise FileNotFoundError(f"{e}{hint}") from e def _render(self): for w in self.grid_frame.winfo_children(): diff --git a/server.py b/server.py index 1090911..d184a88 100644 --- a/server.py +++ b/server.py @@ -1,7 +1,10 @@ """ VersaPad Viewer -- Browser-Variante. -Liest bei jedem Request live die 3 Config-JSONs vom Desktop und rendert -die Steuermatrix (4x5 Grid + 4 Encoder) je Profil als HTML. +Liest bei jedem Request die kombinierte Config-JSON vom Desktop und +rendert die Steuermatrix (4x5 Grid + 4 Encoder) je Profil als HTML. Fehlt +die JSON (z.B. geloescht), wird sie automatisch per Serial vom Board neu +aufgebaut und als neuer Cache gespeichert -- das Board ist die eigentliche +Quelle der Wahrheit, siehe versapad_combined.load_or_fetch(). Kein Build-Schritt, kein externes Framework -- nur stdlib. Start: python server.py [--port 8765] @@ -12,6 +15,7 @@ import webbrowser from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse, parse_qs +import versapad_combined as vcomb import versapad_data as vp PAGE_CSS = """ @@ -87,7 +91,12 @@ def render_encoder(enc): def render_page(profile): - cfg = vp.load_profile(profile) + combined = vcomb.load_or_fetch() + raw = combined["profiles"][profile] + cfg = vp.annotate_profile({ + "buttons": [dict(b) for b in raw["buttons"]], + "encoders": [dict(e) for e in raw["encoders"]], + }) tabs = "".join( f'{html.escape(vp.PROFILE_NAMES[p])}' for p in sorted(vp.PROFILE_NAMES) @@ -107,7 +116,7 @@ def render_page(profile):