VersaGUI
Windows-Tray-App zur Konfiguration und Steuerung des VersaPad v2.
Geschrieben in C# / .NET 7 / WinForms.
Voraussetzungen
- Windows 10/11
- .NET 7 SDK (zum Bauen)
- VersaMCU-Firmware auf dem Board geflasht
- Board per USB verbunden (erscheint als CDC COM-Port, kein Treiber nötig)
Starten
dotnet run
Oder als Release-Build:
dotnet publish -c Release -r win-x64 --self-contained
Die App erscheint als Icon in der Windows-Taskleiste (System Tray). Kein Hauptfenster.
Bedienung
- Board verbinden – App erkennt das Board automatisch per VID/PID (
0x239A / 0x0042) via WMI
- Rechtsklick auf das Tray-Icon → Konfiguration...
- Taste/Encoder anklicken → Aktion auswählen:
- HID Tastatur: Großen Button klicken, dann gewünschte Taste drücken (Strg+C, F5, …)
- HID Consumer: Dropdown – Play/Pause, Lautstärke, etc.
- Host Command: Numerische Command-ID (zukünftig: URL/Programm)
- LED-Farbe: Farbpicker für die Idle-LED des Buttons
- Auf Board speichern – überträgt Config in den NVM des Boards
- Beim nächsten Verbinden wird die Config automatisch vom Board geladen
Funktionsumfang (Anforderungskatalog)
1 Verbindung & Geräteerkennung
| # |
Anforderung |
Status |
| 1.1 |
App läuft als Windows Tray-Anwendung ohne sichtbares Hauptfenster |
✅ |
| 1.2 |
Automatische Board-Erkennung per WMI / VID+PID (0x239A / 0x0042) |
✅ |
| 1.3 |
Automatischer Reconnect alle 3 s; 5 s Backoff nach Verbindungsverlust |
✅ |
| 1.4 |
Verbindungsstatus im Tray-Icon (Symbol + Tooltip) sichtbar |
✅ |
| 1.5 |
Beim Verbinden wird die gespeicherte Config automatisch vom Board gelesen |
✅ |
| 1.6 |
Beim Verbinden wird die Makro-Tabelle automatisch vom Board gelesen |
✅ |
2 Tastenbelegung – HID Tastatur
| # |
Anforderung |
Status |
| 2.1 |
Taste durch Drücken erfassen (kein manuelles HID-ID-Eingeben) |
✅ |
| 2.2 |
Modifier-Kombination: Strg / Shift / Alt / Win einzeln oder kombiniert |
✅ |
| 2.3 |
Pfeiltasten, Enter, Escape, F1–F12, Numpad erfassbar |
✅ |
| 2.4 |
Layout-unabhängige Erfassung via PS/2-Scan-Code → HID-Usage; Ö/Ä/Ü auf QWERTZ korrekt |
✅ |
| 2.5 |
Taste-Name wird laut aktivem Windows-Layout angezeigt (GetKeyNameText) |
✅ |
| 2.6 |
Board führt Tastendruck als USB-HID-Tastatureingabe aus (funktioniert ohne laufende App) |
✅ |
| 2.7 |
Hold-Semantik: Taste bleibt gedrückt solange physisch gehalten (OS initiiert Repeat nach ~500ms) |
✅ |
3 Tastenbelegung – HID Consumer / Medientasten
| # |
Anforderung |
Status |
| 3.1 |
Auswahl per Dropdown mit Klartext-Namen |
✅ |
| 3.2 |
Unterstützte Aktionen: Play/Pause, Nächster/Vorheriger Titel, Stop, Lauter/Leiser, Mute, Taschenrechner, Browser Zurück/Vor |
✅ |
| 3.3 |
Hold-Semantik: Media-Control bleibt aktiv solange Taste gehalten wird (z.B. Lautstärke-Wiederholung) |
✅ |
4 Tastenbelegung – Makros
| # |
Anforderung |
Status |
| 4.1 |
Bis zu 4 Schritte pro Makro (je 1 Taste + Modifier) |
✅ |
| 4.2 |
Jeder Schritt per Taste drücken erfassen, inkl. Sondertasten |
✅ |
| 4.3 |
Leere Schritte werden übersprungen (kürzere Makros möglich) |
✅ |
| 4.4 |
32 Makro-Slots – je ein Slot pro MX-Button (0–19) und Encoder-Aktion (20–31) |
✅ |
| 4.5 |
Makro-Tabelle wird separat vom Board gelesen und geschrieben (NVM Row 1) |
✅ |
| 4.6 |
Board führt Makro-Schritte mit 10 ms Key-Down + 20 ms Pause ohne laufende App aus |
✅ |
5 Encoder-Belegung
| # |
Anforderung |
Status |
| 5.1 |
4 Encoder, je 3 Aktionen: SW (Drücken), CW (Rechts), CCW (Links) |
✅ |
| 5.2 |
Gleiche Aktionstypen wie Tasten (HID Key, Consumer, Makro, Host Command) |
✅ |
| 5.3 |
Encoder-SW: Hold-Semantik wie normale Tasten (Taste bleibt gedrückt) |
✅ |
| 5.4 |
Encoder-CW/CCW: Tap-Modell (diskrete Ereignisse, atomare down+delay+up Sequenzen, kein Hold möglich) |
✅ |
6 LED-Konfiguration
| # |
Anforderung |
Status |
| 6.1 |
Basis-LED-Farbe pro MX-Button via Farbpicker (RGB) |
✅ |
| 6.2 |
Animationsmodus pro Button wählbar: Statisch, Blinken, Pulsieren, Regenbogen |
✅ |
| 6.3 |
Animations-Tempo: Schnell (0,5 s) / Mittel (1 s) / Langsam (2 s) / Sehr langsam (4 s) |
✅ |
| 6.4 |
Regenbogen-Modus: Board berechnet Hue-Sweep lokal, gleichmäßige Phasenverteilung |
✅ |
| 6.5 |
LED-Config und Aktionstyp im gleichen Dialog bearbeitbar |
✅ |
7 Konfiguration speichern & laden
| # |
Anforderung |
Status |
| 7.1 |
Config und Makros werden gleichzeitig per „Auf Board speichern" übertragen |
✅ |
| 7.2 |
Board validiert Config mit CRC16-CCITT + Magic + Version; NACK bei Fehler |
✅ |
| 7.3 |
Config bleibt nach Stromverlust im NVM erhalten (kein Flash-Verschleiß bei nur Lesen) |
✅ |
| 7.4 |
JSON-Export der gesamten Config (Aktionen + LED) in Datei |
✅ |
| 7.5 |
JSON-Import mit Versionscheck; ungültige Dateien werden abgelehnt |
✅ |
8 Verbindungsprotokoll
| # |
Anforderung |
Status |
| 8.1 |
8-Byte-Festlängen-Pakete über CDC Serial (kein Treiber nötig) |
✅ |
| 8.2 |
Ping/Pong zur manuellen Verbindungsdiagnose im Konfigurations-Fenster |
✅ |
| 8.3 |
Config-Transfer: BEGIN → n×DATA(6 Byte) → COMMIT → ACK/NACK |
✅ |
| 8.4 |
Makro-Transfer: gleiche Struktur, 43 Chunks à 6 Byte (256 Byte Tabelle) |
✅ |
| 8.5 |
Debug-Logging aller RX-Pakete in %TEMP%\versapad_rx.txt |
✅ |
9 Nicht implementiert / Roadmap
| # |
Anforderung |
Status |
| 9.1 |
Host Command: URL/Programm öffnen wenn Board-Taste gedrückt |
🔲 TODO |
| 9.2 |
Eigenes Tray-Icon (aktuell: Windows-Standard-Icon) |
🔲 TODO |
| 9.3 |
Fader/Potentiometer-Unterstützung (3× ADC-Achsen auf Board vorhanden) |
🔲 TODO |
| 9.4 |
Profile: mehrere Tastenbelegungen speichern und wechseln |
🔲 TODO |
Projekt-Struktur
VersaGUI/
├── VersaGUI.csproj – .NET 7 WinForms Projekt
├── Program.cs – Einstiegspunkt, startet TrayApp
├── TrayApp.cs – ApplicationContext: Tray-Icon, Menü, Paket-Routing
├── ConfigForm.cs – Konfigurations-Fenster (4×5 Grid + Encoder-Panel)
├── ActionDialog.cs – Dialog: Taste erfassen / Consumer / Makro / LED-Animation
├── DeviceConfig.cs – C#-Spiegel von SDeviceConfig (223 B, Version 2) + MacroTable (256 B)
├── ConfigJson.cs – JSON-Export/Import der DeviceConfig
├── Protocol.cs – Protokoll-Konstanten (Commands + Events)
└── SerialManager.cs – COM-Port-Erkennung, Read-Loop, Config + Makros senden/empfangen
Architektur
TrayApp
├── SerialManager – Hintergrund-Thread: COM-Port-Verbindung + Leseloop
│ ├── Connected-Event → Config vom Board anfordern (CMD_CONFIG_READ)
│ └── PacketReceived → TrayApp.OnPacket()
├── DeviceConfig – In-Memory-Modell der Board-Config
└── ConfigForm (optional) – Öffnet sich auf Benutzeranfrage
└── ActionDialog – Modaler Dialog pro Taste/Encoder
SerialManager
- Erkennt das Board per WMI (
Win32_PnPEntity, VID/PID-Filter) → COM-Port-Name
- Lese-Loop im Hintergrund-Thread: sammelt 8-Byte-Pakete, feuert
PacketReceived-Event auf dem UI-Thread
- Reconnect-Timer: versucht alle 3 Sekunden neu zu verbinden; nach Disconnect 5 Sekunden Backoff
- Wichtig:
DtrEnable = true muss vor Open() gesetzt werden – der SAMD21 prüft die DTR-Leitung in if (!SerialUSB) und verwirft sonst alle ausgehenden Pakete
DeviceConfig / Serialisierung
DeviceConfig.ToBytes() erzeugt den exakt 223-Byte-Puffer der SDeviceConfig-Struct (Version 2, little-endian, packed). CRC16-CCITT (Poly 0x1021, Init 0xFFFF) über Bytes 7–222 – identisch zur Firmware-Implementierung.
MacroTable.ToBytes() erzeugt 256 Byte (32 Slots × 4 Steps × 2 Byte).
Config-Übertragung Board→PC (Lesen):
PC sendet: CMD_CONFIG_READ (0x13)
Board sendet: EVT_CONFIG_BEGIN (Chunk-Anzahl = 38)
EVT_CONFIG_DATA × 38 (je 6 Nutzbytes)
EVT_CONFIG_END
Config-Übertragung PC→Board (Schreiben):
PC sendet: CMD_CONFIG_BEGIN (0x10, Chunk-Anzahl)
CMD_CONFIG_DATA × 38
CMD_CONFIG_COMMIT (0x12)
Board sendet: EVT_CONFIG_ACK (0x90) oder EVT_CONFIG_NACK (0x91)
Makro-Übertragung (analog, separate Kommandos 0x20–0x23):
PC sendet: CMD_MACRO_BEGIN (0x20, Chunk-Anzahl = 43)
CMD_MACRO_DATA × 43
CMD_MACRO_COMMIT (0x22)
Board sendet: EVT_MACRO_ACK (0x95)
Kommunikationsprotokoll
Das vollständige Protokoll (alle Command- und Event-IDs, NVM-Layout, Paketformat) ist in VersaMCU/README.md dokumentiert.
Bekannte .NET-Eigenheiten
| Problem |
Lösung |
.NET 7 SerialPort.ReadByte() wirft IOException statt TimeoutException auf Timeout |
catch (IOException) → nur als Fehler behandeln wenn Port nicht mehr offen ist |
DtrEnable = false (Standard) → Board sendet nichts |
DtrEnable = true im SerialPort-Konstruktor setzen, vor Open() |
DTR-Zustandswechsel nach Open() löst CDC-Disconnect auf dem Board aus |
DTR vor Open() setzen → kein Zustandswechsel |