VersaGUI-py/docs/data-model.md
Julian Appel 189ec01e68 Merge branch 'main' into dev/jappel
Reconciles two independent lines of work: main cherry-picked and then
extended dev/jappel's build-script/config-portability/rename-tab/
COM-port fixes, additionally fixing a config-loss bug (rebuild wiped
the config when it lived in the install dir -- moved to roaming
%APPDATA% instead) and adding free-text notes per action plus a
frameless resizable window. dev/jappel keeps its .mcp.json registration
and docs/ reference tree, which main deliberately left out.

Conflict resolutions favored main's versions where the two sides solved
the same problem (config location, build deploy target, COM-port
release, tab rename) since main's fixes were validated against a real
rebuild-wipes-config incident. docs/architecture.md and
docs/data-model.md updated to describe the resulting %APPDATA% config
path and the note field.
2026-08-15 18:16:00 +02:00

8.2 KiB
Raw Blame History

Datenmodell

Alle Formate, die VersaPad Viewer liest/schreibt: die JSON-Repräsentationen und das binäre NVM-Layout des Boards. Das binäre Layout 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) — siehe versapad_protocol.py und die „Existing-Codebase-Regel“ in AGENTS.md, bevor hier etwas geändert wird.

Geometrie

index = spalte * 5 + reihe

4 Spalten (GRID_COLS), 5 Reihen (GRID_ROWS), Reihe 0 = oben, Reihe 4 = unten. Firmware-Reihenfolge, nicht neu herleiten. Damit ergeben sich die 20 Button-Indizes so auf dem physischen Grid:

Spalte 0 Spalte 1 Spalte 2 Spalte 3
Reihe 0 (oben) 0 5 10 15
Reihe 1 1 6 11 16
Reihe 2 2 7 12 17
Reihe 3 3 8 13 18
Reihe 4 (unten) 4 9 14 19

Action

Eine Action beschreibt, was ein Button oder eine Encoder-Bewegung auslöst. Sowohl in JSON als auch binär ein {type, data}-Paar (binär: SAction, 3 Byte — 1 Byte Typ + 2 Byte data, little-endian).

type Enum-Index data-Bedeutung
None 0 ungenutzt (0)
HidKey 1 data = keycode | (modifier << 8) — Keycode HID Usage Page 0x07 im unteren Byte, Modifier-Bitmaske im oberen Byte
HidConsumer 2 data = HID-Consumer-Usage-ID (Usage Page 0x0C), z.B. 0x00CD = Play/Pause
HostCommand 3 Enum-Wert existiert in der Firmware, wird von diesem Tool aktuell nicht gesetzt/editiert (kein set_button_hostcommand-Äquivalent)
Macro 4 data = Makro-Slot-Index (0-31)
ProfileSwitch 5 data = Ziel-Profil (0/1/2) oder 0xFFFF/0x00FF = „nächstes Profil“ (Zyklus)

Modifier-Bitmaske (für HidKey, gilt nicht 1:1 für Makro-Schritte, siehe unten):

Bit Modifier
0x01 Strg
0x02 Shift
0x04 Alt
0x08 Win

Jede Action trägt zusätzlich ein optionales note-Feld (freier Text, z.B. "Speichern in Fusion 360") — rein lokal, wie profile_names (siehe unten): kein Platz dafür in SAction (3 Byte, komplett verplant), to_binary()/pack_config() ignorieren das Feld beim Schreiben ans Board, from_binary() liefert frisch vom Board immer note="". versapad_combined.merge_notes(neu, alt) kopiert bestehende Notizen nach jedem load_from_board()/fetch_from_board() zurück, sonst gingen sie bei jedem Board-Refresh verloren. Editierbar per Programmiermodus-Dialog oder MCP (set_button_note()/set_encoder_note()).

LED

Pro MX-Button (nicht pro Encoder — Encoder haben keine eigene LED):

Feld Typ Bedeutung
r, g, b uint8 (0-255) Farbe
brightness uint8 (0-255) Helligkeit
anim Enum-String (JSON) / Enum-Index (binär) Static, Blink, Pulse, FadeIn, FadeOut, ColorCycle, ColorFade
period_ms uint16 (little-endian) Animationsperiode in ms (Pulse braucht >= 2)

Makro-Schritt

Ein Makro-Schritt ist kein Action — Keycode und Modifier stehen in zwei getrennten Bytes (nicht in einem gepackten 16-Bit-data-Feld wie bei HidKey):

Feld Typ Bedeutung
keycode uint8 HID-Keycode. 0 beendet die Sequenz (Firmware-Konvention — keine Lücken vor dem letzten belegten Schritt lassen)
modifier uint8 Bitmaske, aber nur Strg/Shift/Alt (kein Win — passend zu ActionDialog.cs im Original)

Eine Makro-Tabelle hat 32 Slots (MACRO_SLOTS) mit je bis zu 8 Schritten (MACRO_MAX_STEPS). Sie ist eine einzige globale Tabelle, nicht pro Profil — zwei Profile, die per Macro-Action denselben Slot referenzieren, spielen dieselben Schritte ab. Konvention für die Slot-Zuordnung (von den Tools/der GUI benutzt, nicht von der Firmware erzwungen):

  • Slot 019 = MX-Button-Index (Button i → Slot i)
  • Slot 2031 = 20 + enc*3 + act_idx (Encoder enc, act_idx: 0=Druck/sw, 1=cw, 2=ccw)

JSON: kombiniertes Format (versapad_config_all.json)

Das von diesem Tool selbst gepflegte Format (versapad_combined.py) — alle 3 Profile + Makro-Tabelle + lokale Profilnamen in einer Datei. Passt zum Wire-Protokoll: CONFIG_BEGIN/COMMIT überträgt ohnehin immer den kompletten 740B-Block, nie nur ein Profil.

{
  "active_profile": 0,
  "global_brightness": 255,
  "enc_sensitivity": [1, 1, 1, 1],
  "profile_names": ["Windows", "Fusion 360", "BricsCAD"],
  "profiles": [
    {
      "buttons": [
        {
          "index": 0,
          "action": { "type": "HidKey", "data": 30, "note": "Speichern in Fusion 360" },
          "led": { "r": 80, "g": 40, "b": 0, "brightness": 255,
                    "anim": "Static", "period_ms": 4000 }
        }
        // ... 20 Buttons (index 0-19)
      ],
      "encoders": [
        {
          "index": 0,
          "sw": { "type": "ProfileSwitch", "data": 65535 },
          "cw": { "type": "None", "data": 0 },
          "ccw": { "type": "None", "data": 0 }
        }
        // ... 4 Encoder (index 0-3)
      ]
    }
    // ... 3 Profile
  ],
  "macros": [
    [{ "keycode": 30, "modifier": 0 }, { "keycode": 39, "modifier": 0 }]
    // ... 32 Slots, jeweils eine Liste mit 0-8 Schritten
  ]
}

Profilnamen (profile_names) sind rein lokal — die Firmware-Structs haben keinen Platz für einen String (Header exakt 32B, jedes Profil exakt 236B, alles verplant), sie landen nie aufs Board, egal welcher Schreibpfad benutzt wird. Dasselbe gilt für note in jeder Action (siehe oben).

JSON: Legacy-Einzeldatei-Format (versapad_config1/2/3.json)

Kein von diesem Tool geschriebenes Format — optionaler Export der offiziellen VersaGUI, gelesen von versapad_data.load_profile(). Enthält nur ein einzelnes Profil, keine Makro-Schritte, keinen Profilnamen:

{
  "buttons": [ /* wie oben, 20 Eintraege */ ],
  "encoders": [ /* wie oben, 4 Eintraege */ ]
}

MCP-Tool-Grenzfläche (set_macro, set_button_key, …)

Die MCP-Tools nehmen menschenlesbare Namen entgegen, keine Rohwerte — versapad_data.py übersetzt:

// set_button_key(profile=0, index=0, key="S", modifiers=["Strg"])
// set_macro(slot=7, steps=[{"key": "1", "modifiers": []}, {"key": "0", "modifiers": []}])

hid_key_code_for_name() / consumer_id_for_name() / modifier_bits_for_names() übersetzen Namen → Rohwerte (werfen ValueError mit einer Liste gültiger Namen bei Tippfehlern). Zeichentasten-Labels sind eine US-Layout-Näherung (keine GetKeyNameText()-Auflösung wie im C#-Original).

Binäres NVM-Layout

SDeviceConfig (740 Byte, versapad_protocol.CONFIG_SIZE)

Offset Größe Feld
0 4B (uint32 LE) Magic (0x56503203, NVM_CONFIG_MAGIC)
4 1B Version (3, NVM_CONFIG_VERSION)
5 2B (uint16 LE) CRC16 über Byte 7-739
7 1B active_profile (0-2)
8 1B global_brightness (0-255)
9 4B enc_sensitivity (4× uint8, einer je Encoder)
13 19B reserviert/ungenutzt (Padding)
32 236B Profil 0 (SDeviceProfile)
268 236B Profil 1
504 236B Profil 2

SDeviceProfile (236 Byte)

Offset (relativ) Größe Feld
0 60B (20× 3B SAction) MX-Button-Actions, Index 0-19
60 36B (4× 3× 3B SAction) Encoder-Actions: je Encoder sw, cw, ccw
96 20B LED r je Button
116 20B LED g je Button
136 20B LED b je Button
156 20B LED brightness je Button
176 20B LED anim (Enum-Index) je Button
196 40B (20× uint16 LE) LED period_ms je Button

Die LED-Felder liegen spaltenweise (struct-of-arrays: alle 20 r-Werte, dann alle 20 g-Werte, …), nicht verschachtelt pro Button — pack_profile()/unpack_profile() bauen das entsprechend um.

SMacroTable (512 Byte, versapad_protocol.MACRO_SIZE)

32 Slots × 8 Schritte × 2 Byte (keycode, modifier) = 512 Byte, flach hintereinander: offset = (slot_idx * 8 + step_idx) * 2.

CRC16

crc16() in versapad_protocol.py: CRC-CCITT, Polynom 0x1021, Init 0xFFFF, MSB-first, kein XOR-Out — exakt nvm_config_crc() aus der Firmware (nvm_config.cpp). Wird über Byte 7-739 der Config berechnet (alles nach Magic/Version/CRC-Header selbst).