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.
This commit is contained in:
Julian Appel 2026-08-14 23:17:11 +02:00
parent 13c3745479
commit 5d14bdd826
7 changed files with 147 additions and 57 deletions

View file

@ -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("<Button-1>", 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():