From b4ad7698ed902453f7e3ec9dd86928b76fda636b Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 14 Aug 2026 23:17:11 +0200 Subject: [PATCH 1/4] Move config storage next to the install and auto-create it if missing versapad_combined.DEFAULT_PATH was hardcoded to this one machine's OneDrive desktop, which made the tool unusable anywhere else. versapad_data.app_dir() now resolves to the running .exe's own folder when frozen, or the project directory when run from source, and DEFAULT_PATH hangs off that instead. load_or_fetch() previously raised when both the file was missing and the board unreachable, blocking a fresh install with no config and no board attached. It now falls back to an empty default_combined() in that case, so the tool is immediately usable either way. desktop_viewer's _current_profile_view() picks up the same fallback instead of re-implementing a narrower version of it. Also drop the hardcoded PROFILE_NAMES dict, which had drifted out of sync with the profile_names already stored in the combined JSON -- renaming a profile in Programmiermodus never showed up in the read-only/browser views. server.py and desktop_viewer.py now read names from the same JSON everywhere. versapad_data.CONFIG_PATHS (read-only interop with the official C# VersaGUI's JSON export) is intentionally left on the OneDrive desktop -- nothing in this codebase writes there, it's not part of this tool's own config. (cherry picked from commit 5d14bdd826fe2e46128b61e26ee2859961860c2d) --- .gitignore | 4 ++++ AGENTS.md | 44 ++++++++++++++++++++++++++++++++++++-- README.md | 32 +++++++++++++++++++-------- desktop_viewer.py | 51 +++++++++++++++++++++----------------------- server.py | 6 +++--- versapad_combined.py | 39 ++++++++++++++++++++++++--------- versapad_data.py | 28 ++++++++++++++++++------ 7 files changed, 147 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index 2a1de3f..6c4e83b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ __pycache__/ *.log build/ dist/ + +# Nutzer-Config -- landet neben der Installation (versapad_data.app_dir()), +# beim Start aus dem Quellcode also direkt hier im Projektordner +versapad_config_all.json diff --git a/AGENTS.md b/AGENTS.md index 0604c07..653ea34 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,12 @@ Nutzerorientierte Einführung: [`README.md`](README.md). - `versapad_data.py` — Decoding für Anzeige: JSON laden, HID-Keycode/ Consumer-Usage/Modifier → lesbarer Text, Grid-Geometrie (`index = spalte*5+reihe`), `hid_key_choices()`/`consumer_choices()`. + `app_dir()` liefert das Verzeichnis für die eigene Config + (`versapad_combined.DEFAULT_PATH`) — bei der `.exe` der Installations- + ordner, sonst der Projektordner, siehe Installierbarkeit-Notiz unten. + `CONFIG_PATHS` bleibt bewusst auf dem OneDrive-Desktop hartkodiert (Lese- + Interop mit einem JSON-Export der offiziellen VersaGUI, kein von diesem + Tool geschriebenes Format, siehe dort). - `server.py` — stdlib `http.server`, generiert HTML pro Request neu, Profil-Wechsel über `?profile=0|1|2`, Auto-Reload alle 4s. Rein lesend, kein Programmiermodus (bewusst einfach gehalten). @@ -39,8 +45,14 @@ Nutzerorientierte Einführung: [`README.md`](README.md). liefert `READ_STATUS` schlicht Timeout, kein Absturz. - `versapad_combined.py` — Ein-Datei-Format (alle 3 Profile + Makros + **nur lokal gespeicherte** Profilnamen), Default-Pfad - `~\OneDrive\Desktop\versapad_config_all.json`. Passt zum Wire-Protokoll: - `CONFIG_BEGIN/COMMIT` überträgt ohnehin immer den kompletten 740B-Block. + `versapad_data.app_dir()\versapad_config_all.json` (neben der + Installation, nicht mehr hartkodiert auf einer bestimmten Maschine). + Passt zum Wire-Protokoll: `CONFIG_BEGIN/COMMIT` überträgt ohnehin immer + den kompletten 740B-Block. `load_or_fetch()` legt bei fehlender Datei + automatisch eine neue an — erst Versuch per Serial vom Board, sonst als + leere `default_combined()` (kein Board noetig fuer die Erstbenutzung). + `read_profile_names()` liest nur die Namen (kein Board-Zugriff, fuer + Tab-Beschriftungen im Nur-Lese-Modus, siehe Installierbarkeit-Notiz). **UI:** - `desktop_viewer.py` — Tkinter, flaches Design, drei unabhängige Modi @@ -93,6 +105,34 @@ 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". +- **Installierbarkeit verbessert 2026-08-14:** Drei Probleme beim + Weitergeben an andere Leute behoben. (1) `build_and_deploy.ps1` starb bei + fehlenden Paketen (pyinstaller/pystray/pillow) kommentarlos, v.a. bei + Doppelklick im Explorer, weil das Fenster sich sofort schließt. Fix: + `requirements.txt` (alle Pakete an einer Stelle, README und Skript nutzen + dieselbe Datei), Skript installiert sie selbst per `pip install -r`, + läuft komplett in try/catch, prüft `$LASTEXITCODE` nach jedem nativen + Aufruf, und pausiert am Ende (Erfolg wie Fehler) auf Tastendruck -- + `-NoPause` für CI/Automation. (2) `versapad_combined.DEFAULT_PATH` war + hartkodiert auf `~\OneDrive\Desktop` einer bestimmten Maschine -- für + andere Nutzer unbrauchbar. Fix: `versapad_data.app_dir()` (neue + Funktion) liefert bei der gebauten `.exe` deren Installationsordner + (`sys.executable`-Verzeichnis), sonst den Projektordner (`__file__`- + Verzeichnis) -- `DEFAULT_PATH` hängt jetzt daran, landet also immer neben + der laufenden Installation. `versapad_data.CONFIG_PATHS` (Lese-Interop + mit der C#-VersaGUI) bleibt bewusst auf dem Desktop, siehe oben. (3) Fehlte + die Config UND war kein Board erreichbar, blockierte das Tool mit einer + Fehlermeldung statt zu starten. Fix: `load_or_fetch()` legt jetzt bei + Board-Fehlschlag eine leere `default_combined()` an statt `RuntimeError` + zu werfen -- Erstbenutzung ganz ohne vorhandene Config oder Board + funktioniert jetzt. (4) Profilnamen waren zusätzlich hartkodiert in + `versapad_data.PROFILE_NAMES` (Dict, jetzt entfernt, ersetzt durch + `NUM_PROFILES = 3`) und liefen der JSON-`profile_names` parallel -- + Programmiermodus zeigte umbenannte Profile, Lesemodus/Browser-Ansicht + weiterhin die alten Namen. Fix: `server.py` und `desktop_viewer.py` lesen + Namen jetzt immer aus der kombinierten JSON (`combined["profile_names"]` + bzw. `versapad_combined.read_profile_names()`), eine einzige Quelle der + Wahrheit für alle Frontends. - **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 diff --git a/README.md b/README.md index ebe6691..b165b4f 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,14 @@ Board-Schreibzugriff) und der MCP-Server sind funktionsfähig und gegen ein echtes Board getestet (Read-Modify-Write ist byte-identisch zum Original, inklusive CRC). Nicht vorhanden: automatisierte Tests (Verifikation läuft manuell gegen ein angeschlossenes Board), eine vorgefertigte `.exe` zum -Download (siehe unten, warum), und Mehrbenutzer-/Netzwerkbetrieb. Die -Standard-Dateipfade für die Config-JSONs sind aktuell für eine bestimmte -Windows-Maschine hartkodiert (`versapad_data.CONFIG_PATHS`, -`versapad_combined.DEFAULT_PATH`) — für einen anderen Rechner dort anpassen. +Download (siehe unten, warum), und Mehrbenutzer-/Netzwerkbetrieb. Die eigene +Config-Datei (`versapad_config_all.json`) liegt automatisch neben der +Installation (siehe „Aufbau" unten) und wird bei Bedarf automatisch neu +angelegt — kein manuelles Pfad-Anpassen mehr nötig. Nur die *optionale* +Lese-Interop mit den JSON-Exports der offiziellen VersaGUI +(`versapad_data.CONFIG_PATHS`) ist noch für eine bestimmte Windows-Maschine +hartkodiert (OneDrive-Desktop) — für einen anderen Rechner dort anpassen, +falls gewünscht. ## Features @@ -94,11 +98,21 @@ Das Binärformat (`versapad_protocol.py`) ist 1:1 aus den VersaMCU-Firmware-Quellen übernommen und gegen ein echtes Board validiert (Read → unpack → pack ist bytegenau identisch zum Original, inklusive CRC). -Config-Dateien liegen standardmäßig auf dem Desktop -(`versapad_config1/2/3.json` für den reinen Lesemodus, -`versapad_config_all.json` für den Programmiermodus — Pfade sind aktuell -hartkodiert für eine bestimmte Windows-Maschine, siehe `CONFIG_PATHS` in -`versapad_data.py` bzw. `DEFAULT_PATH` in `versapad_combined.py`). +Die eigene Config-Datei (`versapad_config_all.json` — alle 3 Profile + +Makros + lokale Profilnamen, siehe `versapad_combined.py`) liegt neben der +Installation: bei der gebauten `.exe` im selben Ordner, beim Start aus dem +Quellcode im Projektordner (`versapad_data.app_dir()`). Fehlt sie (z.B. +frische Installation), wird sie automatisch angelegt — per Serial vom +Board, falls eins angeschlossen ist, sonst als leere Default-Config. +Profilnamen (`profile_names`) stehen direkt in dieser Datei und lassen sich +per Doppelklick auf einen Tab (Programmiermodus) oder `rename_profile()` +(MCP) ändern. + +Die klassischen `versapad_config1/2/3.json` sind kein von diesem Tool +geschriebenes Format, sondern optionale Lese-Interop mit einem JSON-Export +der offiziellen VersaGUI (C#/.NET) — Pfad aktuell hartkodiert auf den +OneDrive-Desktop einer bestimmten Windows-Maschine, siehe `CONFIG_PATHS` in +`versapad_data.py`. ## MCP-Server diff --git a/desktop_viewer.py b/desktop_viewer.py index 926337b..54cd76f 100644 --- a/desktop_viewer.py +++ b/desktop_viewer.py @@ -167,8 +167,8 @@ class VersaPadViewer(tk.Tk): self.tabs = tk.Frame(self, bg=BG) self.tabs.pack(fill="x", padx=20, pady=(12, 10)) self.tab_buttons = {} - for p in sorted(vp.PROFILE_NAMES): - btn = tk.Label(self.tabs, text=vp.PROFILE_NAMES[p], bg=CARD_BG, fg=TEXT, + for p in range(vp.NUM_PROFILES): + btn = tk.Label(self.tabs, text=f"Profil {p}", bg=CARD_BG, fg=TEXT, font=("Segoe UI", 10, "bold"), padx=14, pady=6, cursor="hand2") btn.pack(side="left", padx=(0, 8)) btn.bind("", lambda e, prof=p: self.set_profile(prof, manual=True)) @@ -242,11 +242,16 @@ class VersaPadViewer(tk.Tk): self._render() def _update_tab_labels(self): + """Profilnamen kommen immer aus der kombinierten JSON (nicht mehr + hartkodiert) -- im Programmiermodus aus dem In-Memory-State, sonst + per schlankem Datei-Read (kein Board-Zugriff, siehe + versapad_combined.profile_names()).""" + if self.editing.get() and self.combined: + names = self.combined["profile_names"] + else: + names = vcomb.read_profile_names() for p, btn in self.tab_buttons.items(): - if self.editing.get() and self.combined: - btn.configure(text=self.combined["profile_names"][p]) - else: - btn.configure(text=vp.PROFILE_NAMES[p]) + btn.configure(text=names[p]) def _rename_tab(self, profile): if not self.editing.get() or self.combined is None: @@ -526,16 +531,19 @@ 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. - 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): + Fehlt sie (z.B. versehentlich geloescht, oder frische Installation + ganz ohne Config) und ist Live-Sync gerade aus (COM-Port frei), wird + sie automatisch angelegt -- per Serial vom Board, oder als leere + Default-Config, wenn auch kein Board erreichbar ist (siehe + versapad_combined.load_or_fetch()) -- das Tool ist damit auch ganz + ohne vorhandene Config sofort benutzbar, kein Datei-Handling von + Hand mehr noetig. Nur wenn Live-Sync gerade an ist (haelt den + COM-Port) und noch keine Datei existiert, weicht es zuletzt auf die + klassischen versapad_config{1,2,3}.json (Export der offiziellen + VersaGUI) aus.""" + if os.path.exists(vcomb.DEFAULT_PATH) or not self.live_sync.get(): try: - data = vcomb.load_file(vcomb.DEFAULT_PATH) + data = vcomb.load_or_fetch(link=self._link) raw = data["profiles"][self.profile] cfg = vp.annotate_profile({ "buttons": [dict(b) for b in raw["buttons"]], @@ -544,18 +552,6 @@ class VersaPadViewer(tk.Tk): return cfg, f"Quelle: {vcomb.DEFAULT_PATH}" except (KeyError, IndexError, ValueError): 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: @@ -564,6 +560,7 @@ class VersaPadViewer(tk.Tk): raise FileNotFoundError(f"{e}{hint}") from e def _render(self): + self._update_tab_labels() for w in self.grid_frame.winfo_children(): w.destroy() for w in self.enc_frame.winfo_children(): diff --git a/server.py b/server.py index d184a88..9188d64 100644 --- a/server.py +++ b/server.py @@ -98,8 +98,8 @@ def render_page(profile): "encoders": [dict(e) for e in raw["encoders"]], }) tabs = "".join( - f'{html.escape(vp.PROFILE_NAMES[p])}' - for p in sorted(vp.PROFILE_NAMES) + f'{html.escape(combined["profile_names"][p])}' + for p in range(vp.NUM_PROFILES) ) cells = "".join(render_cell(b) for b in cfg["buttons"]) encoders = "".join(render_encoder(e) for e in cfg["encoders"]) @@ -130,7 +130,7 @@ class Handler(BaseHTTPRequestHandler): profile = int(query.get("profile", ["0"])[0]) except ValueError: profile = 0 - if profile not in vp.PROFILE_NAMES: + if not (0 <= profile < vp.NUM_PROFILES): profile = 0 try: body = render_page(profile).encode("utf-8") diff --git a/versapad_combined.py b/versapad_combined.py index e6011ae..aad8f48 100644 --- a/versapad_combined.py +++ b/versapad_combined.py @@ -19,7 +19,7 @@ import versapad_data as vp import versapad_protocol as proto import versapad_serial as vs -DEFAULT_PATH = os.path.expanduser(r"~\OneDrive\Desktop\versapad_config_all.json") +DEFAULT_PATH = os.path.join(vp.app_dir(), "versapad_config_all.json") DEFAULT_NAMES = ["Windows", "Fusion 360", "BricsCAD"] @@ -126,18 +126,37 @@ def fetch_from_board(link=None, profile_names=None): def load_or_fetch(path=DEFAULT_PATH, link=None, profile_names=None): """Bevorzugt die lokale Kombi-Datei. Fehlt sie (z.B. versehentlich - geloescht), wird sie automatisch per Serial vom Board neu aufgebaut und - als neuer Cache gespeichert, statt einen Fehler zu werfen -- das Board - behaelt die Config dauerhaft im NVM, die Desktop-JSON ist nur ein - Lesecache dafuer und muss nicht von Hand gepflegt werden. Ist das Board - nicht erreichbar (nicht verbunden, COM-Port belegt), wirft es - RuntimeError mit Klartext-Ursache -- Aufrufer entscheidet, ob es einen - weiteren Fallback gibt (z.B. alte Einzel-JSONs).""" + geloescht, oder frische Installation ohne jede Config), wird sie + automatisch neu angelegt -- zuerst per Serial-Versuch vom Board (das + behaelt die Config dauerhaft im NVM, die JSON ist nur ein Lesecache + dafuer), und falls auch das Board nicht erreichbar ist (nicht + verbunden, COM-Port belegt, frisch installiert ohne Board in Reichweite) + als leere Default-Config (vgl. default_combined()) -- damit ist das + Tool auch ganz ohne vorhandene Config sofort benutzbar, statt mit + einem Fehler zu blockieren.""" if os.path.exists(path): return load_file(path) - combined = fetch_from_board(link=link, profile_names=profile_names) + try: + combined = fetch_from_board(link=link, profile_names=profile_names) + except RuntimeError: + combined = default_combined() + if profile_names: + combined["profile_names"] = profile_names try: save_file(combined, path) except OSError: - pass # Board-Daten trotzdem verwertbar, nur der Cache konnte nicht geschrieben werden + pass # Daten trotzdem verwertbar, nur der Cache konnte nicht geschrieben werden return combined + + +def read_profile_names(path=DEFAULT_PATH): + """Nur die (lokalen) Profilnamen lesen, ohne die volle Config zu + brauchen -- fuer Tab-Beschriftungen im Nur-Lese-Modus. Greift bewusst + nicht aufs Board zu (kein COM-Port-Konflikt mit Live-Sync), faellt bei + fehlender/kaputter Datei auf DEFAULT_NAMES zurueck.""" + if os.path.exists(path): + try: + return load_file(path).get("profile_names", list(DEFAULT_NAMES)) + except (OSError, ValueError): + pass + return list(DEFAULT_NAMES) diff --git a/versapad_data.py b/versapad_data.py index f4abcc0..0e147c6 100644 --- a/versapad_data.py +++ b/versapad_data.py @@ -7,19 +7,35 @@ Kein Schreibzugriff auf die JSONs -- reines Lesen/Anzeigen. """ import json import os +import sys +NUM_PROFILES = 3 + + +def app_dir(): + """Verzeichnis fuer die eigene Config-Datei (versapad_config_all.json, + siehe versapad_combined.DEFAULT_PATH): bei der gebauten .exe (--onedir) + das Installationsverzeichnis neben der .exe, sonst der Ordner dieses + Moduls (Projektordner beim Start aus dem Quellcode). Kein hartkodierter + Pfad mehr -- so laesst sich das Tool auf jede Maschine kopieren/ + installieren, ohne Pfade von Hand anzupassen.""" + if getattr(sys, "frozen", False): + return os.path.dirname(sys.executable) + return os.path.dirname(os.path.abspath(__file__)) + + +# CONFIG_PATHS zeigt bewusst weiterhin auf den OneDrive-Desktop -- das sind +# keine von diesem Tool geschriebenen Dateien, sondern ein Export der +# offiziellen (C#/.NET-)VersaGUI auf dieser einen Maschine (reine Lese- +# Interop, siehe README "Bekannte Einschraenkungen"). Fuer das eigentliche, +# von diesem Tool selbst gepflegte Format siehe versapad_combined.DEFAULT_PATH +# (liegt jetzt in app_dir(), nicht mehr hartkodiert auf dem Desktop). CONFIG_PATHS = { 0: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config1.json"), 1: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config2.json"), 2: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config3.json"), } -PROFILE_NAMES = { - 0: "Profil 0 – Windows", - 1: "Profil 1 – Fusion 360", - 2: "Profil 2 – BricsCAD", -} - # index = spalte*5 + reihe, Reihe 0 = oben, Reihe 4 = unten (VersaMCU-Firmware-Reihenfolge) GRID_COLS = 4 GRID_ROWS = 5 From 9e21360393322aa42d8adae8d44eeb5b494c992e Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 14 Aug 2026 23:17:38 +0200 Subject: [PATCH 2/4] Allow renaming profile tabs outside Programmiermodus Double-click-to-rename already existed but silently no-op'd unless Programmiermodus was on, and even there it only updated the in-memory self.combined without saving -- the name was lost unless some later button edit happened to trigger an autosave. Since profile_names lives purely in the combined JSON and never touches the board, there's no reason to gate it behind the heavier editing mode: it now works from any mode, loading/saving the combined file directly (auto-creating it if needed) when not already in an active Programmiermodus session, and always persists immediately. (cherry picked from commit 82c3fd005668a5e015cd9582894b2e909c4ca963) --- README.md | 4 ++-- desktop_viewer.py | 35 +++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b165b4f..3638b6f 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,8 @@ Quellcode im Projektordner (`versapad_data.app_dir()`). Fehlt sie (z.B. frische Installation), wird sie automatisch angelegt — per Serial vom Board, falls eins angeschlossen ist, sonst als leere Default-Config. Profilnamen (`profile_names`) stehen direkt in dieser Datei und lassen sich -per Doppelklick auf einen Tab (Programmiermodus) oder `rename_profile()` -(MCP) ändern. +per Doppelklick auf einen Tab (in jedem Modus — Nur-Lesen, Live-Sync oder +Programmiermodus) oder `rename_profile()` (MCP) ändern. Die klassischen `versapad_config1/2/3.json` sind kein von diesem Tool geschriebenes Format, sondern optionale Lese-Interop mit einem JSON-Export diff --git a/desktop_viewer.py b/desktop_viewer.py index 54cd76f..81b1805 100644 --- a/desktop_viewer.py +++ b/desktop_viewer.py @@ -254,14 +254,37 @@ class VersaPadViewer(tk.Tk): btn.configure(text=names[p]) def _rename_tab(self, profile): - if not self.editing.get() or self.combined is None: - return - current = self.combined["profile_names"][profile] + """Profilname per Doppelklick auf den Tab umbenennen -- geht in + jedem Modus (rein lokal, landet nie aufs Board, siehe Kritische + Domänenregeln). Im Programmiermodus wird der bereits geladene + In-Memory-State direkt bearbeitet + autosaved; sonst wird die + kombinierte Datei frisch gelesen/geschrieben (legt sie bei Bedarf + automatisch an, siehe versapad_combined.load_or_fetch()).""" + if self.editing.get() and self.combined is not None: + combined = self.combined + else: + try: + combined = vcomb.load_or_fetch() + except (OSError, ValueError, KeyError) as e: + messagebox.showerror("Umbenennen fehlgeschlagen", str(e)) + return + + current = combined["profile_names"][profile] name = simpledialog.askstring("Profil umbenennen", "Neuer Name (nur lokal, nicht aufs Board):", initialvalue=current, parent=self) - if name: - self.combined["profile_names"][profile] = name - self._update_tab_labels() + if not name: + return + combined["profile_names"][profile] = name + + if combined is self.combined: + self._autosave_combined() + else: + try: + vcomb.save_file(combined, vcomb.DEFAULT_PATH) + except OSError as e: + messagebox.showerror("Umbenennen fehlgeschlagen", f"Speichern fehlgeschlagen: {e}") + return + self._update_tab_labels() def _show_mcp_info(self): win = tk.Toplevel(self) From 02d6c0c5e94a25f4f1443a7d98c5fe88b68dd776 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 14 Aug 2026 23:15:25 +0200 Subject: [PATCH 3/4] Make build_and_deploy.ps1 self-installing and fail loudly pip install of pyinstaller/pystray/pillow was a separate manual step the README only mentioned in prose, so the script silently died on missing packages -- especially bad on Explorer double-click, where the window closes before any error is visible. Consolidate all dependencies into requirements.txt (also used by README's plain "pip install -r" flow), have the script install it itself, wrap the whole build in try/catch with exit-code checks after every native call, and pause on both success and failure unless -NoPause is passed. Also move the build output from %LOCALAPPDATA% into dist/ next to the script, so it's easy to find and matches the already-gitignored dist/ entry. (cherry picked from commit 13c37454797fe825ffe6d12c64e2b32f2d0f676d) --- README.md | 24 +++++++---- build_and_deploy.ps1 | 98 +++++++++++++++++++++++++++++++++----------- requirements.txt | 5 +++ 3 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 requirements.txt diff --git a/README.md b/README.md index 3638b6f..7a06535 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,18 @@ falls gewünscht. ## Voraussetzungen - Python 3.11 oder neuer -- Pakete: `pyserial` (Board-Kommunikation), `pystray` + `pillow` - (Tray-Icon/Desktop-App), optional `mcp` (nur für den MCP-Server) +- Alle Pakete stehen in `requirements.txt` (`pyserial` fürs Board, + `pystray` + `pillow` fürs Tray-Icon/Desktop-Fenster, `mcp` für den + MCP-Server, `pyinstaller` fürs `.exe`-Bauen): ```bash -pip install pyserial pystray pillow mcp +pip install -r requirements.txt ``` Für den Browser-Modus (`server.py`) reicht die Python-Standardbibliothek — -keine zusätzlichen Pakete nötig. +keine zusätzlichen Pakete nötig. `build_and_deploy.ps1` installiert +`requirements.txt` beim Bauen automatisch selbst — ein manuelles +`pip install` vorher ist dafür nicht nötig. ## Starten @@ -69,8 +72,15 @@ Maschine/Python-Version unterschiedlich). Selbst bauen: .\build_and_deploy.ps1 ``` -Das Skript baut mit PyInstaller (`--onedir --windowed`, eigenes Icon) und -kopiert das Ergebnis nach `%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`. +Das Skript installiert/aktualisiert selbst alle nötigen Pakete aus +`requirements.txt` (kein manuelles `pip install` vorher nötig), baut dann +mit PyInstaller (`--onedir --windowed`, eigenes Icon) und kopiert das +Ergebnis nach `dist\VersaPadViewer\VersaPadViewer.exe` im Projektordner. +Bricht ein Schritt ab (fehlendes Python, PyInstaller-Fehler, ...), zeigt das +Skript eine klare Fehlermeldung und wartet auf einen Tastendruck, statt sich +bei Doppelklick im Explorer kommentarlos zu schließen (`-NoPause` +unterdrückt das für automatisierte Aufrufe/CI). + **Wichtig:** Sowohl Bauen als auch Ausführen müssen auf einem lokalen Laufwerk passieren — von einem Netzlaufwerk (SMB-Share) aus scheitert PyInstaller beim Bauen (Pfadlängen-Problem mit Tcl/Tk-Zeitzonendaten) und @@ -79,8 +89,6 @@ das Nachladen der Bundle-DLLs von einem Netzwerkpfad, ohne jede Fehlermeldung). `build_and_deploy.ps1` kopiert den Quellcode deshalb automatisch zuerst nach `%TEMP%` und baut nur dort. -Voraussetzung: `pip install pyinstaller` zusätzlich zu den obigen Paketen. - ## Aufbau | Datei | Zweck | diff --git a/build_and_deploy.ps1 b/build_and_deploy.ps1 index 8243798..e0617e0 100644 --- a/build_and_deploy.ps1 +++ b/build_and_deploy.ps1 @@ -1,5 +1,5 @@ # Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis -# nach lokal (C:\Users\\AppData\Local\VersaPadViewer). +# nach $projectDir\dist\VersaPadViewer. # # WICHTIG: Sowohl Bauen als auch Laufen muessen lokal passieren, NICHT auf # dem Netzlaufwerk (Z:\Git\...): @@ -12,35 +12,87 @@ # SMB-Share, kombiniert mit dem eh schon langen Projektpfad. # # Deshalb: Quellcode zuerst nach lokal (%TEMP%) kopieren, dort bauen, danach -# das fertige Bundle nach %LOCALAPPDATA% kopieren. Z: wird nur zum Lesen der -# Quelldateien angefasst. +# das fertige Bundle nach $projectDir\dist kopieren. Z: wird nur zum Lesen +# der Quelldateien angefasst. +# +# Fehlerverhalten: Bei Doppelklick im Explorer schliesst sich das Fenster +# sofort nach Skriptende -- ohne Pause waeren Fehlermeldungen unsichtbar +# ("stirbt ohne jede Fehlermeldung"). Deshalb: alles in try/catch, im +# Fehlerfall UND am Ende eine Pause, ausser bei -NoPause (fuer CI/Automation). + +param( + [switch]$NoPause +) $ErrorActionPreference = "Stop" $projectDir = $PSScriptRoot $buildSrc = "$env:TEMP\versapad_build_src" -$localDir = "$env:LOCALAPPDATA\VersaPadViewer" +$localDir = "$projectDir\dist\VersaPadViewer" +$requirementsFile = "$projectDir\requirements.txt" -if (Test-Path $buildSrc) { - Remove-Item $buildSrc -Recurse -Force +function Assert-LastExitCode([string]$step) { + if ($LASTEXITCODE -ne 0) { + throw "$step ist fehlgeschlagen (Exit-Code $LASTEXITCODE) -- Ausgabe oben pruefen." + } +} + +function Wait-ForKeyIfInteractive { + if ($NoPause) { return } + try { + Read-Host "Taste druecken zum Schliessen" + } catch { + # z.B. nicht-interaktiver Aufruf (stdin nicht verfuegbar) -- einfach ignorieren + } } -New-Item -ItemType Directory -Path $buildSrc | Out-Null -Copy-Item "$projectDir\*.py" -Destination $buildSrc -Copy-Item "$projectDir\icon.ico" -Destination $buildSrc -Copy-Item "$projectDir\icon.png" -Destination $buildSrc -Push-Location $buildSrc try { - py -m PyInstaller --windowed --onedir --name VersaPadViewer ` - --icon icon.ico --add-data "icon.png;." --add-data "icon.ico;." ` - desktop_viewer.py --noconfirm -} finally { - Pop-Location -} + Write-Host "Pruefe Python-Installation..." + py --version + Assert-LastExitCode "'py --version' (ist Python installiert und im PATH?)" -if (Test-Path $localDir) { - Remove-Item $localDir -Recurse -Force -} -Copy-Item "$buildSrc\dist\VersaPadViewer" -Destination $localDir -Recurse -Remove-Item $buildSrc -Recurse -Force + if (-not (Test-Path $requirementsFile)) { + throw "requirements.txt nicht gefunden unter $requirementsFile" + } + Write-Host "Installiere/aktualisiere benoetigte Pakete aus requirements.txt..." + py -m pip install --quiet --disable-pip-version-check -r $requirementsFile + Assert-LastExitCode "Paketinstallation (pip install -r requirements.txt)" -Write-Host "Fertig: $localDir\VersaPadViewer.exe" + if (Test-Path $buildSrc) { + Remove-Item $buildSrc -Recurse -Force + } + New-Item -ItemType Directory -Path $buildSrc | Out-Null + Copy-Item "$projectDir\*.py" -Destination $buildSrc + Copy-Item "$projectDir\icon.ico" -Destination $buildSrc + Copy-Item "$projectDir\icon.png" -Destination $buildSrc + + Push-Location $buildSrc + try { + Write-Host "Baue mit PyInstaller (--onedir --windowed)..." + py -m PyInstaller --windowed --onedir --name VersaPadViewer ` + --icon icon.ico --add-data "icon.png;." --add-data "icon.ico;." ` + desktop_viewer.py --noconfirm + Assert-LastExitCode "PyInstaller-Build" + } finally { + Pop-Location + } + + if (-not (Test-Path "$buildSrc\dist\VersaPadViewer\VersaPadViewer.exe")) { + throw "PyInstaller hat keine VersaPadViewer.exe erzeugt, obwohl der Exit-Code 0 war -- Build-Ausgabe oben pruefen." + } + + if (Test-Path $localDir) { + Remove-Item $localDir -Recurse -Force + } + Copy-Item "$buildSrc\dist\VersaPadViewer" -Destination $localDir -Recurse + Remove-Item $buildSrc -Recurse -Force + + Write-Host "" + Write-Host "Fertig: $localDir\VersaPadViewer.exe" -ForegroundColor Green + Wait-ForKeyIfInteractive +} +catch { + Write-Host "" + Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red + Wait-ForKeyIfInteractive + exit 1 +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ed0027c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pyserial +pystray +pillow +mcp +pyinstaller From 9f0ae127943412bf37ec6edb8dfaba4eb1acbd75 Mon Sep 17 00:00:00 2001 From: cjjohn <72096478+Grovy311@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:55:41 +0200 Subject: [PATCH 4/4] Keep build_and_deploy.ps1 deploying to %LOCALAPPDATA%, document upstream cherry-picks Cherry-picked commits from Julian Appel's dev/jappel branch changed the default deploy target to $projectDir\dist\VersaPadViewer. Revert just that to %LOCALAPPDATA%\VersaPadViewer, which is where the actually installed/running instance on this machine lives -- switching would orphan the existing install and any shortcuts pointing at it. Document the cherry-pick provenance and what was deliberately left out (.mcp.json's hardcoded path, the docs/ tree) in AGENTS.md. --- AGENTS.md | 18 ++++++++++++++++++ README.md | 2 +- build_and_deploy.ps1 | 7 +++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 653ea34..fcc3487 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -133,6 +133,24 @@ Dokumentation und Verifikation unten für die Größeneinschätzung). Namen jetzt immer aus der kombinierten JSON (`combined["profile_names"]` bzw. `versapad_combined.read_profile_names()`), eine einzige Quelle der Wahrheit für alle Frontends. +- **Herkunft der drei Punkte oben + Tab-Umbenennen-Fix:** per Cherry-Pick aus + Julian Appels eigenem `dev/jappel`-Branch (`git.jappel.io/jappel/ + VersaGUI-py`) übernommen, der sich zeitgleich mit unserem eigenen + COM-Port-Fix entwickelt hat (kein Fork-Sync-Automatismus -- manuell + gegengelesen und übernommen, siehe Sessionlog). **Eine Abweichung vom + Original:** dort landet der Build jetzt in `$projectDir\dist\ + VersaPadViewer` statt `%LOCALAPPDATA%\VersaPadViewer` -- hier bewusst + NICHT übernommen, weil das die tatsächlich installierte/genutzte Instanz + wäre und bestehende Verknüpfungen sonst ins Leere zeigen würden. + `build_and_deploy.ps1` hier weiterhin `%LOCALAPPDATA%\VersaPadViewer`. + Bestehende `versapad_config_all.json` vom alten Desktop-Pfad wurde einmalig + nach `%LOCALAPPDATA%\VersaPadViewer\` migriert (kopiert, Original bleibt). + Nicht übernommen: die `.mcp.json`-Registrierung (hartkodierter Pfad auf + Julians Maschine, `C:\Users\Julian\...`) -- dafür stattdessen ein Issue in + seinem Repo (git.jappel.io/jappel/VersaGUI-py) angelegt, damit relative/ + portable Pfadauflösung dort nachgezogen werden kann; die drei + `docs/*.md`-Referenzdateien (unser eigener Dokumentationsstandard hält für + dieses Tool bewusst bei README + AGENTS.md ohne vollen `docs/`-Baum). - **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 diff --git a/README.md b/README.md index 7a06535..9de3204 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Maschine/Python-Version unterschiedlich). Selbst bauen: Das Skript installiert/aktualisiert selbst alle nötigen Pakete aus `requirements.txt` (kein manuelles `pip install` vorher nötig), baut dann mit PyInstaller (`--onedir --windowed`, eigenes Icon) und kopiert das -Ergebnis nach `dist\VersaPadViewer\VersaPadViewer.exe` im Projektordner. +Ergebnis nach `%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`. Bricht ein Schritt ab (fehlendes Python, PyInstaller-Fehler, ...), zeigt das Skript eine klare Fehlermeldung und wartet auf einen Tastendruck, statt sich bei Doppelklick im Explorer kommentarlos zu schließen (`-NoPause` diff --git a/build_and_deploy.ps1 b/build_and_deploy.ps1 index e0617e0..bdf1608 100644 --- a/build_and_deploy.ps1 +++ b/build_and_deploy.ps1 @@ -1,5 +1,8 @@ # Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis -# nach $projectDir\dist\VersaPadViewer. +# nach %LOCALAPPDATA%\VersaPadViewer (nicht $projectDir\dist wie im +# jappel-Upstream -- hier bereits die tatsaechlich installierte/genutzte +# Instanz, siehe Speicher-Notiz "VersaPad Viewer Tool"; ein Pfadwechsel +# wuerde bestehende Verknuepfungen/Autostart-Eintraege brechen). # # WICHTIG: Sowohl Bauen als auch Laufen muessen lokal passieren, NICHT auf # dem Netzlaufwerk (Z:\Git\...): @@ -27,7 +30,7 @@ param( $ErrorActionPreference = "Stop" $projectDir = $PSScriptRoot $buildSrc = "$env:TEMP\versapad_build_src" -$localDir = "$projectDir\dist\VersaPadViewer" +$localDir = "$env:LOCALAPPDATA\VersaPadViewer" $requirementsFile = "$projectDir\requirements.txt" function Assert-LastExitCode([string]$step) {