From 0d6ab2173396722cefc27e91115571915eeba90d Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Mon, 30 Mar 2026 19:51:38 +0200 Subject: [PATCH] Added doc --- doc/00_architecture.md | 69 ++++++++++++++++++++++++++ doc/01_serial_manager.md | 76 ++++++++++++++++++++++++++++ doc/02_device_config.md | 104 +++++++++++++++++++++++++++++++++++++++ doc/03_tray_app.md | 62 +++++++++++++++++++++++ doc/04_config_form.md | 75 ++++++++++++++++++++++++++++ doc/05_action_dialog.md | 91 ++++++++++++++++++++++++++++++++++ doc/06_config_json.md | 48 ++++++++++++++++++ doc/INDEX.md | 25 ++++++++++ 8 files changed, 550 insertions(+) create mode 100644 doc/00_architecture.md create mode 100644 doc/01_serial_manager.md create mode 100644 doc/02_device_config.md create mode 100644 doc/03_tray_app.md create mode 100644 doc/04_config_form.md create mode 100644 doc/05_action_dialog.md create mode 100644 doc/06_config_json.md create mode 100644 doc/INDEX.md diff --git a/doc/00_architecture.md b/doc/00_architecture.md new file mode 100644 index 0000000..e3cff66 --- /dev/null +++ b/doc/00_architecture.md @@ -0,0 +1,69 @@ +# VersaGUI – Architektur-Übersicht + +## Technologie-Stack + +| Merkmal | Wert | +|---|---| +| Sprache | C# / .NET 7 | +| UI-Framework | WinForms (`[STAThread]`) | +| Einstiegspunkt | `Program.cs` → `Application.Run(new TrayApp())` | +| Laufzeitmodell | `ApplicationContext` (kein `Form` als Hauptfenster) | +| Threading | UI-Thread + 1 Hintergrund-Lese-Thread + Timer-Thread | + +## Komponentenübersicht + +| Datei | Verantwortung | +|---|---| +| `TrayApp` | `ApplicationContext`; hält Tray-Icon, öffnet ConfigForm, verarbeitet Board-Events | +| `SerialManager` | Verbindungsverwaltung, WMI-Erkennung, Lese-Thread, Sende-Methoden | +| `ConfigForm` | Hauptfenster (Grid + Encoder-Panel + Footer); öffnet ActionDialog | +| `ActionDialog` | Modaler Dialog zum Bearbeiten einer Aktion + LED-Einstellungen | +| `DeviceConfig` | C#-Spiegel von `SDeviceConfig`; Serialisierung/Deserialisierung (223 B) | +| `MacroTable` | C#-Spiegel von `SMacroTable`; Serialisierung/Deserialisierung (256 B) | +| `ConfigJson` | JSON-Import/Export für `DeviceConfig` | +| `Protocol` | Konstanten für alle Command/Event-IDs (spiegelt `usb_serial.h`) | + +## Datenfluss + +``` +Board → SerialManager (ReadLoop, BG-Thread) + → SynchronizationContext.Post (→ UI-Thread) + → TrayApp.OnPacket() + ├── Config-Dump: _rxConfigBuf aufbauen → DeviceConfig.FromBytes() + ├── Makro-Dump: _rxMacroBuf aufbauen → MacroTable.FromBytes() + └── HOST_COMMAND-Events: (TODO: Aktion ausführen) + +Benutzer → ConfigForm → ActionDialog + → DeviceConfig / MacroTable (in-memory ändern) + → SerialManager.SendConfig() + SendMacros() (BG-Task) + → Board (chunked, 6 B/Paket) +``` + +## Threading-Modell + +``` +UI-Thread : TrayApp, ConfigForm, ActionDialog, alle WinForms-Controls +BG-Thread : SerialManager.ReadLoop() – blockiert auf ReadByte() +Timer-Thread : SerialManager._reconnectTimer → TryConnect() alle 3 s +Sende-Task : ConfigForm.OnSave() → Task.Run() (blockiert ~400 ms für Transfer) +``` + +Alle Board-Events werden per `SynchronizationContext.Post` auf den UI-Thread gepostet. Controls dürfen nie vom BG-Thread angefasst werden. + +## Verbindungslebenszyklus + +``` +Start → Timer feuert → TryConnect() → WMI-Suche (VID 0x239A / PID 0x0042) + → SerialPort öffnen (DtrEnable=true VOR Open()!) + → 200 ms warten → ReadLoop starten → Connected-Event → RequestConfig() + RequestMacros() + +Disconnect → ReadLoop bricht ab → Disconnected-Event → 5 s Backoff → Timer läuft weiter +``` + +## Invarianten / Constraints + +- **DtrEnable=true muss VOR `Open()` gesetzt werden**: SAMD21 prüft DTR für `usb_serial_send()`. Der Default-Wert false würde alle Board→PC-Antworten still verwerfen. +- **IOException ≠ Disconnect**: .NET 7 wirft `IOException` statt `TimeoutException` bei `ReadByte()`-Timeout. Nur als echten Fehler behandeln wenn `_port.IsOpen == false`. +- **Packed-kompatible Serialisierung**: `DeviceConfig.ToBytes()` muss exakt 223 B in derselben Reihenfolge wie `SDeviceConfig` (packed C++) erzeugen. Jede Änderung am Firmware-Layout muss hier gespiegelt werden. +- **Config-Version**: `DeviceConfig.Version == 2`. `FromBytes()` prüft Magic + Version + CRC; schlägt einer fehl → Methode gibt `false` zurück, Config bleibt unverändert. +- **Debug-Log**: `versapad_rx.txt` in `%TEMP%` – vor Release-Nutzung entfernen (TODO). diff --git a/doc/01_serial_manager.md b/doc/01_serial_manager.md new file mode 100644 index 0000000..97f7eae --- /dev/null +++ b/doc/01_serial_manager.md @@ -0,0 +1,76 @@ +# SerialManager + +**Datei:** `SerialManager.cs` + +## Verantwortung + +- COM-Port-Erkennung per WMI (VID/PID) +- Verbindungsaufbau und automatischer Reconnect +- Hintergrund-Lese-Thread mit Paket-Assembler +- Sende-Methoden für alle Protokoll-Operationen + +## COM-Port-Erkennung + +```csharp +VidPid = "VID_239A&PID_0042" // muss mit platformio.ini übereinstimmen + +SELECT Name, DeviceID FROM Win32_PnPEntity WHERE DeviceID LIKE '%VID_239A&PID_0042%' +// Name enthält z.B. "USB Serial Device (COM3)" → extrahiert "COM3" +``` + +Erkennt das Board auch bei wechselnder COM-Nummer. Funktioniert auch nach USB-Reinitialisierung (Kabel abziehen/stecken nach SWD-Flash). + +## Verbindungsaufbau (TryConnect) + +``` +Monitor.TryEnter(_connectLock) – nur ein gleichzeitiger Versuch +FindPort() per WMI +SerialPort erstellen: + ReadTimeout = 500 ms + WriteTimeout = 500 ms + DtrEnable = true ← VOR Open() setzen! (SAMD21 CDC-Constraint) +port.Open() +Thread.Sleep(200) – CDC stabilisieren +ReadLoop-Thread starten +Connected-Event → UI-Thread +``` + +**Reconnect-Timer**: alle 3 s, Start nach 500 ms. 5 s Backoff (`_waitingAfterDisconnect`) nach Verbindungsverlust damit Board vollständig re-enumerieren kann. + +## ReadLoop (Hintergrund-Thread) + +``` +while (port.IsOpen): + b = port.ReadByte() – blockiert 500 ms (ReadTimeout) + buf[bufFill++] = b + if bufFill < 8: continue + → SerialPacket fertig → SynchronizationContext.Post → PacketReceived auf UI-Thread + bufFill = 0 + +Bei IOException: + if port.IsOpen: ignorieren ← .NET 7: IOException = normaler Timeout (kein Fehler!) + else: break → Disconnect +``` + +## Sende-Methoden + +| Methode | Funktion | +|---|---| +| `Send(pkt)` | Rohe 8-Byte-Übertragung (fire-and-forget, ignoriert Sendefehler) | +| `SetLedOverride(keyId, r, g, b)` | CMD 0x01 | +| `ClearLedOverride(keyId)` | CMD 0x02 | +| `SetLedBase(keyId, r, g, b)` | CMD 0x03 | +| `RequestConfig()` | CMD 0x13 – Board antwortet mit Config-Dump | +| `RequestMacros()` | CMD 0x23 – Board antwortet mit Makro-Dump | +| `SendConfig(cfg)` | BEGIN(0x10) → 38×DATA(0x11) → COMMIT(0x12), 5 ms zwischen Chunks | +| `SendMacros(macros)` | BEGIN(0x20) → 43×DATA(0x21) → COMMIT(0x22), 5 ms zwischen Chunks | + +`SendConfig` und `SendMacros` blockieren ~400 ms → werden in `Task.Run()` aus `ConfigForm.OnSave()` aufgerufen. + +## Events + +| Event | Gefeuert wenn | +|---|---| +| `Connected` | Verbindung erfolgreich hergestellt (auf UI-Thread) | +| `Disconnected` | Verbindung verloren (auf UI-Thread) | +| `PacketReceived` | Vollständiges 8-Byte-Paket empfangen (auf UI-Thread) | diff --git a/doc/02_device_config.md b/doc/02_device_config.md new file mode 100644 index 0000000..b4f3a58 --- /dev/null +++ b/doc/02_device_config.md @@ -0,0 +1,104 @@ +# DeviceConfig & MacroTable + +**Datei:** `DeviceConfig.cs` + +## Überblick + +C#-Spiegel der Firmware-Structs. Muss byte-kompatibel mit `SDeviceConfig` (nvm_config.h) und `SMacroTable` (macro_config.h) sein. + +--- + +## DeviceConfig + +### Felder + +| Feld | Typ | Inhalt | +|---|---|---| +| `MxActions[20]` | `DeviceAction[]` | Aktionen für MX-Buttons 0–19 | +| `EncActions[4,3]` | `DeviceAction[,]` | Encoder [0–3][SW=0/CW=1/CCW=2] | +| `LedBase[20]` | `Color[]` | RGB-Basis-LED-Farbe je Button | +| `LedAnim[20]` | `LedAnimType[]` | Animation je Button | +| `LedPeriod[20]` | `ushort[]` | Animationsperiode in ms | + +### Serialisierungs-Layout (ToBytes / FromBytes, 223 B) + +``` +Offset 0 4B Magic 0x56503202 (little-endian) +Offset 4 1B Version = 2 +Offset 5 2B CRC16-CCITT über Bytes 7–222 (little-endian) +Offset 7 60B MxActions[20] je 3B: type(1) + data_lo(1) + data_hi(1) +Offset 67 36B EncActions[4][3] je 3B +Offset103 20B LedBase[i].R +Offset123 20B LedBase[i].G +Offset143 20B LedBase[i].B +Offset163 20B LedAnim[i] als byte +Offset183 40B LedPeriod[i] als uint16 little-endian +``` + +### CRC16-CCITT + +Polynom `0x1021`, Init `0xFFFF`, über Bytes 7–222 (nach dem CRC-Feld selbst). Muss identisch mit Firmware-Implementierung sein. `DeviceConfig.Crc16()` ist statisch und direkt testbar. + +### Defaults (entspricht Firmware-Defaults) + +- Alle Aktionen: `None` +- LEDs: warm-weiß (R=80, G=40, B=0) +- Animation: `ColorCycle` (Regenbogen), Period 4000 ms + +--- + +## DeviceAction + +```csharp +public enum ActionType : byte { None=0, HidKey=1, HidConsumer=2, HostCommand=3, Macro=4 } + +public class DeviceAction { + public ActionType Type { get; set; } + public ushort Data { get; set; } + // HidKey: Low-Byte = HID Keycode, High-Byte = Modifier + // HidConsumer: Consumer Usage ID + // HostCommand: Command-ID + // Macro: Slot-Index 0–31 +} +``` + +`DeviceAction.Display` gibt einen lesbaren String zurück (z.B. `"Strg+C"`, `"Play/Pause"`, `"Makro 3"`). + +--- + +## MacroTable + +```csharp +public class MacroTable { + public const int Slots = 32; + public const int MaxSteps = 4; + public MacroStep[,] Steps { get; } // [slot][step] +} +public class MacroStep { + public byte Keycode { get; set; } // 0 = leer + public byte Modifier { get; set; } +} +``` + +### Slot-Konvention + +| Slots | Verwendung | +|---|---| +| 0–19 | MX-Button `mxIdx` (`MacroTable.SlotForMx(mxIdx)`) | +| 20–31 | Encoder: `20 + enc * 3 + actIdx` (`MacroTable.SlotForEncoder(enc, actIdx)`) | + +### Serialisierung (256 B) + +32 Slots × 4 Steps × 2 B = 256 B. Keycode zuerst, dann Modifier. Kein Magic/CRC (Board akzeptiert jeden Inhalt). + +--- + +## LedAnimType + +```csharp +public enum LedAnimType : byte { + Static=0, Blink=1, Pulse=2, ColorCycle=5 +} +``` + +Werte entsprechen `LEDAnim` in der Firmware. `FADE_IN` (3) und `FADE_OUT` (4) existieren in der Firmware aber nicht in der GUI (nicht konfigurierbar, nur `COLOR_FADE` intern). diff --git a/doc/03_tray_app.md b/doc/03_tray_app.md new file mode 100644 index 0000000..adf582b --- /dev/null +++ b/doc/03_tray_app.md @@ -0,0 +1,62 @@ +# TrayApp + +**Datei:** `TrayApp.cs` + +## Verantwortung + +- App-Lebenszyklus als `ApplicationContext` (kein Hauptfenster) +- Tray-Icon mit Verbindungsstatus und Kontextmenü +- Empfang und Routing aller Board-Events (via `SerialManager.PacketReceived`) +- Config/Makro-Dump-Empfang (chunked, via `_rxConfigBuf` / `_rxMacroBuf`) +- Öffnen des `ConfigForm` (nur eine Instanz gleichzeitig) + +## Tray-Menü + +``` +[●/○] VersaPad – verbunden / nicht verbunden (disabled, nur Anzeige) +────────────────────────────────────────────── +Konfiguration... → OpenConfigForm() +────────────────────────────────────────────── +Beenden → Application.Exit() +``` + +Icon und Tooltip spiegeln den Verbindungsstatus: +- Verbunden: `SystemIcons.Information`, Text "VersaPad – verbunden" +- Getrennt: `SystemIcons.Application`, Text "VersaPad – nicht verbunden" + +(TODO: eigenes Icon einbinden) + +## Board-Event-Handling (OnPacket) + +| Event-ID | Aktion | +|---|---| +| `EvtConfigBegin` | `_rxConfigBuf = new byte[223]` | +| `EvtConfigData` | Chunk in `_rxConfigBuf` eintragen (`KeyId * 6` = Byte-Offset) | +| `EvtConfigEnd` | `DeviceConfig.FromBytes()` → `ConfigForm.RefreshAll()` | +| `EvtMacroBegin` | `_rxMacroBuf = new byte[256]` | +| `EvtMacroData` | Chunk in `_rxMacroBuf` eintragen | +| `EvtMacroEnd` | `MacroTable.FromBytes()` | +| `EvtPong` | MessageBox "Ping OK" | +| `EvtConfigAck` | MessageBox "Config gespeichert" | +| `EvtConfigNack` | MessageBox "Config FEHLER" | +| `EvtMacroAck` | MessageBox "Makros gespeichert" | +| `EvtKeyDown` | TODO: HOST_COMMAND-Aktion ausführen | +| `EvtEncCw/Ccw` | TODO: Encoder HOST_COMMAND | + +## Verbindungslebenszyklus + +``` +OnConnected(): + Icon + Text + Menü-Item aktualisieren + serial.RequestConfig() → Config-Dump vom Board + serial.RequestMacros() → Makro-Dump vom Board + +OnDisconnected(): + Icon + Text + Menü-Item aktualisieren +``` + +## TODOs in dieser Klasse + +- HOST_COMMAND-Ausführung: `EvtKeyDown` empfangen → Aktion aus Config laden → URL/Programm starten +- Eigenes Tray-Icon statt `SystemIcons.Application` +- Debug-Log (`versapad_rx.txt`) entfernen diff --git a/doc/04_config_form.md b/doc/04_config_form.md new file mode 100644 index 0000000..6ba5d2c --- /dev/null +++ b/doc/04_config_form.md @@ -0,0 +1,75 @@ +# ConfigForm + +**Datei:** `ConfigForm.cs` + +## Verantwortung + +Hauptkonfigurationsfenster: zeigt alle 20 MX-Buttons und 4 Encoder, öffnet `ActionDialog` bei Klick, speichert Config + Makros auf das Board. + +## Layout + +``` +┌── Tasten (GroupBox) ──────────────────────────────────────────┐ +│ TableLayoutPanel 4 Spalten × 5 Zeilen │ +│ Jede Zelle = Button mit LED-Hintergrundfarbe + Aktionstext │ +└───────────────────────────────────────────────────────────────┘ +┌── Encoder (GroupBox) ─────────────────────────────────────────┐ +│ Header: SW / CW / CCW │ +│ 4 Zeilen × 3 Buttons (ENC 0–3) │ +└───────────────────────────────────────────────────────────────┘ +[Auf Board speichern] [Ping] [Exportieren] [Importieren] [Schließen] +``` + +`FixedSingle`-Border, `StartPosition = CenterScreen`, kein Maximieren. + +## MX-Button-Darstellung (RefreshMxButton) + +| Animation | Hintergrund | Textfarbe | +|---|---|---| +| ColorCycle | `(40, 40, 40)` dunkelgrau | Weiß | +| Andere | `LedBase[mxIdx]` | Schwarz/Weiß nach Luminanz | + +Text = `action.Display + animName` (z.B. `"Strg+C [Blinken]"`). + +Luminanz-Formel für Textfarben-Kontrast: `(R*299 + G*587 + B*114) / 1000` + +## mx_idx ↔ Physisches Layout + +``` +mx_idx = col * 5 + row (col=0..3, row=0..4) +``` + +Entspricht `key_id - 5` in der Firmware. Im TableLayoutPanel: Spalte=col, Zeile=row. + +## Speichern (OnSave) + +```csharp +Task.Run(() => { + _serial.SendConfig(_config); // ~300 ms + Thread.Sleep(50); + _serial.SendMacros(_macros); // ~250 ms + InvokeOnUi(() => { /* Button-Text + Enabled zurücksetzen */ }); +}); +``` + +Save-Button wird während der Übertragung deaktiviert, Text wechselt zu "Wird gesendet...". +Save-Button ist nur aktiviert wenn Board verbunden (`_serial.IsConnected`). + +## Import / Export + +- **Export**: `ConfigJson.Serialize(_config)` → `SaveFileDialog` → `.json`-Datei +- **Import**: `OpenFileDialog` → Datei lesen → `ConfigJson.Deserialize()` → `RefreshAll()` + +Fehler (IO, JSON-Parse, falsche Version) werden per `MessageBox` angezeigt. + +## RefreshAll + +Wird von `TrayApp` nach erfolgreicher Config vom Board aufgerufen (über `_configForm?.RefreshAll()`). Aktualisiert alle 20 MX-Buttons und 12 Encoder-Buttons ohne Dialog. + +## Extensions (in derselben Datei) + +```csharp +// Statischer ToolTip für alle Controls +public static void ToolTipText(this Control ctrl, string text) +public static string TypeDescription(this DeviceAction a) +``` diff --git a/doc/05_action_dialog.md b/doc/05_action_dialog.md new file mode 100644 index 0000000..5d0d645 --- /dev/null +++ b/doc/05_action_dialog.md @@ -0,0 +1,91 @@ +# ActionDialog + +**Datei:** `ActionDialog.cs` + +## Verantwortung + +Modaler Dialog zum Bearbeiten einer einzelnen Aktion (MX-Button oder Encoder-Slot) inkl. optionaler LED-Einstellungen. + +## Ergebnis-Properties + +```csharp +public DeviceAction ResultAction { get; } // Typ + Keycode/Usage/etc. +public Color ResultColor { get; } // LED-Basisfarbe +public LedAnimType ResultAnim { get; } // Animation +public ushort ResultPeriod { get; } // Periode in ms +``` + +## Panels (je nach Typ ein Panel sichtbar) + +| Typ | Panel | Inhalt | +|---|---|---| +| HID Tastatur | `_hidKeyPanel` | Capture-Button + Strg/Shift/Alt/Win-Checkboxen | +| HID Consumer | `_consumerPanel` | Dropdown mit 12 Medien-Aktionen | +| Host Command | `_cmdPanel` | TextBox für numerische Command-ID | +| Makro | `_macroPanel` | 4 Step-Buttons + je Strg/Shift/Alt-Checkboxen | +| Keine | — | Alle Panels ausgeblendet | + +LED-Panels (`_colorPanel`, `_animPanel`) erscheinen zusätzlich wenn `showColor=true` (nur MX-Buttons, nicht Encoder). + +`UpdateLayout()` repositioniert LED-Panels und passt `ClientSize` dynamisch an wenn der Typ gewechselt wird. + +## Tasten-Capture (HID-Modus) + +1. Benutzer klickt "Taste drücken..." +2. `_capturing = true` +3. Nächste Taste in `ProcessCmdKey()` → `VkToHid()` → `_hidKeycode` setzen +4. Capture-Button zeigt Tastenname (`HidKeyName()`) + +**Warum `ProcessCmdKey` statt `OnKeyDown`?** +WinForms behandelt Pfeil- und Enter-Tasten als "Dialog Keys" in `ProcessDialogKey()` — sie erreichen `OnKeyDown` nie. `ProcessCmdKey` wird vor `ProcessDialogKey` aufgerufen und kann diese Tasten abfangen. + +## Makro-Capture + +Jeder der 4 Steps hat einen eigenen Capture-Button. `_captureStep` (0–3, -1 = inaktiv) zeigt welcher Step gerade aufnimmt. Capture-Logik identisch mit HID-Modus, schreibt in `_stepKeycodes[captureStep]`. + +## Schlüssellookup (layout-unabhängig) + +**Problem**: Windows VK-Codes sind tastaturlayout-abhängig. `Keys.OemQuotes` auf QWERTZ → Ö, auf US → `'`. Würde falsche HID-Codes liefern. + +**Lösung**: PS/2 Scan-Codes sind layout-unabhängig (physische Tastenposition). + +``` +VK → Scan-Code via MapVirtualKey(vk, MAPVK_VK_TO_VSC) +Scan-Code → HID Usage via s_scanToHid[sc] +``` + +`s_scanToHid`: vollständige PS/2-Set-1 → HID-Tabelle (~60 Einträge). + +**Ausnahme**: Erweiterte Navigationstasten (Pfeiltasten, Pos1, Ende, …) geben via `MapVirtualKey` den Numpad-Scan-Code zurück. Für diese gibt es eine direkte `Keys → HID`-Tabelle (`s_extVkToHid`). + +**Anzeige**: `HidKeyName(hid)` gibt den lokalisierten Tastennamen zurück: +- Sondertasten: fest eingetragene deutsche Namen ("Esc", "→", "F5", …) +- Zeichentasten: `GetKeyNameText(scanCode << 16, ...)` → Windows gibt den Namen laut aktivem Layout zurück ("Ö" auf QWERTZ, ";" auf US) + +## Consumer-Aktionen + +12 HID Consumer Usage IDs als feste Liste: + +| Usage | Name | +|---|---| +| 0x00CD | Play / Pause | +| 0x00B5 | Nächster Titel | +| 0x00B6 | Vorheriger Titel | +| 0x00B7 | Stop | +| 0x00E9 | Lauter | +| 0x00EA | Leiser | +| 0x00E2 | Stummschalten | +| 0x0192 | Taschenrechner | +| 0x0223 | Browser – Startseite | +| 0x0224 | Browser – Zurück | +| 0x0225 | Browser – Vor | +| 0x00B0 | Aufnahme | + +## LED-Einstellungen + +Nur wenn `showColor=true` (MX-Buttons). Enthält: +- **Farbpicker**: `ColorDialog` → `_colorBtn.BackColor` +- **Animations-Dropdown**: Statisch / Blinken / Pulsieren / Regenbogen +- **Periode-Dropdown**: Presets von "Sehr langsam (8 s)" bis "Sehr schnell (250 ms)" + +Bei "Regenbogen" wird der Farbpicker ausgeblendet (Farbe irrelevant). diff --git a/doc/06_config_json.md b/doc/06_config_json.md new file mode 100644 index 0000000..72f7eb3 --- /dev/null +++ b/doc/06_config_json.md @@ -0,0 +1,48 @@ +# ConfigJson (Import / Export) + +**Datei:** `ConfigJson.cs` + +## Format + +Menschenlesbares JSON mit `System.Text.Json` (`WriteIndented=true`, Enums als Strings via `JsonStringEnumConverter`). + +```json +{ + "version": 2, + "buttons": [ + { + "index": 0, + "action": { "type": "HidKey", "data": 260 }, + "led": { "r": 80, "g": 40, "b": 0, "anim": "ColorCycle", "period_ms": 4000 } + }, + ... + ], + "encoders": [ + { + "index": 0, + "sw": { "type": "None", "data": 0 }, + "cw": { "type": "None", "data": 0 }, + "ccw": { "type": "None", "data": 0 } + }, + ... + ] +} +``` + +## Serialisierung + +`ConfigJson.Serialize(cfg)` → JSON-String. Exportiert alle 20 Buttons + 4 Encoder vollständig. + +## Deserialisierung + +`ConfigJson.Deserialize(json, cfg)`: +- Prüft `version` – wirft `InvalidDataException` bei Mismatch +- Schreibt in bestehendes `DeviceConfig`-Objekt (kein `new`) +- Fehlende `buttons`/`encoders`-Arrays werden ignoriert (partial import möglich) +- Ungültige `index`-Werte werden übersprungen + +## Anmerkungen + +- `MacroTable` wird **nicht** exportiert (kein JSON-Format für Makros definiert) +- `data` enthält den `ushort`-Wert direkt (für HidKey z.B. `Keycode | (Modifier << 8)`) +- Die Datei ist kein Binärformat und kann manuell bearbeitet werden diff --git a/doc/INDEX.md b/doc/INDEX.md new file mode 100644 index 0000000..1b71d11 --- /dev/null +++ b/doc/INDEX.md @@ -0,0 +1,25 @@ +# VersaGUI – Dokumentations-Index + +Jede Datei deckt eine GUI-Komponente ab. Für Claude: die relevante(n) Dateien zu Beginn einer Aufgabe lesen statt die gesamten Quelldateien zu scannen. + +| Datei | Inhalt | +|---|---| +| [00_architecture.md](00_architecture.md) | Threading-Modell, Datenfluss, Verbindungslebenszyklus, globale Constraints (DTR, IOException, Packed-Layout) | +| [01_serial_manager.md](01_serial_manager.md) | WMI-Erkennung, TryConnect, ReadLoop, Sende-Methoden, Reconnect-Backoff | +| [02_device_config.md](02_device_config.md) | DeviceConfig + MacroTable: Felder, Byte-Layout (223/256 B), CRC16, LedAnimType | +| [03_tray_app.md](03_tray_app.md) | ApplicationContext, Tray-Icon, Board-Event-Routing, Config/Makro-Dump-Empfang, TODOs | +| [04_config_form.md](04_config_form.md) | Grid-Layout, mx_idx-Formel, RefreshMxButton, OnSave (Task.Run), Import/Export | +| [05_action_dialog.md](05_action_dialog.md) | Panels je Typ, ProcessCmdKey-Capture, layout-unabhängiger Scan-Code-Lookup, Consumer-Liste | +| [06_config_json.md](06_config_json.md) | JSON-Format, Serialize/Deserialize, Einschränkungen | + +## Schnell-Referenz: Was steht wo? + +- **DtrEnable-Problem** → [00_architecture.md](00_architecture.md), [01_serial_manager.md](01_serial_manager.md) +- **IOException ≠ Disconnect (.NET 7)** → [00_architecture.md](00_architecture.md), [01_serial_manager.md](01_serial_manager.md) +- **Byte-Layout der 223-Byte-Config** → [02_device_config.md](02_device_config.md) +- **Warum ProcessCmdKey statt OnKeyDown?** → [05_action_dialog.md](05_action_dialog.md) +- **Warum Scan-Codes statt VK-Codes (Umlaut-Problem)?** → [05_action_dialog.md](05_action_dialog.md) +- **mx_idx ↔ key_id-Umrechnung** → [04_config_form.md](04_config_form.md) +- **Makro-Slot-Konvention** → [02_device_config.md](02_device_config.md) +- **HOST_COMMAND noch nicht implementiert** → [03_tray_app.md](03_tray_app.md) +- **Task.Run beim Speichern** → [04_config_form.md](04_config_form.md)