VersaGUI-py/docs/data-model.md
Julian Appel 83429363c1 Add architecture/data-model/protocol reference docs
Human-facing reference documentation, split from AGENTS.md's agent-facing
domain rules and bug history (which stays there, not duplicated here):

- docs/architecture.md: layer diagram, module responsibilities, config
  storage location, the three GUI modes, and the port-exclusivity /
  multi-process caveats around concurrent access
- docs/data-model.md: the combined and legacy JSON formats, the binary
  SDeviceConfig/SDeviceProfile/SMacroTable NVM layout byte-for-byte, action
  types, LED fields, macro-slot conventions, button grid geometry
- docs/protocol.md: the 8-byte serial packet format, command/event tables,
  the read/write/status-poll flows, connection lifecycle, and error states

README.md now links to all three from a new "Dokumentation" section, and
AGENTS.md's outdated "docs/ tree isn't warranted yet" note is removed now
that it exists on explicit user request.
2026-08-14 23:19:13 +02:00

7.5 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

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 },
          "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.

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).