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.
8.2 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 |
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
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, "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).