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.
7.5 KiB
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
0–19= MX-Button-Index (Buttoni→ Sloti) - Slot
20–31=20 + enc*3 + act_idx(Encoderenc,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).