Compare commits

..

No commits in common. "9f0ae127943412bf37ec6edb8dfaba4eb1acbd75" and "8ba524c07bb00fe2a3255e7bc0c9c49acda1c122" have entirely different histories.

9 changed files with 94 additions and 293 deletions

4
.gitignore vendored
View file

@ -3,7 +3,3 @@ __pycache__/
*.log *.log
build/ build/
dist/ 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

View file

@ -16,12 +16,6 @@ Nutzerorientierte Einführung: [`README.md`](README.md).
- `versapad_data.py` — Decoding für Anzeige: JSON laden, HID-Keycode/ - `versapad_data.py` — Decoding für Anzeige: JSON laden, HID-Keycode/
Consumer-Usage/Modifier → lesbarer Text, Grid-Geometrie Consumer-Usage/Modifier → lesbarer Text, Grid-Geometrie
(`index = spalte*5+reihe`), `hid_key_choices()`/`consumer_choices()`. (`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, - `server.py` — stdlib `http.server`, generiert HTML pro Request neu,
Profil-Wechsel über `?profile=0|1|2`, Auto-Reload alle 4s. Rein lesend, Profil-Wechsel über `?profile=0|1|2`, Auto-Reload alle 4s. Rein lesend,
kein Programmiermodus (bewusst einfach gehalten). kein Programmiermodus (bewusst einfach gehalten).
@ -45,14 +39,8 @@ Nutzerorientierte Einführung: [`README.md`](README.md).
liefert `READ_STATUS` schlicht Timeout, kein Absturz. liefert `READ_STATUS` schlicht Timeout, kein Absturz.
- `versapad_combined.py` — Ein-Datei-Format (alle 3 Profile + Makros + - `versapad_combined.py` — Ein-Datei-Format (alle 3 Profile + Makros +
**nur lokal gespeicherte** Profilnamen), Default-Pfad **nur lokal gespeicherte** Profilnamen), Default-Pfad
`versapad_data.app_dir()\versapad_config_all.json` (neben der `~\OneDrive\Desktop\versapad_config_all.json`. Passt zum Wire-Protokoll:
Installation, nicht mehr hartkodiert auf einer bestimmten Maschine). `CONFIG_BEGIN/COMMIT` überträgt ohnehin immer den kompletten 740B-Block.
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:** **UI:**
- `desktop_viewer.py` — Tkinter, flaches Design, drei unabhängige Modi - `desktop_viewer.py` — Tkinter, flaches Design, drei unabhängige Modi
@ -105,52 +93,6 @@ Dokumentation und Verifikation unten für die Größeneinschätzung).
vorhanden, und fällt sonst auf die klassischen `versapad_config{1,2,3}.json` 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 zurück — beide Ansichten müssen dieselbe Quelle zeigen, sonst wirkt eine
Bearbeitung "verschwunden". 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.
- **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 - **Bug behoben 2026-08-08:** `server.py` (`vp.load_profile()`) und
`desktop_viewer.py` (`_current_profile_view()`) lasen im Nur-Lese-Modus `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 hart von den Desktop-JSONs -- fehlten sie (z.B. User loescht sie), gab es

View file

@ -11,14 +11,10 @@ Board-Schreibzugriff) und der MCP-Server sind funktionsfähig und gegen ein
echtes Board getestet (Read-Modify-Write ist byte-identisch zum Original, echtes Board getestet (Read-Modify-Write ist byte-identisch zum Original,
inklusive CRC). Nicht vorhanden: automatisierte Tests (Verifikation läuft inklusive CRC). Nicht vorhanden: automatisierte Tests (Verifikation läuft
manuell gegen ein angeschlossenes Board), eine vorgefertigte `.exe` zum manuell gegen ein angeschlossenes Board), eine vorgefertigte `.exe` zum
Download (siehe unten, warum), und Mehrbenutzer-/Netzwerkbetrieb. Die eigene Download (siehe unten, warum), und Mehrbenutzer-/Netzwerkbetrieb. Die
Config-Datei (`versapad_config_all.json`) liegt automatisch neben der Standard-Dateipfade für die Config-JSONs sind aktuell für eine bestimmte
Installation (siehe „Aufbau" unten) und wird bei Bedarf automatisch neu Windows-Maschine hartkodiert (`versapad_data.CONFIG_PATHS`,
angelegt — kein manuelles Pfad-Anpassen mehr nötig. Nur die *optionale* `versapad_combined.DEFAULT_PATH`) — für einen anderen Rechner dort anpassen.
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 ## Features
@ -41,18 +37,15 @@ falls gewünscht.
## Voraussetzungen ## Voraussetzungen
- Python 3.11 oder neuer - Python 3.11 oder neuer
- Alle Pakete stehen in `requirements.txt` (`pyserial` fürs Board, - Pakete: `pyserial` (Board-Kommunikation), `pystray` + `pillow`
`pystray` + `pillow` fürs Tray-Icon/Desktop-Fenster, `mcp` für den (Tray-Icon/Desktop-App), optional `mcp` (nur für den MCP-Server)
MCP-Server, `pyinstaller` fürs `.exe`-Bauen):
```bash ```bash
pip install -r requirements.txt pip install pyserial pystray pillow mcp
``` ```
Für den Browser-Modus (`server.py`) reicht die Python-Standardbibliothek — Für den Browser-Modus (`server.py`) reicht die Python-Standardbibliothek —
keine zusätzlichen Pakete nötig. `build_and_deploy.ps1` installiert keine zusätzlichen Pakete nötig.
`requirements.txt` beim Bauen automatisch selbst — ein manuelles
`pip install` vorher ist dafür nicht nötig.
## Starten ## Starten
@ -72,15 +65,8 @@ Maschine/Python-Version unterschiedlich). Selbst bauen:
.\build_and_deploy.ps1 .\build_and_deploy.ps1
``` ```
Das Skript installiert/aktualisiert selbst alle nötigen Pakete aus Das Skript baut mit PyInstaller (`--onedir --windowed`, eigenes Icon) und
`requirements.txt` (kein manuelles `pip install` vorher nötig), baut dann kopiert das Ergebnis nach `%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`.
mit PyInstaller (`--onedir --windowed`, eigenes Icon) und kopiert das
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`
unterdrückt das für automatisierte Aufrufe/CI).
**Wichtig:** Sowohl Bauen als auch Ausführen müssen auf einem lokalen **Wichtig:** Sowohl Bauen als auch Ausführen müssen auf einem lokalen
Laufwerk passieren — von einem Netzlaufwerk (SMB-Share) aus scheitert Laufwerk passieren — von einem Netzlaufwerk (SMB-Share) aus scheitert
PyInstaller beim Bauen (Pfadlängen-Problem mit Tcl/Tk-Zeitzonendaten) und PyInstaller beim Bauen (Pfadlängen-Problem mit Tcl/Tk-Zeitzonendaten) und
@ -89,6 +75,8 @@ das Nachladen der Bundle-DLLs von einem Netzwerkpfad, ohne jede
Fehlermeldung). `build_and_deploy.ps1` kopiert den Quellcode deshalb Fehlermeldung). `build_and_deploy.ps1` kopiert den Quellcode deshalb
automatisch zuerst nach `%TEMP%` und baut nur dort. automatisch zuerst nach `%TEMP%` und baut nur dort.
Voraussetzung: `pip install pyinstaller` zusätzlich zu den obigen Paketen.
## Aufbau ## Aufbau
| Datei | Zweck | | Datei | Zweck |
@ -106,21 +94,11 @@ Das Binärformat (`versapad_protocol.py`) ist 1:1 aus den
VersaMCU-Firmware-Quellen übernommen und gegen ein echtes Board validiert VersaMCU-Firmware-Quellen übernommen und gegen ein echtes Board validiert
(Read → unpack → pack ist bytegenau identisch zum Original, inklusive CRC). (Read → unpack → pack ist bytegenau identisch zum Original, inklusive CRC).
Die eigene Config-Datei (`versapad_config_all.json` — alle 3 Profile + Config-Dateien liegen standardmäßig auf dem Desktop
Makros + lokale Profilnamen, siehe `versapad_combined.py`) liegt neben der (`versapad_config1/2/3.json` für den reinen Lesemodus,
Installation: bei der gebauten `.exe` im selben Ordner, beim Start aus dem `versapad_config_all.json` für den Programmiermodus — Pfade sind aktuell
Quellcode im Projektordner (`versapad_data.app_dir()`). Fehlt sie (z.B. hartkodiert für eine bestimmte Windows-Maschine, siehe `CONFIG_PATHS` in
frische Installation), wird sie automatisch angelegt — per Serial vom `versapad_data.py` bzw. `DEFAULT_PATH` in `versapad_combined.py`).
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 (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
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 ## MCP-Server

View file

@ -1,8 +1,5 @@
# Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis # Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis
# nach %LOCALAPPDATA%\VersaPadViewer (nicht $projectDir\dist wie im # nach lokal (C:\Users\<user>\AppData\Local\VersaPadViewer).
# 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 # WICHTIG: Sowohl Bauen als auch Laufen muessen lokal passieren, NICHT auf
# dem Netzlaufwerk (Z:\Git\...): # dem Netzlaufwerk (Z:\Git\...):
@ -15,87 +12,35 @@
# SMB-Share, kombiniert mit dem eh schon langen Projektpfad. # SMB-Share, kombiniert mit dem eh schon langen Projektpfad.
# #
# Deshalb: Quellcode zuerst nach lokal (%TEMP%) kopieren, dort bauen, danach # Deshalb: Quellcode zuerst nach lokal (%TEMP%) kopieren, dort bauen, danach
# das fertige Bundle nach $projectDir\dist kopieren. Z: wird nur zum Lesen # das fertige Bundle nach %LOCALAPPDATA% kopieren. Z: wird nur zum Lesen der
# der Quelldateien angefasst. # 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" $ErrorActionPreference = "Stop"
$projectDir = $PSScriptRoot $projectDir = $PSScriptRoot
$buildSrc = "$env:TEMP\versapad_build_src" $buildSrc = "$env:TEMP\versapad_build_src"
$localDir = "$env:LOCALAPPDATA\VersaPadViewer" $localDir = "$env:LOCALAPPDATA\VersaPadViewer"
$requirementsFile = "$projectDir\requirements.txt"
function Assert-LastExitCode([string]$step) { if (Test-Path $buildSrc) {
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
}
}
try {
Write-Host "Pruefe Python-Installation..."
py --version
Assert-LastExitCode "'py --version' (ist Python installiert und im PATH?)"
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)"
if (Test-Path $buildSrc) {
Remove-Item $buildSrc -Recurse -Force Remove-Item $buildSrc -Recurse -Force
} }
New-Item -ItemType Directory -Path $buildSrc | Out-Null New-Item -ItemType Directory -Path $buildSrc | Out-Null
Copy-Item "$projectDir\*.py" -Destination $buildSrc Copy-Item "$projectDir\*.py" -Destination $buildSrc
Copy-Item "$projectDir\icon.ico" -Destination $buildSrc Copy-Item "$projectDir\icon.ico" -Destination $buildSrc
Copy-Item "$projectDir\icon.png" -Destination $buildSrc Copy-Item "$projectDir\icon.png" -Destination $buildSrc
Push-Location $buildSrc Push-Location $buildSrc
try { try {
Write-Host "Baue mit PyInstaller (--onedir --windowed)..."
py -m PyInstaller --windowed --onedir --name VersaPadViewer ` py -m PyInstaller --windowed --onedir --name VersaPadViewer `
--icon icon.ico --add-data "icon.png;." --add-data "icon.ico;." ` --icon icon.ico --add-data "icon.png;." --add-data "icon.ico;." `
desktop_viewer.py --noconfirm desktop_viewer.py --noconfirm
Assert-LastExitCode "PyInstaller-Build" } finally {
} finally {
Pop-Location Pop-Location
} }
if (-not (Test-Path "$buildSrc\dist\VersaPadViewer\VersaPadViewer.exe")) { if (Test-Path $localDir) {
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 Remove-Item $localDir -Recurse -Force
} }
Copy-Item "$buildSrc\dist\VersaPadViewer" -Destination $localDir -Recurse Copy-Item "$buildSrc\dist\VersaPadViewer" -Destination $localDir -Recurse
Remove-Item $buildSrc -Recurse -Force Remove-Item $buildSrc -Recurse -Force
Write-Host "" Write-Host "Fertig: $localDir\VersaPadViewer.exe"
Write-Host "Fertig: $localDir\VersaPadViewer.exe" -ForegroundColor Green
Wait-ForKeyIfInteractive
}
catch {
Write-Host ""
Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red
Wait-ForKeyIfInteractive
exit 1
}

View file

@ -167,8 +167,8 @@ class VersaPadViewer(tk.Tk):
self.tabs = tk.Frame(self, bg=BG) self.tabs = tk.Frame(self, bg=BG)
self.tabs.pack(fill="x", padx=20, pady=(12, 10)) self.tabs.pack(fill="x", padx=20, pady=(12, 10))
self.tab_buttons = {} self.tab_buttons = {}
for p in range(vp.NUM_PROFILES): for p in sorted(vp.PROFILE_NAMES):
btn = tk.Label(self.tabs, text=f"Profil {p}", bg=CARD_BG, fg=TEXT, btn = tk.Label(self.tabs, text=vp.PROFILE_NAMES[p], bg=CARD_BG, fg=TEXT,
font=("Segoe UI", 10, "bold"), padx=14, pady=6, cursor="hand2") font=("Segoe UI", 10, "bold"), padx=14, pady=6, cursor="hand2")
btn.pack(side="left", padx=(0, 8)) btn.pack(side="left", padx=(0, 8))
btn.bind("<Button-1>", lambda e, prof=p: self.set_profile(prof, manual=True)) btn.bind("<Button-1>", lambda e, prof=p: self.set_profile(prof, manual=True))
@ -242,48 +242,20 @@ class VersaPadViewer(tk.Tk):
self._render() self._render()
def _update_tab_labels(self): 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(): for p, btn in self.tab_buttons.items():
btn.configure(text=names[p]) if self.editing.get() and self.combined:
btn.configure(text=self.combined["profile_names"][p])
else:
btn.configure(text=vp.PROFILE_NAMES[p])
def _rename_tab(self, profile): def _rename_tab(self, profile):
"""Profilname per Doppelklick auf den Tab umbenennen -- geht in if not self.editing.get() or self.combined is None:
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 return
current = self.combined["profile_names"][profile]
current = combined["profile_names"][profile]
name = simpledialog.askstring("Profil umbenennen", "Neuer Name (nur lokal, nicht aufs Board):", name = simpledialog.askstring("Profil umbenennen", "Neuer Name (nur lokal, nicht aufs Board):",
initialvalue=current, parent=self) initialvalue=current, parent=self)
if not name: if name:
return self.combined["profile_names"][profile] = name
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() self._update_tab_labels()
def _show_mcp_info(self): def _show_mcp_info(self):
@ -554,19 +526,16 @@ class VersaPadViewer(tk.Tk):
Bevorzugt die kombinierte Datei (versapad_config_all.json), falls Bevorzugt die kombinierte Datei (versapad_config_all.json), falls
vorhanden -- so zeigen im Programmiermodus gespeicherte Aenderungen vorhanden -- so zeigen im Programmiermodus gespeicherte Aenderungen
sich auch hier, statt dass die alten Einzel-JSONs weiter durchscheinen. sich auch hier, statt dass die alten Einzel-JSONs weiter durchscheinen.
Fehlt sie (z.B. versehentlich geloescht, oder frische Installation Fehlt sie (z.B. versehentlich geloescht) und ist Live-Sync gerade aus
ganz ohne Config) und ist Live-Sync gerade aus (COM-Port frei), wird (COM-Port frei), wird sie automatisch per Serial vom Board neu
sie automatisch angelegt -- per Serial vom Board, oder als leere aufgebaut und als neuer Cache gespeichert -- das Board ist die
Default-Config, wenn auch kein Board erreichbar ist (siehe eigentliche Quelle der Wahrheit, kein Datei-Handling von Hand mehr
versapad_combined.load_or_fetch()) -- das Tool ist damit auch ganz noetig. Nur wenn das nicht klappt (Board nicht erreichbar, Live-Sync
ohne vorhandene Config sofort benutzbar, kein Datei-Handling von haelt den Port), weicht es zuletzt auf die klassischen
Hand mehr noetig. Nur wenn Live-Sync gerade an ist (haelt den versapad_config{1,2,3}.json aus."""
COM-Port) und noch keine Datei existiert, weicht es zuletzt auf die if os.path.exists(vcomb.DEFAULT_PATH):
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: try:
data = vcomb.load_or_fetch(link=self._link) data = vcomb.load_file(vcomb.DEFAULT_PATH)
raw = data["profiles"][self.profile] raw = data["profiles"][self.profile]
cfg = vp.annotate_profile({ cfg = vp.annotate_profile({
"buttons": [dict(b) for b in raw["buttons"]], "buttons": [dict(b) for b in raw["buttons"]],
@ -575,6 +544,18 @@ class VersaPadViewer(tk.Tk):
return cfg, f"Quelle: {vcomb.DEFAULT_PATH}" return cfg, f"Quelle: {vcomb.DEFAULT_PATH}"
except (KeyError, IndexError, ValueError): except (KeyError, IndexError, ValueError):
pass # kaputte/unvollstaendige Datei -- weiter unten ausweichen 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: try:
return vp.load_profile(self.profile), f"Quelle: {vp.CONFIG_PATHS[self.profile]}" return vp.load_profile(self.profile), f"Quelle: {vp.CONFIG_PATHS[self.profile]}"
except FileNotFoundError as e: except FileNotFoundError as e:
@ -583,7 +564,6 @@ class VersaPadViewer(tk.Tk):
raise FileNotFoundError(f"{e}{hint}") from e raise FileNotFoundError(f"{e}{hint}") from e
def _render(self): def _render(self):
self._update_tab_labels()
for w in self.grid_frame.winfo_children(): for w in self.grid_frame.winfo_children():
w.destroy() w.destroy()
for w in self.enc_frame.winfo_children(): for w in self.enc_frame.winfo_children():

View file

@ -1,5 +0,0 @@
pyserial
pystray
pillow
mcp
pyinstaller

View file

@ -98,8 +98,8 @@ def render_page(profile):
"encoders": [dict(e) for e in raw["encoders"]], "encoders": [dict(e) for e in raw["encoders"]],
}) })
tabs = "".join( tabs = "".join(
f'<a class="tab {"active" if p == profile else ""}" href="/?profile={p}">{html.escape(combined["profile_names"][p])}</a>' f'<a class="tab {"active" if p == profile else ""}" href="/?profile={p}">{html.escape(vp.PROFILE_NAMES[p])}</a>'
for p in range(vp.NUM_PROFILES) for p in sorted(vp.PROFILE_NAMES)
) )
cells = "".join(render_cell(b) for b in cfg["buttons"]) cells = "".join(render_cell(b) for b in cfg["buttons"])
encoders = "".join(render_encoder(e) for e in cfg["encoders"]) encoders = "".join(render_encoder(e) for e in cfg["encoders"])
@ -130,7 +130,7 @@ class Handler(BaseHTTPRequestHandler):
profile = int(query.get("profile", ["0"])[0]) profile = int(query.get("profile", ["0"])[0])
except ValueError: except ValueError:
profile = 0 profile = 0
if not (0 <= profile < vp.NUM_PROFILES): if profile not in vp.PROFILE_NAMES:
profile = 0 profile = 0
try: try:
body = render_page(profile).encode("utf-8") body = render_page(profile).encode("utf-8")

View file

@ -19,7 +19,7 @@ import versapad_data as vp
import versapad_protocol as proto import versapad_protocol as proto
import versapad_serial as vs import versapad_serial as vs
DEFAULT_PATH = os.path.join(vp.app_dir(), "versapad_config_all.json") DEFAULT_PATH = os.path.expanduser(r"~\OneDrive\Desktop\versapad_config_all.json")
DEFAULT_NAMES = ["Windows", "Fusion 360", "BricsCAD"] DEFAULT_NAMES = ["Windows", "Fusion 360", "BricsCAD"]
@ -126,37 +126,18 @@ def fetch_from_board(link=None, profile_names=None):
def load_or_fetch(path=DEFAULT_PATH, 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 """Bevorzugt die lokale Kombi-Datei. Fehlt sie (z.B. versehentlich
geloescht, oder frische Installation ohne jede Config), wird sie geloescht), wird sie automatisch per Serial vom Board neu aufgebaut und
automatisch neu angelegt -- zuerst per Serial-Versuch vom Board (das als neuer Cache gespeichert, statt einen Fehler zu werfen -- das Board
behaelt die Config dauerhaft im NVM, die JSON ist nur ein Lesecache behaelt die Config dauerhaft im NVM, die Desktop-JSON ist nur ein
dafuer), und falls auch das Board nicht erreichbar ist (nicht Lesecache dafuer und muss nicht von Hand gepflegt werden. Ist das Board
verbunden, COM-Port belegt, frisch installiert ohne Board in Reichweite) nicht erreichbar (nicht verbunden, COM-Port belegt), wirft es
als leere Default-Config (vgl. default_combined()) -- damit ist das RuntimeError mit Klartext-Ursache -- Aufrufer entscheidet, ob es einen
Tool auch ganz ohne vorhandene Config sofort benutzbar, statt mit weiteren Fallback gibt (z.B. alte Einzel-JSONs)."""
einem Fehler zu blockieren."""
if os.path.exists(path): if os.path.exists(path):
return load_file(path) return load_file(path)
try:
combined = fetch_from_board(link=link, profile_names=profile_names) combined = fetch_from_board(link=link, profile_names=profile_names)
except RuntimeError:
combined = default_combined()
if profile_names:
combined["profile_names"] = profile_names
try: try:
save_file(combined, path) save_file(combined, path)
except OSError: except OSError:
pass # Daten trotzdem verwertbar, nur der Cache konnte nicht geschrieben werden pass # Board-Daten trotzdem verwertbar, nur der Cache konnte nicht geschrieben werden
return combined 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)

View file

@ -7,35 +7,19 @@ Kein Schreibzugriff auf die JSONs -- reines Lesen/Anzeigen.
""" """
import json import json
import os 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 = { CONFIG_PATHS = {
0: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config1.json"), 0: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config1.json"),
1: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config2.json"), 1: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config2.json"),
2: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config3.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) # index = spalte*5 + reihe, Reihe 0 = oben, Reihe 4 = unten (VersaMCU-Firmware-Reihenfolge)
GRID_COLS = 4 GRID_COLS = 4
GRID_ROWS = 5 GRID_ROWS = 5