Compare commits
3 Commits
24fe162833
...
7d381218f3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d381218f3 | |||
| 08db94bb6b | |||
| aa6b7a8ec9 |
@@ -0,0 +1,71 @@
|
||||
# Arbeitsanweisungen für Coding-Agents
|
||||
|
||||
Diese Datei gilt für das gesamte VersaGUI-Repository.
|
||||
|
||||
## Ziel und Nachbar-Repository
|
||||
|
||||
VersaGUI ist die Windows-/WinForms-Konfigurationsanwendung für VersaPad v2.
|
||||
Sie läuft auf .NET 7 und kommuniziert über feste 8-Byte-CDC-Pakete mit der
|
||||
Firmware im benachbarten, eigenständigen Repository `../VersaMCU`.
|
||||
|
||||
`DelphiGUI` gehört nicht zum gepflegten Scope und darf bei Änderungen nicht
|
||||
als Referenz oder Ziel verwendet werden.
|
||||
|
||||
## Vor Änderungen lesen
|
||||
|
||||
1. `README.md`
|
||||
2. `doc/INDEX.md`
|
||||
3. `doc/00_architecture.md`
|
||||
4. `doc/07_known_limitations.md`
|
||||
5. bei Protokoll-/Layoutänderungen zusätzlich:
|
||||
- `src/Protocol.cs`
|
||||
- `src/DeviceConfig.cs`
|
||||
- `../VersaMCU/AGENTS.md`
|
||||
- `../VersaMCU/doc/06_nvm_config.md`
|
||||
- `../VersaMCU/doc/07_serial_protocol.md`
|
||||
|
||||
## Gemeinsame Binärverträge
|
||||
|
||||
- `DeviceConfig` muss exakt `SDeviceConfig` entsprechen: Config v3, 740 Byte.
|
||||
- `MacroTable` muss exakt `SMacroTable` entsprechen: 512 Byte.
|
||||
- Enum-Werte, Packing, Offsets, CRC-Bereich, Chunkgrößen und USB-IDs sind
|
||||
gemeinsame Verträge mit VersaMCU.
|
||||
- Änderungen daran in beiden Repositories implementieren, dokumentieren,
|
||||
bauen und getrennt committen.
|
||||
- Host-Command-IDs niemals ungeprüft als Shellkommando ausführen. Eine spätere
|
||||
Host-Aktionsfunktion benötigt ein explizites sicheres Mapping.
|
||||
- Das Binärmodell akzeptiert alle Firmware-Animationen `0..6`; der aktuelle
|
||||
`ActionDialog` bietet davon nur Static, Blink, Pulse und ColorCycle an.
|
||||
Bestehende, im Dialog nicht angebotene Werte nicht stillschweigend
|
||||
überschreiben.
|
||||
|
||||
## Threading und Transfers
|
||||
|
||||
- WinForms-Controls ausschließlich auf dem UI-Thread anfassen.
|
||||
- `SerialManager.ReadLoop()` läuft im Hintergrund und postet Events über den
|
||||
`SynchronizationContext`.
|
||||
- SerialPort-Pakete müssen unter dem Write-Lock vollständig geschrieben werden.
|
||||
- Config-/Makrotransfers nicht parallel ausführen.
|
||||
- Dumps und Uploads nur akzeptieren, wenn Chunkzahl, Indizes und
|
||||
Vollständigkeit stimmen.
|
||||
- DTR muss vor `SerialPort.Open()` aktiviert sein.
|
||||
|
||||
## Verifikation
|
||||
|
||||
Mindestens:
|
||||
|
||||
```bash
|
||||
dotnet build src/VersaGUI.csproj --no-restore
|
||||
dotnet run --project tests/VersaGUI.ContractTests/VersaGUI.ContractTests.csproj
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Bei gemeinsamen Vertragsänderungen zusätzlich:
|
||||
|
||||
```bash
|
||||
pio run -d ../VersaMCU -e versapad
|
||||
```
|
||||
|
||||
Die Contract-Tests prüfen Config-/Makrogrößen, CRC, Feldvalidierung,
|
||||
Chunkvollständigkeit und Host-Command-Payloads. Sie ersetzen keinen Test mit
|
||||
angeschlossenem Board.
|
||||
@@ -1,209 +1,138 @@
|
||||
# VersaGUI
|
||||
|
||||
Windows-Tray-App zur Konfiguration und Steuerung des VersaPad v2.
|
||||
Geschrieben in **C# / .NET 7 / WinForms**.
|
||||
Windows-Tray-App zur Konfiguration des VersaPad v2.
|
||||
Geschrieben in C# mit WinForms.
|
||||
|
||||
Diese GUI und das benachbarte Repository `../VersaMCU` sind die gemeinsam
|
||||
gepflegten Referenzen für Protokoll und Datenlayout. Andere GUI-Ports gehören
|
||||
nicht zum aktuellen Scope.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Windows 10/11
|
||||
- [.NET 7 SDK](https://dotnet.microsoft.com/download/dotnet/7.0) (zum Bauen)
|
||||
- VersaMCU-Firmware auf dem Board geflasht
|
||||
- Board per USB verbunden (erscheint als CDC COM-Port, kein Treiber nötig)
|
||||
- .NET 7 SDK mit Windows-Desktop-Unterstützung
|
||||
- geflashte `VersaMCU`-Firmware
|
||||
- Board per USB als Composite Device (Keyboard-HID, Consumer-HID und CDC)
|
||||
verbunden
|
||||
|
||||
## Starten
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
dotnet run --project src/VersaGUI.csproj
|
||||
```
|
||||
|
||||
Oder als Release-Build:
|
||||
## Was die App macht
|
||||
|
||||
- erkennt das Board automatisch per VID/PID
|
||||
- oeffnet den CDC-COM-Port mit aktivem DTR
|
||||
- liest beim Verbinden zuerst die Config und danach die Makros
|
||||
- validiert Chunkzahl, eindeutige Indizes, Vollständigkeit, CRC und Feldwerte
|
||||
- zeigt MX-Buttons und Encoder-Aktionen an
|
||||
- bearbeitet alle drei Profile über die Profilauswahl
|
||||
- schreibt Config und Makros getrennt, aber in einem UI-Vorgang auf das Board
|
||||
- sendet einen Protokoll-Ping und zeigt die Antwort an
|
||||
|
||||
## Aktueller Datenstand
|
||||
|
||||
### DeviceConfig
|
||||
|
||||
- Magic `0x56503203`
|
||||
- Version `3`
|
||||
- `740` Byte
|
||||
- 3 Profile
|
||||
- globale Helligkeit
|
||||
- per-LED-Helligkeit
|
||||
|
||||
Globale Helligkeit und Encoder-Sensitivität werden bytegenau erhalten, besitzen
|
||||
aber derzeit keine eigenen Bedienelemente. Per-LED-Helligkeit kann über JSON
|
||||
gesetzt werden.
|
||||
|
||||
### MacroTable
|
||||
|
||||
- 32 Slots
|
||||
- 8 Steps pro Slot
|
||||
- `512` Byte
|
||||
|
||||
## Uebertragungsformat
|
||||
|
||||
Es wird dasselbe 8-Byte-Protokoll wie in der Firmware verwendet.
|
||||
|
||||
Aktuelle Chunk-Zahlen:
|
||||
|
||||
- Config: `124`
|
||||
- Makros: `86`
|
||||
|
||||
Speicherablauf:
|
||||
|
||||
```text
|
||||
CONFIG_BEGIN
|
||||
CONFIG_DATA x124
|
||||
CONFIG_COMMIT
|
||||
wait for ACK/NACK
|
||||
|
||||
MACRO_BEGIN
|
||||
MACRO_DATA x86
|
||||
MACRO_COMMIT
|
||||
wait for ACK/NACK
|
||||
```
|
||||
|
||||
## GUI-Funktionen
|
||||
|
||||
- HID-Key-Zuweisung inklusive Modifier
|
||||
- Consumer-Keys
|
||||
- Host-Command-IDs konfigurieren und empfangen; Desktop-Aktionen werden noch
|
||||
nicht ausgeführt
|
||||
- Makros mit bis zu 8 Steps
|
||||
- Profilwechsel als ActionType
|
||||
- LED-Farbe, Animation und Periode pro MX-Button
|
||||
- JSON Import/Export
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```text
|
||||
VersaGUI/
|
||||
|-- AGENTS.md
|
||||
|-- doc/
|
||||
|-- tests/VersaGUI.ContractTests/
|
||||
`-- src/
|
||||
|-- Program.cs
|
||||
|-- TrayApp.cs
|
||||
|-- SerialManager.cs
|
||||
|-- ChunkTransferBuffer.cs
|
||||
|-- DeviceConfig.cs
|
||||
|-- ConfigForm.cs
|
||||
|-- ActionDialog.cs
|
||||
|-- ConfigJson.cs
|
||||
`-- Protocol.cs
|
||||
```
|
||||
|
||||
## Build und Vertragsprüfungen
|
||||
|
||||
```bash
|
||||
dotnet publish -c Release -r win-x64 --self-contained
|
||||
dotnet build src/VersaGUI.csproj --no-restore
|
||||
dotnet run --project tests/VersaGUI.ContractTests/VersaGUI.ContractTests.csproj
|
||||
```
|
||||
|
||||
Die App erscheint als Icon in der Windows-Taskleiste (System Tray). Kein Hauptfenster.
|
||||
## Grenzen / Status
|
||||
|
||||
## Bedienung
|
||||
- die App ist Windows-only
|
||||
- Host-Command-Events enthalten Command-ID, Key-/Encoder-ID und Richtung; ein
|
||||
sicheres Mapping auf konkrete Desktop-Aktionen ist noch nicht implementiert
|
||||
- der ActionDialog bietet aktuell vier der sieben vom Binärformat unterstützten
|
||||
LED-Animationen an
|
||||
- ein vollständig ausbleibendes Dump-Ende besitzt noch keinen Empfangs-Timeout
|
||||
|
||||
1. **Board verbinden** – App erkennt das Board automatisch per VID/PID (`0x239A / 0x0042`) via WMI
|
||||
2. **Rechtsklick** auf das Tray-Icon → **Konfiguration...**
|
||||
3. 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
|
||||
4. **Auf Board speichern** – überträgt Config in den NVM des Boards
|
||||
5. Beim nächsten Verbinden wird die Config automatisch vom Board geladen
|
||||
Die vollständige Liste steht in
|
||||
[`doc/07_known_limitations.md`](doc/07_known_limitations.md).
|
||||
|
||||
---
|
||||
## Einstieg für Coding-LLMs
|
||||
|
||||
## Funktionsumfang (Anforderungskatalog)
|
||||
Repository-Anweisungen, gemeinsame Binärverträge und Verifikation stehen in
|
||||
[`AGENTS.md`](AGENTS.md).
|
||||
|
||||
### 1 Verbindung & Geräteerkennung
|
||||
## Weiterführende Doku
|
||||
|
||||
| # | 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](../VersaMCU/README.md#serial-protokoll-8-bytes-fixed) 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 |
|
||||
- [Dokumentationsindex](doc/INDEX.md)
|
||||
- [Datenlayout](doc/02_device_config.md)
|
||||
- [Bekannte Einschränkungen](doc/07_known_limitations.md)
|
||||
- [../VersaMCU/doc/07_serial_protocol.md](../VersaMCU/doc/07_serial_protocol.md)
|
||||
|
||||
+24
-11
@@ -16,10 +16,11 @@
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `ConfigForm` | Hauptfenster (Profilauswahl, Grid, Encoder-Panel und 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) |
|
||||
| `DeviceConfig` | C#-Spiegel von `SDeviceConfig`; Validierung und Serialisierung (740 B) |
|
||||
| `MacroTable` | C#-Spiegel von `SMacroTable`; Validierung und Serialisierung (512 B) |
|
||||
| `ChunkTransferBuffer` | prüft Dump-Chunkzahl, Indizes und Vollständigkeit |
|
||||
| `ConfigJson` | JSON-Import/Export für `DeviceConfig` |
|
||||
| `Protocol` | Konstanten für alle Command/Event-IDs (spiegelt `usb_serial.h`) |
|
||||
|
||||
@@ -29,9 +30,9 @@
|
||||
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)
|
||||
├── Config-Dump vollständig sammeln → DeviceConfig.FromBytes()
|
||||
├── danach Makro-Dump vollständig sammeln → MacroTable.FromBytes()
|
||||
└── HOST_COMMAND-Events mit 16-Bit-Command-ID empfangen
|
||||
|
||||
Benutzer → ConfigForm → ActionDialog
|
||||
→ DeviceConfig / MacroTable (in-memory ändern)
|
||||
@@ -45,7 +46,7 @@ Benutzer → ConfigForm → ActionDialog
|
||||
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)
|
||||
Sende-Task : ConfigForm.OnSave() → Task.Run() (blockiert bis ACK/NACK oder Timeout)
|
||||
```
|
||||
|
||||
Alle Board-Events werden per `SynchronizationContext.Post` auf den UI-Thread gepostet. Controls dürfen nie vom BG-Thread angefasst werden.
|
||||
@@ -55,7 +56,8 @@ Alle Board-Events werden per `SynchronizationContext.Post` auf den UI-Thread gep
|
||||
```
|
||||
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()
|
||||
→ 200 ms warten → ReadLoop starten → Connected-Event
|
||||
→ RequestConfig() → vollständig validieren → RequestMacros()
|
||||
|
||||
Disconnect → ReadLoop bricht ab → Disconnected-Event → 5 s Backoff → Timer läuft weiter
|
||||
```
|
||||
@@ -64,6 +66,17 @@ Disconnect → ReadLoop bricht ab → Disconnected-Event → 5 s Backoff → Tim
|
||||
|
||||
- **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).
|
||||
- **Packed-kompatible Serialisierung**: `DeviceConfig.ToBytes()` erzeugt exakt
|
||||
740 B in derselben Reihenfolge wie `SDeviceConfig`; `MacroTable` exakt 512 B.
|
||||
- **Config-Version**: `DeviceConfig.Version == 3`. `FromBytes()` prüft Magic,
|
||||
Version, CRC, Profilindex, Actions, LED-Enums und kritische Perioden, bevor
|
||||
Objektzustand geändert wird.
|
||||
- **Transfer-Synchronisation**: Config- und Makro-Uploads laufen unter einem
|
||||
gemeinsamen Transfer-Lock; einzelne Pakete unter einem Write-Lock.
|
||||
- **Host-Commands**: Pakete sind vollständig definiert, die Ausführung einer
|
||||
Command-ID als Desktop-Aktion ist bewusst noch nicht implementiert.
|
||||
- **UI-Modell-Grenze**: Das Binärmodell kann alle sieben Firmware-Animationen
|
||||
lesen und schreiben. Der ActionDialog bietet aktuell vier davon zur Auswahl.
|
||||
|
||||
Bewusst offene Punkte stehen gesammelt in
|
||||
[`07_known_limitations.md`](07_known_limitations.md).
|
||||
|
||||
@@ -75,7 +75,7 @@ _configAckOk / _macroAckOk – volatile bool (true = ACK, false = NACK)
|
||||
|
||||
| Methode | Rückgabe | Funktion |
|
||||
|---|---|---|
|
||||
| `Send(pkt)` | void | Rohe 8-Byte-Übertragung (fire-and-forget) |
|
||||
| `Send(pkt)` | bool | Rohe, unter Write-Lock atomare 8-Byte-Übertragung |
|
||||
| `SetLedOverride(keyId, r, g, b)` | void | CMD 0x01 |
|
||||
| `ClearLedOverride(keyId)` | void | CMD 0x02 |
|
||||
| `SetLedBase(keyId, r, g, b)` | void | CMD 0x03 |
|
||||
@@ -84,7 +84,15 @@ _configAckOk / _macroAckOk – volatile bool (true = ACK, false = NACK)
|
||||
| `SendConfig(cfg)` | bool | BEGIN(0x10) → 124×DATA(0x11) → COMMIT(0x12), 5 ms zwischen Chunks, wartet auf ACK/NACK |
|
||||
| `SendMacros(macros)` | bool | BEGIN(0x20) → 86×DATA(0x21) → COMMIT(0x22), 5 ms zwischen Chunks, wartet auf ACK/NACK |
|
||||
|
||||
`SendConfig` und `SendMacros` blockieren ~1,5 s (Chunks + NVM-Zeit) → werden in `Task.Run()` aus `ConfigForm.OnSave()` aufgerufen.
|
||||
Die reine Chunk-Pausierung beträgt nominal etwa 640 ms für Config und 450 ms
|
||||
für Makros, jeweils zuzüglich Port-, NVM- und ACK-Zeit. Nach jedem COMMIT wird
|
||||
höchstens 3 s auf ACK/NACK gewartet. Deshalb laufen beide Methoden aus
|
||||
`ConfigForm.OnSave()` in `Task.Run()` und niemals auf dem UI-Thread.
|
||||
|
||||
Vor `BEGIN` wird das zugehörige ACK-Gate geleert. Beide Blob-Sender teilen
|
||||
einen Transfer-Lock, prüfen jeden `Send()`-Rückgabewert und brechen bei
|
||||
Disconnect sofort ab. Ein Disconnect gibt wartende ACK-Gates mit Fehlerstatus
|
||||
frei.
|
||||
|
||||
## Events
|
||||
|
||||
|
||||
+130
-89
@@ -1,120 +1,161 @@
|
||||
# DeviceConfig & MacroTable
|
||||
# DeviceConfig und MacroTable
|
||||
|
||||
**Datei:** `DeviceConfig.cs`
|
||||
Datei:
|
||||
|
||||
## Überblick
|
||||
- `src/DeviceConfig.cs`
|
||||
|
||||
C#-Spiegel der Firmware-Structs. Muss byte-kompatibel mit `SDeviceConfig` (nvm_config.h) und `SMacroTable` (macro_config.h) sein.
|
||||
## Zweck
|
||||
|
||||
---
|
||||
Die C#-Klassen spiegeln das aktuelle Firmware-Layout bytegenau.
|
||||
Sie muessen deshalb synchron zu `VersaMCU/src/config/nvm_config.h` und `macro_config.h` bleiben.
|
||||
|
||||
## DeviceConfig
|
||||
|
||||
### Felder
|
||||
### Globaler Stand
|
||||
|
||||
| Feld | Typ | Inhalt |
|
||||
|---|---|---|
|
||||
| `ActiveProfileIndex` | `byte` | Aktives Profil (0–2) |
|
||||
| `GlobalBrightness` | `byte` | Globale LED-Helligkeit (0–255) |
|
||||
| `MxActions[20]` | `DeviceAction[]` | Aktionen für MX-Buttons 0–19 (aus aktivem Profil) |
|
||||
| `EncActions[4,3]` | `DeviceAction[,]` | Encoder [0–3][SW=0/CW=1/CCW=2] (aus aktivem Profil) |
|
||||
| `LedBase[20]` | `Color[]` | RGB-Basis-LED-Farbe je Button (aus aktivem Profil) |
|
||||
| `LedAnim[20]` | `LedAnimType[]` | Animation je Button (aus aktivem Profil) |
|
||||
| `LedPeriod[20]` | `ushort[]` | Animationsperiode in ms (aus aktivem Profil) |
|
||||
- Magic `0x56503203`
|
||||
- Version `3`
|
||||
- Groesse `740` Byte
|
||||
- 3 Profile
|
||||
|
||||
### Serialisierungs-Layout (ToBytes / FromBytes, 740 B)
|
||||
### Wichtige Felder
|
||||
|
||||
```
|
||||
Offset 0 4B Magic 0x56503203 (little-endian)
|
||||
Offset 4 1B Version = 3
|
||||
Offset 5 2B CRC16-CCITT über Bytes 7–739 (little-endian)
|
||||
Offset 7 1B active_profile (0–2)
|
||||
Offset 8 1B global_brightness
|
||||
Offset 9 4B enc_sensitivity[4]
|
||||
Offset 13 19B Reserve (_reserve)
|
||||
| Feld | Bedeutung |
|
||||
|---|---|
|
||||
| `ActiveProfileIndex` | aktives Profil 0..2 |
|
||||
| `GlobalBrightness` | globale LED-Helligkeit |
|
||||
| `Profiles[3]` | komplette Profil-Daten |
|
||||
| `EncSensitivity[4]` | im Vertrag enthalten; Firmware nutzt den Wert derzeit nicht |
|
||||
|
||||
Profil 0 (Offset 32, 236 B):
|
||||
Offset 32 60B MxActions[20] je 3B: type(1) + data_lo(1) + data_hi(1)
|
||||
Offset 92 36B EncActions[4][3] je 3B
|
||||
Offset 128 20B LedBase[i].R
|
||||
Offset 148 20B LedBase[i].G
|
||||
Offset 168 20B LedBase[i].B
|
||||
Offset 188 20B LedBrightness[i]
|
||||
Offset 208 20B LedAnim[i] als byte
|
||||
Offset 228 40B LedPeriod[i] als uint16 little-endian
|
||||
Fuer die GUI gibt es zusaetzlich Komfortzugriffe auf das aktive Profil:
|
||||
|
||||
Profil 1 (Offset 268, 236 B): identisches Layout
|
||||
Profil 2 (Offset 504, 236 B): identisches Layout
|
||||
- `MxActions[20]`
|
||||
- `EncActions[4,3]`
|
||||
- `LedBase[20]`
|
||||
- `LedAnim[20]`
|
||||
- `LedPeriod[20]`
|
||||
|
||||
### Layout
|
||||
|
||||
```text
|
||||
Offset 0 4B magic
|
||||
Offset 4 1B version
|
||||
Offset 5 2B crc
|
||||
Offset 7 1B active_profile
|
||||
Offset 8 1B global_brightness
|
||||
Offset 9 4B enc_sensitivity[4]
|
||||
Offset 13 19B reserve
|
||||
Offset 32 profile 0
|
||||
Offset 268 profile 1
|
||||
Offset 504 profile 2
|
||||
```
|
||||
|
||||
### CRC16-CCITT
|
||||
Jedes Profil belegt 236 Byte:
|
||||
|
||||
Polynom `0x1021`, Init `0xFFFF`, über Bytes 7–739 (nach dem CRC-Feld selbst, einschließlich `active_profile`). 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), Helligkeit 255
|
||||
- Animation: `ColorCycle` (Regenbogen), Period 4000 ms
|
||||
- `active_profile = 0`, `global_brightness = 255`
|
||||
|
||||
---
|
||||
|
||||
## DeviceAction
|
||||
|
||||
```csharp
|
||||
public enum ActionType : byte {
|
||||
None=0, HidKey=1, HidConsumer=2, HostCommand=3, Macro=4, ProfileSwitch=5
|
||||
}
|
||||
|
||||
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
|
||||
// ProfileSwitch: 0–2 = Ziel-Profil, 0xFFFF = nächstes Profil (Zyklus)
|
||||
}
|
||||
```text
|
||||
20 x mx_actions
|
||||
12 x enc_actions
|
||||
20 x led_r
|
||||
20 x led_g
|
||||
20 x led_b
|
||||
20 x led_brightness
|
||||
20 x led_anim
|
||||
20 x led_period_ms
|
||||
```
|
||||
|
||||
`DeviceAction.Display` gibt einen lesbaren String zurück (z.B. `"Strg+C"`, `"Play/Pause"`, `"→ Profil 2"`, `"→ Nächstes Profil"`).
|
||||
### Action-Daten
|
||||
|
||||
---
|
||||
| Typ | Datenbereich |
|
||||
|---|---|
|
||||
| `None` | Daten werden ignoriert |
|
||||
| `HidKey` | Low-Byte Keycode `0x00..0x65`, High-Byte Modifier |
|
||||
| `HidConsumer` | Usage `0x0000..0x03FF` |
|
||||
| `HostCommand` | frei definierte Command-ID `0x0000..0xFFFF` |
|
||||
| `Macro` | Slot `0..31` |
|
||||
| `ProfileSwitch` | Profil `0..2`; `0x00FF` oder `0xFFFF` bedeuten „nächstes Profil“ |
|
||||
|
||||
### LED-Animationen im Binärformat
|
||||
|
||||
| Wert | Enum |
|
||||
|---|---|
|
||||
| `0` | `Static` |
|
||||
| `1` | `Blink` |
|
||||
| `2` | `Pulse` |
|
||||
| `3` | `FadeIn` |
|
||||
| `4` | `FadeOut` |
|
||||
| `5` | `ColorCycle` |
|
||||
| `6` | `ColorFade` |
|
||||
|
||||
`DeviceConfig` akzeptiert alle Werte `0..6`. Der aktuelle ActionDialog bietet
|
||||
nur `Static`, `Blink`, `Pulse` und `ColorCycle` an.
|
||||
|
||||
## CRC
|
||||
|
||||
CRC16-CCITT:
|
||||
|
||||
- Polynom `0x1021`
|
||||
- Init `0xFFFF`
|
||||
- Bereich `7..739`
|
||||
|
||||
Die CRC muss exakt zur Firmware passen.
|
||||
|
||||
## Defaults
|
||||
|
||||
Firmware-Defaults:
|
||||
|
||||
- alle Actions `None`
|
||||
- `GlobalBrightness = 255`
|
||||
- `enc_sensitivity = 1`
|
||||
- Base-Farbe `R=80, G=40, B=0`
|
||||
- Animation `ColorCycle`
|
||||
- Periode `4000 ms`
|
||||
|
||||
Sichtbarer Effekt auf dem Board:
|
||||
|
||||
- alle MX-LEDs laufen im Regenbogenmodus
|
||||
|
||||
## MacroTable
|
||||
|
||||
```csharp
|
||||
public class MacroTable {
|
||||
public const int Slots = 32;
|
||||
public const int MaxSteps = 8;
|
||||
public MacroStep[,] Steps { get; } // [slot][step]
|
||||
}
|
||||
public class MacroStep {
|
||||
public byte Keycode { get; set; } // 0 = leer
|
||||
public byte Modifier { get; set; }
|
||||
}
|
||||
```
|
||||
### Stand
|
||||
|
||||
- 32 Slots
|
||||
- 8 Steps pro Slot
|
||||
- 512 Byte gesamt
|
||||
|
||||
### Slot-Konvention
|
||||
|
||||
| Slots | Verwendung |
|
||||
| Slots | Bedeutung |
|
||||
|---|---|
|
||||
| 0–19 | MX-Button `mxIdx` (`MacroTable.SlotForMx(mxIdx)`) |
|
||||
| 20–31 | Encoder: `20 + enc * 3 + actIdx` (`MacroTable.SlotForEncoder(enc, actIdx)`) |
|
||||
| `0..19` | MX-Buttons |
|
||||
| `20..31` | Encoder-Aktionen |
|
||||
|
||||
### Serialisierung (512 B)
|
||||
### Serialisierung
|
||||
|
||||
32 Slots × 8 Steps × 2 B = 512 B. Keycode zuerst, dann Modifier. Kein Magic/CRC (Board akzeptiert jeden Inhalt).
|
||||
Ein Step besteht aus:
|
||||
|
||||
---
|
||||
- `keycode`
|
||||
- `modifier`
|
||||
|
||||
## LedAnimType
|
||||
`keycode == 0` beendet die Ausführung des Slots. Belegte Steps nach der ersten
|
||||
Lücke werden daher zwar serialisiert, von der Firmware aber nicht ausgeführt.
|
||||
|
||||
```csharp
|
||||
public enum LedAnimType : byte {
|
||||
Static=0, Blink=1, Pulse=2, ColorCycle=5
|
||||
}
|
||||
```
|
||||
Es gibt kein Magic und keine CRC fuer die Makrotabelle.
|
||||
Beim Transfer prüft die GUI trotzdem exakte Größe und alle HID-Keycodes; die
|
||||
Firmware prüft zusätzlich die vollständige Chunkmenge.
|
||||
|
||||
Werte entsprechen `LEDAnim` in der Firmware. `FADE_IN` (3) und `FADE_OUT` (4) existieren in der Firmware aber nicht in der GUI (nicht konfigurierbar).
|
||||
## Validierung
|
||||
|
||||
`ToBytes()` verweigert ungültige Objektzustände. `FromBytes()` verändert das
|
||||
bestehende Objekt erst, nachdem Größe, Magic, Version, CRC, aktives Profil,
|
||||
Action-Werte, LED-Enums und die Mindestperiode von 2 ms für `Pulse` geprüft
|
||||
wurden. Für die Makrotabelle gelten exakte 512 Byte und Keycodes bis `0x65`.
|
||||
|
||||
## Wichtig fuer Aenderungen
|
||||
|
||||
Wenn sich Firmware-Layout, Magic, Version, Profilzahl oder Makrogroesse aendern, muessen mindestens diese Stellen zusammen angepasst werden:
|
||||
|
||||
- `VersaMCU`
|
||||
- `VersaGUI`
|
||||
|
||||
Beide sind eigenständige Git-Repositories und werden getrennt gebaut und
|
||||
committed. Andere GUI-Ports gehören nicht zum gepflegten Scope.
|
||||
|
||||
+18
-13
@@ -7,7 +7,7 @@
|
||||
- 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`)
|
||||
- sequenzieller Config-/Makro-Dump-Empfang über `ChunkTransferBuffer`
|
||||
- ACK/NACK-Weiterleitung an SerialManager
|
||||
- Öffnen des `ConfigForm` (nur eine Instanz gleichzeitig)
|
||||
|
||||
@@ -31,19 +31,18 @@ Icon und Tooltip spiegeln den Verbindungsstatus:
|
||||
|
||||
| Event-ID | Aktion |
|
||||
|---|---|
|
||||
| `EvtConfigBegin` | `_rxConfigBuf = new byte[740]` |
|
||||
| `EvtConfigData` | Chunk in `_rxConfigBuf` eintragen (`KeyId * 6` = Byte-Offset) |
|
||||
| `EvtConfigEnd` | `DeviceConfig.FromBytes()` → `ConfigForm.RefreshAll()` |
|
||||
| `EvtConfigBegin/Data/End` | Chunkzahl, Indizes und Vollständigkeit prüfen; danach `DeviceConfig.FromBytes()` |
|
||||
| `EvtConfigAck` | `serial.SignalConfigAck()` — gibt SendConfig()-Thread frei |
|
||||
| `EvtConfigNack` | `serial.SignalConfigNack()` — gibt SendConfig()-Thread frei (Fehler) |
|
||||
| `EvtMacroBegin` | `_rxMacroBuf = new byte[512]` |
|
||||
| `EvtMacroData` | Chunk in `_rxMacroBuf` eintragen |
|
||||
| `EvtMacroEnd` | `MacroTable.FromBytes()` |
|
||||
| `EvtMacroBegin/Data/End` | vollständigen 512-Byte-Dump prüfen; danach `MacroTable.FromBytes()` |
|
||||
| `EvtMacroAck` | `serial.SignalMacroAck()` — gibt SendMacros()-Thread frei |
|
||||
| `EvtMacroNack` | `serial.SignalMacroNack()` — gibt SendMacros()-Thread frei (Fehler) |
|
||||
| `EvtPong` | MessageBox "Ping OK" |
|
||||
| `EvtKeyDown` | TODO: HOST_COMMAND-Aktion ausführen |
|
||||
| `EvtEncCw/Ccw` | TODO: Encoder HOST_COMMAND |
|
||||
| `EvtKeyDown/Up` | Host-Command-ID in Byte 2/3 empfangen |
|
||||
| `EvtEncCw/Ccw` | Host-Command-ID und Encoderrichtung empfangen |
|
||||
|
||||
Diese vier Host-Events sendet die Firmware nur für Actions vom Typ
|
||||
`HostCommand`. `TrayApp` dekodiert die 16-Bit-ID, führt sie aber noch nicht aus.
|
||||
|
||||
ACK/NACK-Events zeigen keine eigene MessageBox mehr — das Ergebnis wird nach Abschluss beider Transfers gebündelt in `ConfigForm.OnSave()` angezeigt.
|
||||
|
||||
@@ -53,14 +52,20 @@ ACK/NACK-Events zeigen keine eigene MessageBox mehr — das Ergebnis wird nach A
|
||||
OnConnected():
|
||||
Icon + Text + Menü-Item aktualisieren
|
||||
serial.RequestConfig() → Config-Dump vom Board
|
||||
serial.RequestMacros() → Makro-Dump vom Board
|
||||
nach gültigem Config-End:
|
||||
serial.RequestMacros() → Makro-Dump vom Board
|
||||
|
||||
OnDisconnected():
|
||||
Icon + Text + Menü-Item aktualisieren
|
||||
```
|
||||
|
||||
## TODOs in dieser Klasse
|
||||
Ein mit `END` abgeschlossener, aber unvollständiger oder ungültiger Dump wird
|
||||
einmal wiederholt. Schlägt auch der zweite Versuch fehl, zeigt das Tray-Icon
|
||||
eine Warnung. Bleibt `END` vollständig aus, gibt es derzeit keinen
|
||||
Empfangs-Timeout; siehe
|
||||
[`07_known_limitations.md`](07_known_limitations.md).
|
||||
|
||||
- HOST_COMMAND-Ausführung: `EvtKeyDown` empfangen → Aktion aus Config laden → URL/Programm starten
|
||||
## Offene Punkte
|
||||
|
||||
- sichere Zuordnung von Command-IDs zu explizit erlaubten Desktop-Aktionen
|
||||
- Eigenes Tray-Icon statt `SystemIcons.Application`
|
||||
- Debug-Log (`versapad_rx.txt`) entfernen
|
||||
|
||||
+19
-5
@@ -4,7 +4,11 @@
|
||||
|
||||
## Verantwortung
|
||||
|
||||
Hauptkonfigurationsfenster: zeigt alle 20 MX-Buttons und 4 Encoder, öffnet `ActionDialog` bei Klick, speichert Config + Makros auf das Board.
|
||||
Hauptkonfigurationsfenster: wählt eines von drei Profilen, zeigt dessen 20
|
||||
MX-Buttons und 4 Encoder, öffnet `ActionDialog` bei Klick und speichert Config
|
||||
plus Makros auf das Board.
|
||||
|
||||
Oberhalb des Tasten-Grids befindet sich die Profilauswahl für Profil 1 bis 3.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -45,15 +49,23 @@ Entspricht `key_id - 5` in der Firmware. Im TableLayoutPanel: Spalte=col, Zeile=
|
||||
|
||||
```csharp
|
||||
Task.Run(() => {
|
||||
_serial.SendConfig(_config); // ~300 ms
|
||||
Thread.Sleep(50);
|
||||
_serial.SendMacros(_macros); // ~250 ms
|
||||
bool cfgOk = _serial.SendConfig(_config); // wartet auf ACK/NACK
|
||||
bool macroOk = _serial.SendMacros(_macros);
|
||||
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`).
|
||||
Config und Makros werden sequenziell gesendet; ein gemeinsamer Transfer-Lock
|
||||
verhindert parallele Blob-Transfers.
|
||||
|
||||
## Profile
|
||||
|
||||
Die Profilauswahl setzt `ActiveProfileIndex` und schaltet alle Komfortzugriffe
|
||||
von `DeviceConfig` auf das gewählte Profil um. `RefreshAll()` synchronisiert
|
||||
die Auswahl nach Board-Reload oder JSON-Import und zeichnet anschließend die
|
||||
20 MX- und 12 Encoder-Schaltflächen neu.
|
||||
|
||||
## Import / Export
|
||||
|
||||
@@ -64,7 +76,9 @@ 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.
|
||||
Wird von `TrayApp` nach erfolgreicher Config oder Makrotabelle vom Board sowie
|
||||
nach JSON-Import aufgerufen. Aktualisiert Profilauswahl, alle 20 MX-Buttons und
|
||||
12 Encoder-Buttons ohne Dialog.
|
||||
|
||||
## Extensions (in derselben Datei)
|
||||
|
||||
|
||||
+19
-5
@@ -39,8 +39,9 @@ LED-Panels (`_colorPanel`, `_animPanel`) erscheinen zusätzlich wenn `showColor=
|
||||
// Index 2: "Profil 2" → Data = 1
|
||||
// Index 3: "Profil 3" → Data = 2
|
||||
|
||||
// Initialbelegung:
|
||||
_profileCombo.SelectedIndex = action.Data == 0xFFFF ? 0 : action.Data + 1;
|
||||
// Initialbelegung: beide von der Firmware akzeptierten Zykluswerte erkennen
|
||||
_profileCombo.SelectedIndex =
|
||||
action.Data is 0x00FF or 0xFFFF ? 0 : action.Data + 1;
|
||||
|
||||
// In OnOk():
|
||||
data = _profileCombo.SelectedIndex == 0
|
||||
@@ -48,7 +49,9 @@ data = _profileCombo.SelectedIndex == 0
|
||||
: (ushort)(_profileCombo.SelectedIndex - 1);
|
||||
```
|
||||
|
||||
Im Board wird `0xFFFF` als `(uint8_t)0xFF` gespeichert und in der Firmware als "nächstes Profil" interpretiert.
|
||||
`SAction.data` ist ein 16-Bit-Feld. Die GUI schreibt für „nächstes Profil“
|
||||
kanonisch `0xFFFF`; Firmware und Deserialisierung akzeptieren zusätzlich den
|
||||
älteren Wert `0x00FF`.
|
||||
|
||||
## Tasten-Capture (HID-Modus)
|
||||
|
||||
@@ -62,7 +65,10 @@ WinForms behandelt Pfeil- und Enter-Tasten als "Dialog Keys" in `ProcessDialogKe
|
||||
|
||||
## Makro-Capture
|
||||
|
||||
Jeder der 8 Steps hat einen eigenen Capture-Button. `_captureStep` (0–7, -1 = inaktiv) zeigt welcher Step gerade aufnimmt. Capture-Logik identisch mit HID-Modus, schreibt in `_stepKeycodes[captureStep]`.
|
||||
Jeder der 8 Steps hat einen eigenen Capture-Button. `_captureStep` (0–7,
|
||||
-1 = inaktiv) zeigt, welcher Step gerade aufnimmt. Capture-Logik identisch mit
|
||||
HID-Modus, schreibt in `_stepKeycodes[captureStep]`. Der erste leere Step
|
||||
beendet die Makrosequenz; deshalb dürfen belegte Steps keine Lücke enthalten.
|
||||
|
||||
## Schlüssellookup (layout-unabhängig)
|
||||
|
||||
@@ -107,6 +113,14 @@ Scan-Code → HID Usage via s_scanToHid[sc]
|
||||
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)"
|
||||
- **Periode-Dropdown**: 500 ms, 1 s, 2 s oder 4 s
|
||||
|
||||
Bei "Regenbogen" wird der Farbpicker ausgeblendet (Farbe irrelevant).
|
||||
|
||||
Das Binärmodell kennt außerdem `FadeIn`, `FadeOut` und `ColorFade`; diese drei
|
||||
Werte sind im Dialog noch nicht auswählbar. Per-LED-Helligkeit wird im
|
||||
Binärformat und JSON erhalten, besitzt hier aber ebenfalls kein Bedienelement.
|
||||
Beim Öffnen einer nicht angebotenen Animation fällt die Auswahl derzeit auf
|
||||
`Static` zurück. Unbekannte, aber formal gültige Consumer-Usages fallen auf
|
||||
„Play / Pause“ zurück. Details stehen in
|
||||
[`07_known_limitations.md`](07_known_limitations.md).
|
||||
|
||||
+20
-3
@@ -8,12 +8,20 @@ Menschenlesbares JSON mit `System.Text.Json` (`WriteIndented=true`, Enums als St
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"profile": 0,
|
||||
"buttons": [
|
||||
{
|
||||
"index": 0,
|
||||
"action": { "type": "HidKey", "data": 260 },
|
||||
"led": { "r": 80, "g": 40, "b": 0, "anim": "ColorCycle", "period_ms": 4000 }
|
||||
"led": {
|
||||
"r": 80,
|
||||
"g": 40,
|
||||
"b": 0,
|
||||
"brightness": 255,
|
||||
"anim": "ColorCycle",
|
||||
"period_ms": 4000
|
||||
}
|
||||
},
|
||||
...
|
||||
],
|
||||
@@ -31,12 +39,16 @@ Menschenlesbares JSON mit `System.Text.Json` (`WriteIndented=true`, Enums als St
|
||||
|
||||
## Serialisierung
|
||||
|
||||
`ConfigJson.Serialize(cfg)` → JSON-String. Exportiert alle 20 Buttons + 4 Encoder vollständig.
|
||||
`ConfigJson.Serialize(cfg)` → JSON-String. Exportiert alle 20 Buttons und vier
|
||||
Encoder des aktuell aktiven Profils vollständig, einschließlich
|
||||
Per-LED-Helligkeit.
|
||||
|
||||
## Deserialisierung
|
||||
|
||||
`ConfigJson.Deserialize(json, cfg)`:
|
||||
- Prüft `version` – wirft `InvalidDataException` bei Mismatch
|
||||
- prüft und übernimmt `profile` im Bereich `0..2`
|
||||
- validiert Actions, LED-Enums und die Mindestperiode für `Pulse`
|
||||
- Schreibt in bestehendes `DeviceConfig`-Objekt (kein `new`)
|
||||
- Fehlende `buttons`/`encoders`-Arrays werden ignoriert (partial import möglich)
|
||||
- Ungültige `index`-Werte werden übersprungen
|
||||
@@ -44,5 +56,10 @@ Menschenlesbares JSON mit `System.Text.Json` (`WriteIndented=true`, Enums als St
|
||||
## Anmerkungen
|
||||
|
||||
- `MacroTable` wird **nicht** exportiert (kein JSON-Format für Makros definiert)
|
||||
- globale Helligkeit und Encoder-Sensitivität werden nicht exportiert
|
||||
- `profile` wählt das Zielprofil aus und macht es gleichzeitig zum aktiven
|
||||
Profil; die beiden anderen Profile bleiben unverändert
|
||||
- `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
|
||||
- Ein ungültiger Wert wird vor dem Anwenden mit `InvalidDataException`
|
||||
abgelehnt
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Bekannte Einschränkungen
|
||||
|
||||
Diese Seite trennt bewusst offene Punkte von bereits implementierten
|
||||
Verträgen. Sie ist vor Änderungen an Verbindungslogik, ActionDialog oder
|
||||
Binärformat zu lesen.
|
||||
|
||||
## Host-Commands
|
||||
|
||||
Firmware und GUI übertragen für `HostCommand`:
|
||||
|
||||
- Key- beziehungsweise Encoder-ID
|
||||
- Richtung über die Event-ID
|
||||
- 16-Bit-Command-ID in Byte 2/3
|
||||
|
||||
`TrayApp` führt die Command-ID noch nicht als Desktop-Aktion aus. Eine spätere
|
||||
Implementierung benötigt ein explizites Allowlist-Mapping; beliebige
|
||||
Shellbefehle aus Config-Daten sind nicht zulässig.
|
||||
|
||||
## LED- und globale Einstellungen
|
||||
|
||||
Das Binärformat und `DeviceConfig` unterstützen sieben Animationen:
|
||||
`Static`, `Blink`, `Pulse`, `FadeIn`, `FadeOut`, `ColorCycle` und `ColorFade`.
|
||||
Der `ActionDialog` bietet aktuell nur die ersten drei sowie `ColorCycle` an.
|
||||
Beim Öffnen einer nicht angebotenen Animation fällt die Auswahl auf `Static`
|
||||
zurück und würde beim Bestätigen entsprechend gespeichert.
|
||||
|
||||
Per-LED-Helligkeit wird binär und im JSON erhalten, besitzt aber kein
|
||||
Bedienelement. Globale Helligkeit und Encoder-Sensitivität werden binär
|
||||
erhalten, sind weder im Hauptfenster editierbar noch Teil des JSON-Exports.
|
||||
Die Firmware verwendet Encoder-Sensitivität derzeit ebenfalls nicht.
|
||||
|
||||
## ActionDialog
|
||||
|
||||
- Die Consumer-Liste enthält zwölf vordefinierte Usages. Andere formal gültige
|
||||
Usages bis `0x03FF` können aus dem Board gelesen werden, fallen beim Öffnen
|
||||
des Dialogs aber auf „Play / Pause“ zurück.
|
||||
- Das Periodenfeld bietet nur 500, 1000, 2000 und 4000 ms. Andere geladene
|
||||
Perioden fallen im Dialog auf 4000 ms zurück.
|
||||
- Ein Makro endet am ersten Step mit `keycode == 0`; Lücken vor späteren
|
||||
belegten Steps werden nicht ausgeführt.
|
||||
|
||||
## Transfers und Verbindung
|
||||
|
||||
- Das CDC-Protokoll besitzt kein Byte-Framing, keine Paket-CRC und keine
|
||||
Transfer-ID. Verliert der Stream ein Byte, kann die 8-Byte-Grenze bis zum
|
||||
Reconnect verschoben bleiben.
|
||||
- Chunkzahl, eindeutige Indizes, Vollständigkeit und END-Metadaten werden
|
||||
geprüft. Bleibt ein Dump jedoch ohne `END`, existiert noch kein
|
||||
Empfangs-Timeout oder Watchdog; die automatische Wiederholung startet nur
|
||||
nach einem als ungültig erkannten `END`.
|
||||
- `RequestConfig()` und `RequestMacros()` melden einen unmittelbar
|
||||
fehlgeschlagenen `Send()` nicht an `TrayApp`.
|
||||
- Während `SendConfig()` und `SendMacros()` ist nur die Speichern-Schaltfläche
|
||||
deaktiviert. Andere Editoraktionen werden nicht global gesperrt.
|
||||
|
||||
## Testabdeckung
|
||||
|
||||
Die Console-Contract-Tests prüfen:
|
||||
|
||||
- Configgröße, CRC und Roundtrip
|
||||
- ausgewählte ungültige Configfelder
|
||||
- Makrogröße und HID-Keycode-Grenze
|
||||
- Chunkvollständigkeit und doppelte Chunks
|
||||
- 16-Bit-Host-Command-Payload
|
||||
|
||||
Nicht automatisiert geprüft werden WinForms-Interaktionen, WMI-Porterkennung,
|
||||
echte SerialPort-Fehler, Reconnects, NVM-Schreibzeiten, HID-Ausgabe und
|
||||
Hardwareverhalten. Dafür ist weiterhin ein Test mit angeschlossenem VersaPad
|
||||
erforderlich.
|
||||
+23
-19
@@ -1,25 +1,29 @@
|
||||
# VersaGUI – Dokumentations-Index
|
||||
# VersaGUI - Dokumentationsindex
|
||||
|
||||
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.
|
||||
Die Dateien hier beschreiben die aktuelle C#-Referenz-GUI fuer Config v3 und 32x8 Makros.
|
||||
|
||||
Repository-weite LLM-/Agent-Richtlinien stehen in
|
||||
[`../AGENTS.md`](../AGENTS.md). Gemeinsame Firmwareverträge müssen zusätzlich
|
||||
gegen `../../VersaMCU` geprüft werden.
|
||||
|
||||
| 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 |
|
||||
| [00_architecture.md](00_architecture.md) | Threading-Modell, Datenfluss, Verbindungslebenszyklus |
|
||||
| [01_serial_manager.md](01_serial_manager.md) | Port-Erkennung, Connect, ReadLoop, Sende-Pfad |
|
||||
| [02_device_config.md](02_device_config.md) | `DeviceConfig`, `MacroTable`, aktuelles Byte-Layout, CRC |
|
||||
| [03_tray_app.md](03_tray_app.md) | TrayApp, Packet-Routing, Dump-Empfang, UI-Zustand |
|
||||
| [04_config_form.md](04_config_form.md) | Grid, Speichern, Import/Export |
|
||||
| [05_action_dialog.md](05_action_dialog.md) | Key-Capture, Action-Auswahl, LED-Einstellungen |
|
||||
| [06_config_json.md](06_config_json.md) | JSON-Format und Grenzen |
|
||||
| [07_known_limitations.md](07_known_limitations.md) | Bewusst offene GUI-, Protokoll- und Testgrenzen |
|
||||
|
||||
## Schnell-Referenz: Was steht wo?
|
||||
## Schnellreferenz
|
||||
|
||||
- **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)
|
||||
- aktuelles Config-Layout `740` Byte -> [02_device_config.md](02_device_config.md)
|
||||
- aktuelle Makrotabelle `512` Byte -> [02_device_config.md](02_device_config.md)
|
||||
- DTR und Connect-Pfad -> [00_architecture.md](00_architecture.md), [01_serial_manager.md](01_serial_manager.md)
|
||||
- Makro-Slot-Konvention -> [02_device_config.md](02_device_config.md)
|
||||
- Host-Command-Status -> [03_tray_app.md](03_tray_app.md)
|
||||
- bekannte Einschränkungen -> [07_known_limitations.md](07_known_limitations.md)
|
||||
- automatisierte Binär-/Transferverträge ->
|
||||
[`../tests/VersaGUI.ContractTests/`](../tests/VersaGUI.ContractTests/)
|
||||
|
||||
+8
-3
@@ -5,9 +5,12 @@
|
||||
// HID Tastatur → Capture-Button (Klicken + Taste drücken) + Modifier-Checkboxen
|
||||
// HID Consumer → Dropdown mit benannten Medien-/Lautstärke-Aktionen
|
||||
// Host Command → Zahlen-Eingabe (Command-ID)
|
||||
// Makro → acht HID-Key-Steps
|
||||
// Profilwechsel → Zielprofil oder zyklisch nächstes Profil
|
||||
// Keine Aktion → nichts
|
||||
//
|
||||
// Optional (nur MX-Buttons): LED-Basisfarbe + LED-Animation + Geschwindigkeit
|
||||
// Optional (nur MX-Buttons): LED-Basisfarbe + vier auswählbare Animationen +
|
||||
// Geschwindigkeit. Das Binärmodell akzeptiert zusätzlich drei Animationen.
|
||||
|
||||
namespace VersaGUI;
|
||||
|
||||
@@ -336,7 +339,9 @@ public class ActionDialog : Form
|
||||
_profileCombo.Items.AddRange(new object[]
|
||||
{ "Nächstes Profil (Zyklus)", "Profil 1", "Profil 2", "Profil 3" });
|
||||
_profileCombo.SelectedIndex = action.Type == ActionType.ProfileSwitch
|
||||
? (action.Data == 0xFFFF ? 0 : Math.Clamp((int)action.Data + 1, 1, 3))
|
||||
? (action.Data is 0x00FF or 0xFFFF
|
||||
? 0
|
||||
: Math.Clamp((int)action.Data + 1, 1, 3))
|
||||
: 0;
|
||||
_profilePanel.Controls.AddRange(new Control[] { profileLabel, _profileCombo });
|
||||
|
||||
@@ -403,7 +408,7 @@ public class ActionDialog : Form
|
||||
|
||||
var macroHint = new Label
|
||||
{
|
||||
Text = "Klicke einen Step, dann Taste drücken. Leere Steps werden übersprungen.",
|
||||
Text = "Klicke einen Step, dann Taste drücken. Der erste leere Step beendet das Makro.",
|
||||
Location = new Point(0, 0),
|
||||
Size = new Size(396, 28),
|
||||
ForeColor = SystemColors.GrayText,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace VersaGUI;
|
||||
|
||||
/// <summary>
|
||||
/// Sammelt einen Board→GUI-Dump und akzeptiert ihn nur, wenn Chunkzahl,
|
||||
/// Indizes, Vollständigkeit und END-Metadaten exakt zum erwarteten Blob passen.
|
||||
/// </summary>
|
||||
public sealed class ChunkTransferBuffer
|
||||
{
|
||||
private readonly int _size;
|
||||
private readonly int _expectedChunks;
|
||||
|
||||
private byte[]? _buffer;
|
||||
private bool[]? _received;
|
||||
private bool _valid;
|
||||
|
||||
public ChunkTransferBuffer(int size, int expectedChunks)
|
||||
{
|
||||
_size = size;
|
||||
_expectedChunks = expectedChunks;
|
||||
}
|
||||
|
||||
public bool Begin(byte announcedChunks)
|
||||
{
|
||||
_buffer = new byte[_size];
|
||||
_received = new bool[_expectedChunks];
|
||||
_valid = announcedChunks == _expectedChunks;
|
||||
return _valid;
|
||||
}
|
||||
|
||||
public bool Add(SerialPacket packet)
|
||||
{
|
||||
if (!_valid || _buffer is null || _received is null)
|
||||
return false;
|
||||
|
||||
int chunk = packet.KeyId;
|
||||
int offset = chunk * Protocol.PayloadSize;
|
||||
if (chunk >= _expectedChunks || _received[chunk] || offset >= _size)
|
||||
{
|
||||
_valid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
int count = Math.Min(Protocol.PayloadSize, _size - offset);
|
||||
Buffer.BlockCopy(packet.Data, 2, _buffer, offset, count);
|
||||
_received[chunk] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryComplete(byte announcedChunks, out byte[] data)
|
||||
{
|
||||
data = Array.Empty<byte>();
|
||||
|
||||
bool complete =
|
||||
_valid &&
|
||||
announcedChunks == _expectedChunks &&
|
||||
_buffer is not null &&
|
||||
_received is not null &&
|
||||
_received.All(received => received);
|
||||
|
||||
if (complete)
|
||||
data = _buffer!;
|
||||
|
||||
Reset();
|
||||
return complete;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_buffer = null;
|
||||
_received = null;
|
||||
_valid = false;
|
||||
}
|
||||
}
|
||||
+9
-6
@@ -2,6 +2,7 @@
|
||||
// Hauptfenster der VersaPad-Konfiguration.
|
||||
//
|
||||
// Layout:
|
||||
// Profil-Auswahl (Profil 1–3)
|
||||
// ┌── Tasten (4 × 5 Grid) ──────────────────────────────────────────────┐
|
||||
// │ Jede Taste = Button mit Hintergrundfarbe (= LED-Base) und │
|
||||
// │ Aktionsbeschriftung. Klick öffnet ActionDialog. │
|
||||
@@ -9,11 +10,10 @@
|
||||
// ┌── Encoder ─────────────────────────────────────────────────────────┐
|
||||
// │ 4 Zeilen à [SW][CW][CCW] – ohne LED-Farbe │
|
||||
// └────────────────────────────────────────────────────────────────────┘
|
||||
// [Auf Board speichern] [Schließen]
|
||||
// [Auf Board speichern] [Ping] [Exportieren] [Importieren] [Schließen]
|
||||
//
|
||||
// "Auf Board speichern" sendet die Config in 6-Byte-Chunks über Serial.
|
||||
// Firmware-Seite (WRITE_CONFIG) ist noch TODO → Schaltfläche ist
|
||||
// deaktiviert wenn das Board nicht verbunden ist.
|
||||
// "Auf Board speichern" sendet Config und Makros in 6-Byte-Chunks über CDC.
|
||||
// Die Schaltfläche ist deaktiviert, wenn das Board nicht verbunden ist.
|
||||
|
||||
namespace VersaGUI;
|
||||
|
||||
@@ -288,7 +288,7 @@ public class ConfigForm : Form
|
||||
_saveBtn.Enabled = false;
|
||||
_saveBtn.Text = "Wird gesendet...";
|
||||
|
||||
// SendConfig + SendMacros blockieren ~400ms → Background-Thread
|
||||
// SendConfig + SendMacros warten auf ACK/NACK → Background-Thread
|
||||
Task.Run(() =>
|
||||
{
|
||||
bool cfgOk = _serial.SendConfig(_config); // wartet intern auf CONFIG_ACK/NACK
|
||||
@@ -319,6 +319,9 @@ public class ConfigForm : Form
|
||||
// Alle Buttons neu zeichnen (z.B. nach Config-Laden vom Board)
|
||||
public void RefreshAll()
|
||||
{
|
||||
if (_profileCombo.SelectedIndex != _config.ActiveProfileIndex)
|
||||
_profileCombo.SelectedIndex = _config.ActiveProfileIndex;
|
||||
|
||||
for (int i = 0; i < 20; i++) RefreshMxButton(i);
|
||||
for (int enc = 0; enc < 4; enc++)
|
||||
for (int act = 0; act < 3; act++)
|
||||
@@ -435,7 +438,7 @@ internal static class Extensions
|
||||
{
|
||||
ActionType.HidKey => "HID Tastatur-Keycode",
|
||||
ActionType.HidConsumer => "HID Consumer Control (Media/Volume)",
|
||||
ActionType.HostCommand => "Host Command – App führt aus",
|
||||
ActionType.HostCommand => "Host Command-ID für Desktop-Mapping",
|
||||
ActionType.Macro => "Makro-Sequenz",
|
||||
ActionType.ProfileSwitch => "Profil wechseln",
|
||||
_ => "Keine Aktion",
|
||||
|
||||
@@ -86,6 +86,11 @@ internal static class ConfigJson
|
||||
if (doc.Version != DeviceConfig.Version)
|
||||
throw new InvalidDataException(
|
||||
$"Config-Version {doc.Version} wird nicht unterstützt (erwartet {DeviceConfig.Version}).");
|
||||
if (doc.Profile >= DeviceConfig.ProfileCount)
|
||||
throw new InvalidDataException("Profilindex muss im Bereich 0..2 liegen.");
|
||||
|
||||
ValidateDocument(doc);
|
||||
cfg.ActiveProfileIndex = doc.Profile;
|
||||
|
||||
if (doc.Buttons != null)
|
||||
{
|
||||
@@ -127,6 +132,46 @@ internal static class ConfigJson
|
||||
dst.Data = src.Data;
|
||||
}
|
||||
|
||||
private static void ValidateDocument(ConfigJsonDoc doc)
|
||||
{
|
||||
if (doc.Buttons != null)
|
||||
{
|
||||
foreach (ButtonEntry button in doc.Buttons)
|
||||
{
|
||||
ValidateAction(button.Action);
|
||||
if (button.Led == null) continue;
|
||||
|
||||
byte anim = (byte)button.Led.Anim;
|
||||
if (anim > (byte)LedAnimType.ColorFade)
|
||||
throw new InvalidDataException(
|
||||
$"Button {button.Index}: unbekannte LED-Animation {anim}.");
|
||||
if (button.Led.Anim == LedAnimType.Pulse &&
|
||||
button.Led.PeriodMs < 2)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Button {button.Index}: Pulse benötigt mindestens 2 ms.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.Encoders != null)
|
||||
{
|
||||
foreach (EncoderEntry encoder in doc.Encoders)
|
||||
{
|
||||
ValidateAction(encoder.Sw);
|
||||
ValidateAction(encoder.Cw);
|
||||
ValidateAction(encoder.Ccw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateAction(ActionEntry? action)
|
||||
{
|
||||
if (action != null && !DeviceConfig.ActionIsValid(action.Type, action.Data))
|
||||
throw new InvalidDataException(
|
||||
$"Ungültige Action {action.Type} mit Datenwert {action.Data}.");
|
||||
}
|
||||
|
||||
// ── JSON-Datenklassen ─────────────────────────────────────────────────────
|
||||
|
||||
private class ConfigJsonDoc
|
||||
|
||||
+150
-12
@@ -37,9 +37,9 @@ public enum ActionType : byte
|
||||
None = 0,
|
||||
HidKey = 1, // data = Keycode (Low-Byte) + Modifier (High-Byte)
|
||||
HidConsumer = 2, // data = HID Consumer Usage ID
|
||||
HostCommand = 3, // data = Command-ID → App führt aus
|
||||
HostCommand = 3, // data = Command-ID für ein sicheres App-Mapping
|
||||
Macro = 4, // data = Makro-Slot-Index (0–31)
|
||||
ProfileSwitch = 5, // data = Profil-Index (0–2)
|
||||
ProfileSwitch = 5, // data = Profil 0–2 oder 0x00FF/0xFFFF für nächstes Profil
|
||||
}
|
||||
|
||||
// Ein Step in einem Makro (keycode=0 → leerer/letzter Step)
|
||||
@@ -57,6 +57,7 @@ public class MacroTable
|
||||
{
|
||||
public const int Slots = 32;
|
||||
public const int MaxSteps = 8;
|
||||
public const int Size = Slots * MaxSteps * 2;
|
||||
|
||||
// [slot][step]
|
||||
public MacroStep[,] Steps { get; } = new MacroStep[Slots, MaxSteps];
|
||||
@@ -75,7 +76,11 @@ public class MacroTable
|
||||
// Serialisierung: 32 × 8 × 2 = 512 Bytes
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var buf = new byte[512];
|
||||
if (!IsValid())
|
||||
throw new InvalidOperationException(
|
||||
"Makrotabelle enthält einen nicht unterstützten HID-Keycode.");
|
||||
|
||||
var buf = new byte[Size];
|
||||
int pos = 0;
|
||||
for (int s = 0; s < Slots; s++)
|
||||
for (int i = 0; i < MaxSteps; i++)
|
||||
@@ -86,9 +91,11 @@ public class MacroTable
|
||||
return buf;
|
||||
}
|
||||
|
||||
public void FromBytes(byte[] buf)
|
||||
public bool FromBytes(byte[] buf)
|
||||
{
|
||||
if (buf.Length < 512) return;
|
||||
if (buf.Length != Size || !SerializedDataIsValid(buf))
|
||||
return false;
|
||||
|
||||
int pos = 0;
|
||||
for (int s = 0; s < Slots; s++)
|
||||
for (int i = 0; i < MaxSteps; i++)
|
||||
@@ -96,6 +103,24 @@ public class MacroTable
|
||||
Steps[s, i].Keycode = buf[pos++];
|
||||
Steps[s, i].Modifier = buf[pos++];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
for (int slot = 0; slot < Slots; slot++)
|
||||
for (int step = 0; step < MaxSteps; step++)
|
||||
if (Steps[slot, step].Keycode > 0x65)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool SerializedDataIsValid(byte[] buf)
|
||||
{
|
||||
for (int pos = 0; pos < buf.Length; pos += 2)
|
||||
if (buf[pos] > 0x65)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +130,10 @@ public enum LedAnimType : byte
|
||||
Static = 0,
|
||||
Blink = 1,
|
||||
Pulse = 2,
|
||||
FadeIn = 3,
|
||||
FadeOut = 4,
|
||||
ColorCycle = 5,
|
||||
ColorFade = 6,
|
||||
}
|
||||
|
||||
public class DeviceAction
|
||||
@@ -147,7 +175,9 @@ public class DeviceAction
|
||||
if (Type == ActionType.Macro)
|
||||
return $"Makro {Data}";
|
||||
if (Type == ActionType.ProfileSwitch)
|
||||
return Data == 0xFFFF ? "→ Nächstes Profil" : $"→ Profil {Data + 1}";
|
||||
return Data is 0x00FF or 0xFFFF
|
||||
? "→ Nächstes Profil"
|
||||
: $"→ Profil {Data + 1}";
|
||||
return $"CMD {Data}";
|
||||
}
|
||||
}
|
||||
@@ -251,6 +281,9 @@ public class DeviceConfig
|
||||
{
|
||||
public const uint Magic = 0x56503203;
|
||||
public const byte Version = 3;
|
||||
public const int Size = 740;
|
||||
public const int ProfileCount = 3;
|
||||
public const int ProfileSize = 236;
|
||||
|
||||
public const int EncSw = 0;
|
||||
public const int EncCw = 1;
|
||||
@@ -278,8 +311,10 @@ public class DeviceConfig
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
const int size = 740;
|
||||
var buf = new byte[size];
|
||||
if (!TryValidate(out string reason))
|
||||
throw new InvalidOperationException($"Ungültige DeviceConfig: {reason}");
|
||||
|
||||
var buf = new byte[Size];
|
||||
int pos = 0;
|
||||
|
||||
WriteU32(buf, ref pos, Magic);
|
||||
@@ -297,7 +332,7 @@ public class DeviceConfig
|
||||
Profiles[p].ToBytes(buf, pos + p * 236);
|
||||
pos += 3 * 236;
|
||||
|
||||
ushort crc = Crc16(buf, 7, size - 7);
|
||||
ushort crc = Crc16(buf, 7, Size - 7);
|
||||
buf[crcOffset] = (byte)(crc & 0xFF);
|
||||
buf[crcOffset + 1] = (byte)(crc >> 8);
|
||||
|
||||
@@ -306,18 +341,19 @@ public class DeviceConfig
|
||||
|
||||
public bool FromBytes(byte[] buf)
|
||||
{
|
||||
if (buf.Length < 740) return false;
|
||||
if (buf.Length != Size) return false;
|
||||
|
||||
uint magic = ReadU32(buf, 0);
|
||||
if (magic != Magic) return false;
|
||||
if (buf[4] != Version) return false;
|
||||
|
||||
ushort storedCrc = (ushort)(buf[5] | (buf[6] << 8));
|
||||
ushort computedCrc = Crc16(buf, 7, 740 - 7);
|
||||
ushort computedCrc = Crc16(buf, 7, Size - 7);
|
||||
if (storedCrc != computedCrc) return false;
|
||||
if (!SerializedFieldsAreValid(buf)) return false;
|
||||
|
||||
int pos = 7;
|
||||
ActiveProfileIndex = Math.Min(buf[pos++], (byte)2);
|
||||
ActiveProfileIndex = buf[pos++];
|
||||
GlobalBrightness = buf[pos++];
|
||||
for (int i = 0; i < 4; i++) EncSensitivity[i] = buf[pos++];
|
||||
pos += 19; // Reserve überspringen
|
||||
@@ -328,6 +364,108 @@ public class DeviceConfig
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryValidate(out string reason)
|
||||
{
|
||||
if (ActiveProfileIndex >= ProfileCount)
|
||||
{
|
||||
reason = "aktives Profil liegt nicht im Bereich 0..2";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int profile = 0; profile < ProfileCount; profile++)
|
||||
{
|
||||
DeviceProfile current = Profiles[profile];
|
||||
foreach (DeviceAction action in current.MxActions)
|
||||
{
|
||||
if (!ActionIsValid(action.Type, action.Data))
|
||||
{
|
||||
reason = $"ungültige MX-Action in Profil {profile}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int enc = 0; enc < 4; enc++)
|
||||
{
|
||||
for (int actionIndex = 0; actionIndex < 3; actionIndex++)
|
||||
{
|
||||
DeviceAction action = current.EncActions[enc, actionIndex];
|
||||
if (!ActionIsValid(action.Type, action.Data))
|
||||
{
|
||||
reason = $"ungültige Encoder-Action in Profil {profile}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int led = 0; led < 20; led++)
|
||||
{
|
||||
byte anim = (byte)current.LedAnim[led];
|
||||
if (anim > (byte)LedAnimType.ColorFade)
|
||||
{
|
||||
reason = $"ungültige LED-Animation in Profil {profile}";
|
||||
return false;
|
||||
}
|
||||
if (current.LedAnim[led] == LedAnimType.Pulse &&
|
||||
current.LedPeriod[led] < 2)
|
||||
{
|
||||
reason = $"Pulse-Periode unter 2 ms in Profil {profile}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reason = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool ActionIsValid(ActionType type, ushort data)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ActionType.None => true,
|
||||
ActionType.HidKey => (byte)(data & 0xFF) <= 0x65,
|
||||
ActionType.HidConsumer => data <= 0x03FF,
|
||||
ActionType.HostCommand => true,
|
||||
ActionType.Macro => data < MacroTable.Slots,
|
||||
ActionType.ProfileSwitch => data <= 2 || data is 0x00FF or 0xFFFF,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool SerializedFieldsAreValid(byte[] buf)
|
||||
{
|
||||
if (buf[7] >= ProfileCount) return false;
|
||||
|
||||
for (int profile = 0; profile < ProfileCount; profile++)
|
||||
{
|
||||
int profileOffset = 32 + profile * ProfileSize;
|
||||
int actionPos = profileOffset;
|
||||
|
||||
for (int action = 0; action < 32; action++)
|
||||
{
|
||||
ActionType type = (ActionType)buf[actionPos++];
|
||||
ushort data = (ushort)(buf[actionPos] | (buf[actionPos + 1] << 8));
|
||||
actionPos += 2;
|
||||
if (!ActionIsValid(type, data)) return false;
|
||||
}
|
||||
|
||||
int animOffset = profileOffset + 176;
|
||||
int periodOffset = profileOffset + 196;
|
||||
for (int led = 0; led < 20; led++)
|
||||
{
|
||||
byte anim = buf[animOffset + led];
|
||||
ushort period = (ushort)(
|
||||
buf[periodOffset + led * 2] |
|
||||
(buf[periodOffset + led * 2 + 1] << 8));
|
||||
|
||||
if (anim > (byte)LedAnimType.ColorFade) return false;
|
||||
if (anim == (byte)LedAnimType.Pulse && period < 2) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static void WriteU32(byte[] buf, ref int pos, uint v)
|
||||
|
||||
+12
-9
@@ -5,16 +5,18 @@
|
||||
// Alle Pakete: 8 Bytes fest
|
||||
// [0] Command/Event-ID
|
||||
// [1] key_id (Button 0–24 oder Encoder 0–3)
|
||||
// [2] r / Daten-Byte A
|
||||
// [3] g / Daten-Byte B
|
||||
// [4] b
|
||||
// [5..7] reserviert (0x00)
|
||||
// [2..7] kommandospezifische Nutzdaten
|
||||
|
||||
namespace VersaGUI;
|
||||
|
||||
public static class Protocol
|
||||
{
|
||||
public const int PacketSize = 8;
|
||||
public const int PayloadSize = 6;
|
||||
public const int ConfigSize = 740;
|
||||
public const int ConfigChunks = (ConfigSize + PayloadSize - 1) / PayloadSize;
|
||||
public const int MacroSize = 512;
|
||||
public const int MacroChunks = (MacroSize + PayloadSize - 1) / PayloadSize;
|
||||
|
||||
// ── Commands: App → Board (0x01–0x7F) ────────────────────────────────────
|
||||
public const byte CmdSetLedOverride = 0x01; // key_id, r, g, b
|
||||
@@ -25,7 +27,6 @@ public static class Protocol
|
||||
// CmdConfigBegin: Data[1] = Anzahl Chunks
|
||||
// CmdConfigData: Data[1] = Chunk-Index (0-based), Data[2..7] = 6 Bytes Nutzdaten
|
||||
// CmdConfigCommit: Board schreibt empfangene Daten in NVM
|
||||
// Firmware-Seite ist noch TODO – Kommandos sind hier schon definiert.
|
||||
public const byte CmdPing = 0x05; // Board antwortet mit EvtPong
|
||||
public const byte CmdConfigBegin = 0x10;
|
||||
public const byte CmdConfigData = 0x11;
|
||||
@@ -37,10 +38,11 @@ public static class Protocol
|
||||
public const byte CmdMacroRead = 0x23; // Board sendet aktuelle Makro-Tabelle zurück
|
||||
|
||||
// ── Events: Board → App (0x81–0xFF) ──────────────────────────────────────
|
||||
public const byte EvtKeyDown = 0x81; // key_id → HOST_COMMAND-Button gedrückt
|
||||
public const byte EvtKeyUp = 0x82; // key_id → HOST_COMMAND-Button losgelassen
|
||||
public const byte EvtEncCw = 0x83; // key_id → Encoder Schritt CW
|
||||
public const byte EvtEncCcw = 0x84; // key_id → Encoder Schritt CCW
|
||||
// Host-Events übertragen die Command-ID little-endian in Data[2..3].
|
||||
public const byte EvtKeyDown = 0x81; // Host-Action: key_id → gedrückt
|
||||
public const byte EvtKeyUp = 0x82; // Host-Action: key_id → losgelassen
|
||||
public const byte EvtEncCw = 0x83; // Host-Action: enc_id → Schritt CW
|
||||
public const byte EvtEncCcw = 0x84; // Host-Action: enc_id → Schritt CCW
|
||||
public const byte EvtPong = 0x85; // Antwort auf CmdPing
|
||||
public const byte EvtConfigAck = 0x90; // Config erfolgreich in NVM geschrieben
|
||||
public const byte EvtConfigNack = 0x91; // Config CRC/Magic ungültig
|
||||
@@ -64,6 +66,7 @@ public class SerialPacket
|
||||
public byte R => Data[2];
|
||||
public byte G => Data[3];
|
||||
public byte B => Data[4];
|
||||
public ushort DataU16 => (ushort)(Data[2] | (Data[3] << 8));
|
||||
|
||||
// Leeres Paket (für eingehende Daten)
|
||||
public SerialPacket() { }
|
||||
|
||||
+149
-107
@@ -7,7 +7,7 @@
|
||||
//
|
||||
// Threading:
|
||||
// - _readThread: Hintergrund-Thread, liest eingehende Bytes und baut Pakete zusammen.
|
||||
// - _reconnectTimer: System.Threading.Timer, versucht alle 2s neu zu verbinden.
|
||||
// - _reconnectTimer: System.Threading.Timer, versucht alle 3s neu zu verbinden.
|
||||
// - Alle Events werden auf dem UI-Thread gefeuert (via SynchronizationContext),
|
||||
// damit TrayApp direkt NotifyIcon und Menü aktualisieren kann.
|
||||
//
|
||||
@@ -36,14 +36,18 @@ public class SerialManager : IDisposable
|
||||
private System.Threading.Timer? _reconnectTimer;
|
||||
private readonly SynchronizationContext _ui;
|
||||
private readonly object _connectLock = new(); // Verhindert parallele TryConnect()-Aufrufe
|
||||
private readonly object _writeLock = new(); // Ein Paket atomar schreiben
|
||||
private readonly object _transferLock = new(); // Keine parallelen Blob-Transfers
|
||||
private bool _disposed;
|
||||
private bool _waitingAfterDisconnect; // Backoff nach Trennung aktiv
|
||||
private volatile bool _waitingAfterDisconnect; // Backoff nach Trennung aktiv
|
||||
|
||||
// Synchronisation: Board-ACK/NACK abwarten bevor nächster Transfer startet
|
||||
private readonly SemaphoreSlim _configAckGate = new(0, 1);
|
||||
private readonly SemaphoreSlim _macroAckGate = new(0, 1);
|
||||
private volatile bool _configAckOk;
|
||||
private volatile bool _macroAckOk;
|
||||
private volatile bool _configAckExpected;
|
||||
private volatile bool _macroAckExpected;
|
||||
|
||||
public bool IsConnected => _port?.IsOpen == true;
|
||||
|
||||
@@ -67,34 +71,43 @@ public class SerialManager : IDisposable
|
||||
if (!Monitor.TryEnter(_connectLock)) return;
|
||||
try
|
||||
{
|
||||
if (IsConnected || _waitingAfterDisconnect) return;
|
||||
if (_disposed || IsConnected || _waitingAfterDisconnect) return;
|
||||
|
||||
string? portName = FindPort();
|
||||
if (portName is null) return;
|
||||
|
||||
var port = new SerialPort(portName, 115200)
|
||||
SerialPort? port = null;
|
||||
try
|
||||
{
|
||||
ReadTimeout = 500,
|
||||
WriteTimeout = 500,
|
||||
DtrEnable = true, // Muss VOR Open() gesetzt werden – SAMD21 prüft DTR
|
||||
// in SerialUSB-bool für usb_serial_send(). Default ist
|
||||
// false, was alle Board→PC-Antworten still verwirft.
|
||||
// Vor Open() gesetzt verursacht es keinen Zustandswechsel
|
||||
// und triggert keinen CDC-Disconnect auf dem Board.
|
||||
};
|
||||
port = new SerialPort(portName, 115200)
|
||||
{
|
||||
ReadTimeout = 500,
|
||||
WriteTimeout = 500,
|
||||
DtrEnable = true,
|
||||
};
|
||||
|
||||
port.Open();
|
||||
port.Open();
|
||||
|
||||
// Kurz warten bis USB/CDC sich nach der Verbindungsherstellung stabilisiert
|
||||
Thread.Sleep(200);
|
||||
// Kurz warten bis USB/CDC sich nach der Verbindungsherstellung stabilisiert
|
||||
Thread.Sleep(200);
|
||||
|
||||
_port = port;
|
||||
_port = port;
|
||||
|
||||
// Lesethread starten
|
||||
_readThread = new Thread(ReadLoop) { IsBackground = true, Name = "VersaPad-Read" };
|
||||
_readThread.Start();
|
||||
// Lesethread starten
|
||||
_readThread = new Thread(ReadLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "VersaPad-Read",
|
||||
};
|
||||
_readThread.Start();
|
||||
|
||||
_ui.Post(_ => Connected?.Invoke(this, EventArgs.Empty), null);
|
||||
_ui.Post(_ => Connected?.Invoke(this, EventArgs.Empty), null);
|
||||
port = null; // Besitz liegt ab hier bei _port/ReadLoop.
|
||||
}
|
||||
finally
|
||||
{
|
||||
port?.Dispose();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -109,10 +122,16 @@ public class SerialManager : IDisposable
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
var port = _port;
|
||||
_port = null;
|
||||
SerialPort? port = Interlocked.Exchange(ref _port, null);
|
||||
try { port?.Close(); } catch { }
|
||||
port?.Dispose();
|
||||
|
||||
_configAckOk = false;
|
||||
_macroAckOk = false;
|
||||
_configAckExpected = false;
|
||||
_macroAckExpected = false;
|
||||
ReleaseGate(_configAckGate);
|
||||
ReleaseGate(_macroAckGate);
|
||||
}
|
||||
|
||||
// ── COM-Port-Erkennung per WMI ────────────────────────────────────────────
|
||||
@@ -201,11 +220,22 @@ public class SerialManager : IDisposable
|
||||
|
||||
// ── Senden ───────────────────────────────────────────────────────────────
|
||||
|
||||
public void Send(SerialPacket pkt)
|
||||
public bool Send(SerialPacket pkt)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
try { _port!.Write(pkt.Data, 0, Protocol.PacketSize); }
|
||||
catch { /* Sendefehler ignorieren – ReadLoop erkennt Disconnect */ }
|
||||
lock (_writeLock)
|
||||
{
|
||||
SerialPort? port = _port;
|
||||
if (port?.IsOpen != true) return false;
|
||||
try
|
||||
{
|
||||
port.Write(pkt.Data, 0, Protocol.PacketSize);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
@@ -231,72 +261,86 @@ public class SerialManager : IDisposable
|
||||
// Gibt den wartenden SendConfig()/SendMacros()-Thread frei.
|
||||
public void SignalConfigAck()
|
||||
{
|
||||
if (!_configAckExpected) return;
|
||||
_configAckOk = true;
|
||||
if (_configAckGate.CurrentCount == 0) _configAckGate.Release();
|
||||
ReleaseGate(_configAckGate);
|
||||
}
|
||||
|
||||
public void SignalConfigNack()
|
||||
{
|
||||
if (!_configAckExpected) return;
|
||||
_configAckOk = false;
|
||||
if (_configAckGate.CurrentCount == 0) _configAckGate.Release();
|
||||
ReleaseGate(_configAckGate);
|
||||
}
|
||||
|
||||
public void SignalMacroAck()
|
||||
{
|
||||
if (!_macroAckExpected) return;
|
||||
_macroAckOk = true;
|
||||
if (_macroAckGate.CurrentCount == 0) _macroAckGate.Release();
|
||||
ReleaseGate(_macroAckGate);
|
||||
}
|
||||
|
||||
public void SignalMacroNack()
|
||||
{
|
||||
if (!_macroAckExpected) return;
|
||||
_macroAckOk = false;
|
||||
if (_macroAckGate.CurrentCount == 0) _macroAckGate.Release();
|
||||
ReleaseGate(_macroAckGate);
|
||||
}
|
||||
|
||||
// Makro-Tabelle (256 Bytes) in 6-Byte-Chunks senden.
|
||||
private static void ReleaseGate(SemaphoreSlim gate)
|
||||
{
|
||||
if (gate.CurrentCount != 0) return;
|
||||
try { gate.Release(); }
|
||||
catch (SemaphoreFullException) { }
|
||||
}
|
||||
|
||||
private static void ResetGate(SemaphoreSlim gate)
|
||||
{
|
||||
while (gate.Wait(0)) { }
|
||||
}
|
||||
|
||||
// Makro-Tabelle (512 Bytes) in 6-Byte-Chunks senden.
|
||||
// Gibt true zurück wenn das Board MACRO_ACK gesendet hat.
|
||||
public bool SendMacros(MacroTable macros)
|
||||
{
|
||||
byte[] data = macros.ToBytes();
|
||||
const int payload = 6;
|
||||
int chunks = (data.Length + payload - 1) / payload; // 43
|
||||
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX MacroBegin chunks={chunks} dataLen={data.Length} connected={IsConnected}\n");
|
||||
|
||||
Send(new SerialPacket(Protocol.CmdMacroBegin, (byte)chunks));
|
||||
Thread.Sleep(10);
|
||||
|
||||
for (int i = 0; i < chunks; i++)
|
||||
lock (_transferLock)
|
||||
{
|
||||
var pkt = new SerialPacket();
|
||||
pkt.Data[0] = Protocol.CmdMacroData;
|
||||
pkt.Data[1] = (byte)i;
|
||||
int offset = i * payload;
|
||||
int count = Math.Min(payload, data.Length - offset);
|
||||
Buffer.BlockCopy(data, offset, pkt.Data, 2, count);
|
||||
Send(pkt);
|
||||
if (i % 10 == 0)
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX MacroData chunk {i}/{chunks}\n");
|
||||
Thread.Sleep(5);
|
||||
if (!macros.IsValid() || !IsConnected) return false;
|
||||
byte[] data = macros.ToBytes();
|
||||
|
||||
ResetGate(_macroAckGate);
|
||||
_macroAckOk = false;
|
||||
_macroAckExpected = false;
|
||||
|
||||
if (!Send(new SerialPacket(
|
||||
Protocol.CmdMacroBegin, (byte)Protocol.MacroChunks)))
|
||||
return false;
|
||||
Thread.Sleep(10);
|
||||
|
||||
for (int i = 0; i < Protocol.MacroChunks; i++)
|
||||
{
|
||||
var pkt = new SerialPacket();
|
||||
pkt.Data[0] = Protocol.CmdMacroData;
|
||||
pkt.Data[1] = (byte)i;
|
||||
int offset = i * Protocol.PayloadSize;
|
||||
int count = Math.Min(Protocol.PayloadSize, data.Length - offset);
|
||||
Buffer.BlockCopy(data, offset, pkt.Data, 2, count);
|
||||
if (!Send(pkt)) return false;
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
|
||||
Thread.Sleep(10);
|
||||
_macroAckExpected = true;
|
||||
if (!Send(new SerialPacket(Protocol.CmdMacroCommit)))
|
||||
{
|
||||
_macroAckExpected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool gateAcquired = _macroAckGate.Wait(3000);
|
||||
_macroAckExpected = false;
|
||||
return gateAcquired && _macroAckOk;
|
||||
}
|
||||
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX MacroCommit (sent {chunks} chunks)\n");
|
||||
|
||||
// Sicherstellen dass das Gate leer ist bevor wir warten
|
||||
while (_macroAckGate.CurrentCount > 0) _macroAckGate.Wait(0);
|
||||
|
||||
Thread.Sleep(10);
|
||||
Send(new SerialPacket(Protocol.CmdMacroCommit));
|
||||
|
||||
// Auf MACRO_ACK/NACK warten – Board braucht bis zu ~600ms für NVM-Erase + Write (2 Rows)
|
||||
bool gateAcquired = _macroAckGate.Wait(3000);
|
||||
bool success = gateAcquired && _macroAckOk;
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX MacroAck gateAcquired={gateAcquired} ok={success}\n");
|
||||
return success;
|
||||
}
|
||||
|
||||
// Config in 6-Byte-Chunks an das Board senden.
|
||||
@@ -304,46 +348,44 @@ public class SerialManager : IDisposable
|
||||
// Gibt true zurück wenn das Board CONFIG_ACK gesendet hat.
|
||||
public bool SendConfig(DeviceConfig config)
|
||||
{
|
||||
byte[] data = config.ToBytes();
|
||||
const int payload = 6; // Nutzbytes pro Paket (8 - 2 Header-Bytes)
|
||||
int chunks = (data.Length + payload - 1) / payload;
|
||||
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX ConfigBegin chunks={chunks} dataLen={data.Length} connected={IsConnected}\n");
|
||||
|
||||
Send(new SerialPacket(Protocol.CmdConfigBegin, (byte)chunks));
|
||||
Thread.Sleep(10);
|
||||
|
||||
for (int i = 0; i < chunks; i++)
|
||||
lock (_transferLock)
|
||||
{
|
||||
var pkt = new SerialPacket();
|
||||
pkt.Data[0] = Protocol.CmdConfigData;
|
||||
pkt.Data[1] = (byte)i;
|
||||
int offset = i * payload;
|
||||
int count = Math.Min(payload, data.Length - offset);
|
||||
Buffer.BlockCopy(data, offset, pkt.Data, 2, count);
|
||||
Send(pkt);
|
||||
if (i % 10 == 0)
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX ConfigData chunk {i}/{chunks}\n");
|
||||
Thread.Sleep(5); // Firmware-Loop Zeit geben den Puffer zu leeren
|
||||
if (!config.TryValidate(out _) || !IsConnected) return false;
|
||||
byte[] data = config.ToBytes();
|
||||
|
||||
ResetGate(_configAckGate);
|
||||
_configAckOk = false;
|
||||
_configAckExpected = false;
|
||||
|
||||
if (!Send(new SerialPacket(
|
||||
Protocol.CmdConfigBegin, (byte)Protocol.ConfigChunks)))
|
||||
return false;
|
||||
Thread.Sleep(10);
|
||||
|
||||
for (int i = 0; i < Protocol.ConfigChunks; i++)
|
||||
{
|
||||
var pkt = new SerialPacket();
|
||||
pkt.Data[0] = Protocol.CmdConfigData;
|
||||
pkt.Data[1] = (byte)i;
|
||||
int offset = i * Protocol.PayloadSize;
|
||||
int count = Math.Min(Protocol.PayloadSize, data.Length - offset);
|
||||
Buffer.BlockCopy(data, offset, pkt.Data, 2, count);
|
||||
if (!Send(pkt)) return false;
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
|
||||
Thread.Sleep(10);
|
||||
_configAckExpected = true;
|
||||
if (!Send(new SerialPacket(Protocol.CmdConfigCommit)))
|
||||
{
|
||||
_configAckExpected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool gateAcquired = _configAckGate.Wait(3000);
|
||||
_configAckExpected = false;
|
||||
return gateAcquired && _configAckOk;
|
||||
}
|
||||
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX ConfigCommit (sent {chunks} chunks)\n");
|
||||
|
||||
// Sicherstellen dass das Gate leer ist bevor wir warten
|
||||
while (_configAckGate.CurrentCount > 0) _configAckGate.Wait(0);
|
||||
|
||||
Thread.Sleep(10);
|
||||
Send(new SerialPacket(Protocol.CmdConfigCommit));
|
||||
|
||||
// Auf CONFIG_ACK/NACK warten – Board braucht bis zu ~1s für NVM-Erase + Write (3 Rows)
|
||||
bool gateAcquired = _configAckGate.Wait(3000);
|
||||
bool success = gateAcquired && _configAckOk;
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX ConfigAck gateAcquired={gateAcquired} ok={success}\n");
|
||||
return success;
|
||||
}
|
||||
|
||||
// ── IDisposable ───────────────────────────────────────────────────────────
|
||||
|
||||
+55
-38
@@ -10,8 +10,8 @@
|
||||
// Beenden
|
||||
//
|
||||
// Board-Events:
|
||||
// KEY_DOWN / ENC_CW / ENC_CCW mit Aktion HOST_COMMAND werden hier empfangen.
|
||||
// Aktuell: Benachrichtigung anzeigen (HOST_COMMAND-Ausführung folgt später).
|
||||
// KEY_DOWN/KEY_UP/ENC_CW/ENC_CCW enthalten die konfigurierte Host-Command-ID.
|
||||
// Eine konkrete Host-Aktionsausführung ist bewusst noch nicht implementiert.
|
||||
|
||||
namespace VersaGUI;
|
||||
|
||||
@@ -25,9 +25,12 @@ public class TrayApp : ApplicationContext
|
||||
private ToolStripMenuItem _statusItem = null!;
|
||||
private ConfigForm? _configForm;
|
||||
|
||||
// Empfangspuffer für eingehende Dumps vom Board
|
||||
private byte[]? _rxConfigBuf;
|
||||
private byte[]? _rxMacroBuf;
|
||||
private readonly ChunkTransferBuffer _configRx =
|
||||
new(Protocol.ConfigSize, Protocol.ConfigChunks);
|
||||
private readonly ChunkTransferBuffer _macroRx =
|
||||
new(Protocol.MacroSize, Protocol.MacroChunks);
|
||||
private int _configReadAttempts;
|
||||
private int _macroReadAttempts;
|
||||
|
||||
public TrayApp()
|
||||
{
|
||||
@@ -92,9 +95,11 @@ public class TrayApp : ApplicationContext
|
||||
_tray.Icon = SystemIcons.Information;
|
||||
_statusItem.Text = "● Verbunden";
|
||||
|
||||
// Config und Makro-Tabelle vom Board laden
|
||||
// Dumps bewusst sequenziell anfordern, damit sich zwei lange
|
||||
// Board→Host-Transfers nicht gegenseitig überholen können.
|
||||
_configReadAttempts = 1;
|
||||
_macroReadAttempts = 0;
|
||||
_serial.RequestConfig();
|
||||
_serial.RequestMacros();
|
||||
}
|
||||
|
||||
private void OnDisconnected(object? sender, EventArgs e)
|
||||
@@ -102,24 +107,20 @@ public class TrayApp : ApplicationContext
|
||||
_tray.Text = "VersaPad – nicht verbunden";
|
||||
_tray.Icon = SystemIcons.Application;
|
||||
_statusItem.Text = "○ Nicht verbunden";
|
||||
_configRx.Reset();
|
||||
_macroRx.Reset();
|
||||
}
|
||||
|
||||
private void OnPacket(object? sender, SerialPacket pkt)
|
||||
{
|
||||
// Debug: alle empfangenen Pakete loggen
|
||||
File.AppendAllText(
|
||||
Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} RX cmd=0x{pkt.Command:X2} key={pkt.KeyId}\n");
|
||||
|
||||
switch (pkt.Command)
|
||||
{
|
||||
case Protocol.EvtKeyDown:
|
||||
// TODO: HOST_COMMAND-Aktion aus Config laden und ausführen
|
||||
break;
|
||||
|
||||
case Protocol.EvtKeyUp:
|
||||
case Protocol.EvtEncCw:
|
||||
case Protocol.EvtEncCcw:
|
||||
// TODO: Encoder HOST_COMMAND
|
||||
// Command-ID steht jetzt direkt in pkt.DataU16. Die sichere
|
||||
// Zuordnung zu konkreten Host-Aktionen folgt separat.
|
||||
break;
|
||||
|
||||
case Protocol.EvtPong:
|
||||
@@ -137,25 +138,30 @@ public class TrayApp : ApplicationContext
|
||||
|
||||
// ── Config-Dump vom Board ─────────────────────────────────────────
|
||||
case Protocol.EvtConfigBegin:
|
||||
_rxConfigBuf = new byte[740]; // sizeof(SDeviceConfig) = 740 (Version 3)
|
||||
_configRx.Begin(pkt.KeyId);
|
||||
break;
|
||||
|
||||
case Protocol.EvtConfigData:
|
||||
if (_rxConfigBuf != null)
|
||||
{
|
||||
int offset = pkt.KeyId * 6;
|
||||
for (int i = 0; i < 6; i++)
|
||||
if (offset + i < _rxConfigBuf.Length)
|
||||
_rxConfigBuf[offset + i] = pkt.Data[2 + i];
|
||||
}
|
||||
_configRx.Add(pkt);
|
||||
break;
|
||||
|
||||
case Protocol.EvtConfigEnd:
|
||||
if (_rxConfigBuf != null)
|
||||
if (_configRx.TryComplete(pkt.KeyId, out byte[] configData) &&
|
||||
_config.FromBytes(configData))
|
||||
{
|
||||
_config.FromBytes(_rxConfigBuf);
|
||||
_rxConfigBuf = null;
|
||||
_configForm?.RefreshAll();
|
||||
|
||||
_macroReadAttempts = 1;
|
||||
_serial.RequestMacros();
|
||||
}
|
||||
else if (_configReadAttempts < 2 && _serial.IsConnected)
|
||||
{
|
||||
_configReadAttempts++;
|
||||
_serial.RequestConfig();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowTransferError("Config");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -169,29 +175,40 @@ public class TrayApp : ApplicationContext
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroBegin:
|
||||
_rxMacroBuf = new byte[512]; // sizeof(SMacroTable) = 512 (8 Steps)
|
||||
_macroRx.Begin(pkt.KeyId);
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroData:
|
||||
if (_rxMacroBuf != null)
|
||||
{
|
||||
int offset = pkt.KeyId * 6;
|
||||
for (int i = 0; i < 6; i++)
|
||||
if (offset + i < _rxMacroBuf.Length)
|
||||
_rxMacroBuf[offset + i] = pkt.Data[2 + i];
|
||||
}
|
||||
_macroRx.Add(pkt);
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroEnd:
|
||||
if (_rxMacroBuf != null)
|
||||
if (_macroRx.TryComplete(pkt.KeyId, out byte[] macroData) &&
|
||||
_macros.FromBytes(macroData))
|
||||
{
|
||||
_macros.FromBytes(_rxMacroBuf);
|
||||
_rxMacroBuf = null;
|
||||
_configForm?.RefreshAll();
|
||||
}
|
||||
else if (_macroReadAttempts < 2 && _serial.IsConnected)
|
||||
{
|
||||
_macroReadAttempts++;
|
||||
_serial.RequestMacros();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowTransferError("Makro-Tabelle");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowTransferError(string name)
|
||||
{
|
||||
_tray.BalloonTipTitle = "VersaPad – Übertragungsfehler";
|
||||
_tray.BalloonTipText = $"{name} konnte nicht vollständig gelesen werden.";
|
||||
_tray.BalloonTipIcon = ToolTipIcon.Warning;
|
||||
_tray.ShowBalloonTip(4000);
|
||||
}
|
||||
|
||||
// ── Aufräumen ─────────────────────────────────────────────────────────────
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using VersaGUI;
|
||||
|
||||
static void Assert(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
static void TestDeviceConfigRoundTrip()
|
||||
{
|
||||
var source = new DeviceConfig();
|
||||
source.Profiles[2].MxActions[4].Type = ActionType.HostCommand;
|
||||
source.Profiles[2].MxActions[4].Data = 0xBEEF;
|
||||
source.ActiveProfileIndex = 2;
|
||||
|
||||
byte[] data = source.ToBytes();
|
||||
Assert(data.Length == Protocol.ConfigSize, "Configgröße weicht ab.");
|
||||
|
||||
ushort storedCrc = (ushort)(data[5] | (data[6] << 8));
|
||||
Assert(
|
||||
storedCrc == DeviceConfig.Crc16(data, 7, data.Length - 7),
|
||||
"Config-CRC weicht ab.");
|
||||
|
||||
var target = new DeviceConfig();
|
||||
Assert(target.FromBytes(data), "Gültige Config wurde abgelehnt.");
|
||||
Assert(target.ActiveProfileIndex == 2, "Aktives Profil ging verloren.");
|
||||
Assert(
|
||||
target.Profiles[2].MxActions[4].Data == 0xBEEF,
|
||||
"Host-Command-ID ging verloren.");
|
||||
}
|
||||
|
||||
static void TestInvalidConfigIsRejected()
|
||||
{
|
||||
byte[] data = new DeviceConfig().ToBytes();
|
||||
data[7] = 3;
|
||||
ushort crc = DeviceConfig.Crc16(data, 7, data.Length - 7);
|
||||
data[5] = (byte)crc;
|
||||
data[6] = (byte)(crc >> 8);
|
||||
|
||||
Assert(!new DeviceConfig().FromBytes(data), "Ungültiger Profilindex wurde akzeptiert.");
|
||||
|
||||
var invalidPulse = new DeviceConfig();
|
||||
invalidPulse.Profiles[0].LedAnim[0] = LedAnimType.Pulse;
|
||||
invalidPulse.Profiles[0].LedPeriod[0] = 1;
|
||||
Assert(!invalidPulse.TryValidate(out _), "Unsichere Pulse-Periode wurde akzeptiert.");
|
||||
}
|
||||
|
||||
static void TestMacroValidation()
|
||||
{
|
||||
var macros = new MacroTable();
|
||||
macros.Steps[31, 7].Keycode = 0x65;
|
||||
byte[] data = macros.ToBytes();
|
||||
Assert(data.Length == Protocol.MacroSize, "Makrogröße weicht ab.");
|
||||
|
||||
var copy = new MacroTable();
|
||||
Assert(copy.FromBytes(data), "Gültige Makrotabelle wurde abgelehnt.");
|
||||
|
||||
data[0] = 0x66;
|
||||
Assert(!copy.FromBytes(data), "Ungültiger HID-Keycode wurde akzeptiert.");
|
||||
}
|
||||
|
||||
static void TestChunkValidation()
|
||||
{
|
||||
var receiver = new ChunkTransferBuffer(13, 3);
|
||||
Assert(receiver.Begin(3), "Gültige Chunkzahl wurde abgelehnt.");
|
||||
|
||||
for (byte chunk = 0; chunk < 3; chunk++)
|
||||
{
|
||||
var packet = new SerialPacket();
|
||||
packet.Data[1] = chunk;
|
||||
for (int i = 0; i < Protocol.PayloadSize; i++)
|
||||
packet.Data[2 + i] = (byte)(chunk * Protocol.PayloadSize + i);
|
||||
Assert(receiver.Add(packet), $"Chunk {chunk} wurde abgelehnt.");
|
||||
}
|
||||
|
||||
Assert(receiver.TryComplete(3, out byte[] result), "Vollständiger Dump wurde abgelehnt.");
|
||||
Assert(result.Length == 13 && result[12] == 12, "Chunkdaten wurden falsch zusammengesetzt.");
|
||||
|
||||
Assert(receiver.Begin(3), "Zweiter Transfer konnte nicht starten.");
|
||||
var duplicate = new SerialPacket();
|
||||
Assert(receiver.Add(duplicate), "Erster Chunk wurde abgelehnt.");
|
||||
Assert(!receiver.Add(duplicate), "Doppelter Chunk wurde akzeptiert.");
|
||||
Assert(!receiver.TryComplete(3, out _), "Ungültiger Dump wurde abgeschlossen.");
|
||||
}
|
||||
|
||||
static void TestHostCommandPayload()
|
||||
{
|
||||
var packet = new SerialPacket(Protocol.EvtEncCw, 2, 0x34, 0x12);
|
||||
Assert(packet.DataU16 == 0x1234, "16-Bit-Command-ID wurde falsch dekodiert.");
|
||||
}
|
||||
|
||||
TestDeviceConfigRoundTrip();
|
||||
TestInvalidConfigIsRejected();
|
||||
TestMacroValidation();
|
||||
TestChunkValidation();
|
||||
TestHostCommandPayload();
|
||||
|
||||
Console.WriteLine("VersaGUI contract tests passed.");
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\VersaGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user