Compare commits
6 Commits
2eb43826ce
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 24fe162833 | |||
| e01666ab59 | |||
| 851e548166 | |||
| 96f00f0893 | |||
| 0ad7858a09 | |||
| 0d6ab21733 |
@@ -61,6 +61,7 @@ Die App erscheint als Icon in der Windows-Taskleiste (System Tray). Kein Hauptfe
|
||||
| 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
|
||||
|
||||
@@ -68,6 +69,7 @@ Die App erscheint als Icon in der Windows-Taskleiste (System Tray). Kein Hauptfe
|
||||
|---|-------------|--------|
|
||||
| 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
|
||||
|
||||
@@ -86,6 +88,8 @@ Die App erscheint als Icon in der Windows-Taskleiste (System Tray). Kein Hauptfe
|
||||
|---|-------------|--------|
|
||||
| 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
|
||||
|
||||
@@ -124,6 +128,7 @@ Die App erscheint als Icon in der Windows-Taskleiste (System Tray). Kein Hauptfe
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -191,6 +196,10 @@ PC sendet: CMD_MACRO_BEGIN (0x20, Chunk-Anzahl = 43)
|
||||
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 |
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# VersaGUI – Architektur-Übersicht
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
| Merkmal | Wert |
|
||||
|---|---|
|
||||
| Sprache | C# / .NET 7 |
|
||||
| UI-Framework | WinForms (`[STAThread]`) |
|
||||
| Einstiegspunkt | `Program.cs` → `Application.Run(new TrayApp())` |
|
||||
| Laufzeitmodell | `ApplicationContext` (kein `Form` als Hauptfenster) |
|
||||
| Threading | UI-Thread + 1 Hintergrund-Lese-Thread + Timer-Thread |
|
||||
|
||||
## Komponentenübersicht
|
||||
|
||||
| Datei | Verantwortung |
|
||||
|---|---|
|
||||
| `TrayApp` | `ApplicationContext`; hält Tray-Icon, öffnet ConfigForm, verarbeitet Board-Events |
|
||||
| `SerialManager` | Verbindungsverwaltung, WMI-Erkennung, Lese-Thread, Sende-Methoden |
|
||||
| `ConfigForm` | Hauptfenster (Grid + Encoder-Panel + Footer); öffnet ActionDialog |
|
||||
| `ActionDialog` | Modaler Dialog zum Bearbeiten einer Aktion + LED-Einstellungen |
|
||||
| `DeviceConfig` | C#-Spiegel von `SDeviceConfig`; Serialisierung/Deserialisierung (223 B) |
|
||||
| `MacroTable` | C#-Spiegel von `SMacroTable`; Serialisierung/Deserialisierung (256 B) |
|
||||
| `ConfigJson` | JSON-Import/Export für `DeviceConfig` |
|
||||
| `Protocol` | Konstanten für alle Command/Event-IDs (spiegelt `usb_serial.h`) |
|
||||
|
||||
## Datenfluss
|
||||
|
||||
```
|
||||
Board → SerialManager (ReadLoop, BG-Thread)
|
||||
→ SynchronizationContext.Post (→ UI-Thread)
|
||||
→ TrayApp.OnPacket()
|
||||
├── Config-Dump: _rxConfigBuf aufbauen → DeviceConfig.FromBytes()
|
||||
├── Makro-Dump: _rxMacroBuf aufbauen → MacroTable.FromBytes()
|
||||
└── HOST_COMMAND-Events: (TODO: Aktion ausführen)
|
||||
|
||||
Benutzer → ConfigForm → ActionDialog
|
||||
→ DeviceConfig / MacroTable (in-memory ändern)
|
||||
→ SerialManager.SendConfig() + SendMacros() (BG-Task)
|
||||
→ Board (chunked, 6 B/Paket)
|
||||
```
|
||||
|
||||
## Threading-Modell
|
||||
|
||||
```
|
||||
UI-Thread : TrayApp, ConfigForm, ActionDialog, alle WinForms-Controls
|
||||
BG-Thread : SerialManager.ReadLoop() – blockiert auf ReadByte()
|
||||
Timer-Thread : SerialManager._reconnectTimer → TryConnect() alle 3 s
|
||||
Sende-Task : ConfigForm.OnSave() → Task.Run() (blockiert ~400 ms für Transfer)
|
||||
```
|
||||
|
||||
Alle Board-Events werden per `SynchronizationContext.Post` auf den UI-Thread gepostet. Controls dürfen nie vom BG-Thread angefasst werden.
|
||||
|
||||
## Verbindungslebenszyklus
|
||||
|
||||
```
|
||||
Start → Timer feuert → TryConnect() → WMI-Suche (VID 0x239A / PID 0x0042)
|
||||
→ SerialPort öffnen (DtrEnable=true VOR Open()!)
|
||||
→ 200 ms warten → ReadLoop starten → Connected-Event → RequestConfig() + RequestMacros()
|
||||
|
||||
Disconnect → ReadLoop bricht ab → Disconnected-Event → 5 s Backoff → Timer läuft weiter
|
||||
```
|
||||
|
||||
## Invarianten / Constraints
|
||||
|
||||
- **DtrEnable=true muss VOR `Open()` gesetzt werden**: SAMD21 prüft DTR für `usb_serial_send()`. Der Default-Wert false würde alle Board→PC-Antworten still verwerfen.
|
||||
- **IOException ≠ Disconnect**: .NET 7 wirft `IOException` statt `TimeoutException` bei `ReadByte()`-Timeout. Nur als echten Fehler behandeln wenn `_port.IsOpen == false`.
|
||||
- **Packed-kompatible Serialisierung**: `DeviceConfig.ToBytes()` muss exakt 223 B in derselben Reihenfolge wie `SDeviceConfig` (packed C++) erzeugen. Jede Änderung am Firmware-Layout muss hier gespiegelt werden.
|
||||
- **Config-Version**: `DeviceConfig.Version == 2`. `FromBytes()` prüft Magic + Version + CRC; schlägt einer fehl → Methode gibt `false` zurück, Config bleibt unverändert.
|
||||
- **Debug-Log**: `versapad_rx.txt` in `%TEMP%` – vor Release-Nutzung entfernen (TODO).
|
||||
@@ -0,0 +1,95 @@
|
||||
# SerialManager
|
||||
|
||||
**Datei:** `SerialManager.cs`
|
||||
|
||||
## Verantwortung
|
||||
|
||||
- COM-Port-Erkennung per WMI (VID/PID)
|
||||
- Verbindungsaufbau und automatischer Reconnect
|
||||
- Hintergrund-Lese-Thread mit Paket-Assembler
|
||||
- Sende-Methoden für alle Protokoll-Operationen
|
||||
- ACK/NACK-Synchronisation zwischen ReadLoop-Thread und `SendConfig`/`SendMacros`-Task
|
||||
|
||||
## COM-Port-Erkennung
|
||||
|
||||
```csharp
|
||||
VidPid = "VID_239A&PID_0042" // muss mit platformio.ini übereinstimmen
|
||||
|
||||
SELECT Name, DeviceID FROM Win32_PnPEntity WHERE DeviceID LIKE '%VID_239A&PID_0042%'
|
||||
// Name enthält z.B. "USB Serial Device (COM3)" → extrahiert "COM3"
|
||||
```
|
||||
|
||||
Erkennt das Board auch bei wechselnder COM-Nummer. Funktioniert auch nach USB-Reinitialisierung (Kabel abziehen/stecken nach SWD-Flash).
|
||||
|
||||
## Verbindungsaufbau (TryConnect)
|
||||
|
||||
```
|
||||
Monitor.TryEnter(_connectLock) – nur ein gleichzeitiger Versuch
|
||||
FindPort() per WMI
|
||||
SerialPort erstellen:
|
||||
ReadTimeout = 500 ms
|
||||
WriteTimeout = 500 ms
|
||||
DtrEnable = true ← VOR Open() setzen! (SAMD21 CDC-Constraint)
|
||||
port.Open()
|
||||
Thread.Sleep(200) – CDC stabilisieren
|
||||
ReadLoop-Thread starten
|
||||
Connected-Event → UI-Thread
|
||||
```
|
||||
|
||||
**Reconnect-Timer**: alle 3 s, Start nach 500 ms. 5 s Backoff (`_waitingAfterDisconnect`) nach Verbindungsverlust damit Board vollständig re-enumerieren kann.
|
||||
|
||||
## ReadLoop (Hintergrund-Thread)
|
||||
|
||||
```
|
||||
while (port.IsOpen):
|
||||
b = port.ReadByte() – blockiert 500 ms (ReadTimeout)
|
||||
buf[bufFill++] = b
|
||||
if bufFill < 8: continue
|
||||
→ SerialPacket fertig → SynchronizationContext.Post → PacketReceived auf UI-Thread
|
||||
bufFill = 0
|
||||
|
||||
Bei IOException:
|
||||
if port.IsOpen: ignorieren ← .NET 7: IOException = normaler Timeout (kein Fehler!)
|
||||
else: break → Disconnect
|
||||
```
|
||||
|
||||
## ACK/NACK-Synchronisation
|
||||
|
||||
`SendConfig` und `SendMacros` blockieren nach COMMIT auf ein `SemaphoreSlim`-Gate bis das Board antwortet:
|
||||
|
||||
```
|
||||
_configAckGate / _macroAckGate – SemaphoreSlim(0, 1)
|
||||
_configAckOk / _macroAckOk – volatile bool (true = ACK, false = NACK)
|
||||
```
|
||||
|
||||
| Methode | Aufruf durch | Wirkung |
|
||||
|---|---|---|
|
||||
| `SignalConfigAck()` | TrayApp bei EvtConfigAck | `_configAckOk = true`, Gate freigeben |
|
||||
| `SignalConfigNack()` | TrayApp bei EvtConfigNack | `_configAckOk = false`, Gate freigeben |
|
||||
| `SignalMacroAck()` | TrayApp bei EvtMacroAck | `_macroAckOk = true`, Gate freigeben |
|
||||
| `SignalMacroNack()` | TrayApp bei EvtMacroNack | `_macroAckOk = false`, Gate freigeben |
|
||||
|
||||
`SendConfig()` und `SendMacros()` geben `bool` zurück (`true` = ACK erhalten, `false` = NACK oder Timeout nach 3 s).
|
||||
|
||||
## Sende-Methoden
|
||||
|
||||
| Methode | Rückgabe | Funktion |
|
||||
|---|---|---|
|
||||
| `Send(pkt)` | void | Rohe 8-Byte-Übertragung (fire-and-forget) |
|
||||
| `SetLedOverride(keyId, r, g, b)` | void | CMD 0x01 |
|
||||
| `ClearLedOverride(keyId)` | void | CMD 0x02 |
|
||||
| `SetLedBase(keyId, r, g, b)` | void | CMD 0x03 |
|
||||
| `RequestConfig()` | void | CMD 0x13 – Board antwortet mit Config-Dump |
|
||||
| `RequestMacros()` | void | CMD 0x23 – Board antwortet mit Makro-Dump |
|
||||
| `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.
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Gefeuert wenn |
|
||||
|---|---|
|
||||
| `Connected` | Verbindung erfolgreich hergestellt (auf UI-Thread) |
|
||||
| `Disconnected` | Verbindung verloren (auf UI-Thread) |
|
||||
| `PacketReceived` | Vollständiges 8-Byte-Paket empfangen (auf UI-Thread) |
|
||||
@@ -0,0 +1,120 @@
|
||||
# DeviceConfig & MacroTable
|
||||
|
||||
**Datei:** `DeviceConfig.cs`
|
||||
|
||||
## Überblick
|
||||
|
||||
C#-Spiegel der Firmware-Structs. Muss byte-kompatibel mit `SDeviceConfig` (nvm_config.h) und `SMacroTable` (macro_config.h) sein.
|
||||
|
||||
---
|
||||
|
||||
## DeviceConfig
|
||||
|
||||
### Felder
|
||||
|
||||
| Feld | Typ | Inhalt |
|
||||
|---|---|---|
|
||||
| `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) |
|
||||
|
||||
### Serialisierungs-Layout (ToBytes / FromBytes, 740 B)
|
||||
|
||||
```
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
Profil 1 (Offset 268, 236 B): identisches Layout
|
||||
Profil 2 (Offset 504, 236 B): identisches Layout
|
||||
```
|
||||
|
||||
### CRC16-CCITT
|
||||
|
||||
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)
|
||||
}
|
||||
```
|
||||
|
||||
`DeviceAction.Display` gibt einen lesbaren String zurück (z.B. `"Strg+C"`, `"Play/Pause"`, `"→ Profil 2"`, `"→ Nächstes Profil"`).
|
||||
|
||||
---
|
||||
|
||||
## 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; }
|
||||
}
|
||||
```
|
||||
|
||||
### Slot-Konvention
|
||||
|
||||
| Slots | Verwendung |
|
||||
|---|---|
|
||||
| 0–19 | MX-Button `mxIdx` (`MacroTable.SlotForMx(mxIdx)`) |
|
||||
| 20–31 | Encoder: `20 + enc * 3 + actIdx` (`MacroTable.SlotForEncoder(enc, actIdx)`) |
|
||||
|
||||
### Serialisierung (512 B)
|
||||
|
||||
32 Slots × 8 Steps × 2 B = 512 B. Keycode zuerst, dann Modifier. Kein Magic/CRC (Board akzeptiert jeden Inhalt).
|
||||
|
||||
---
|
||||
|
||||
## LedAnimType
|
||||
|
||||
```csharp
|
||||
public enum LedAnimType : byte {
|
||||
Static=0, Blink=1, Pulse=2, ColorCycle=5
|
||||
}
|
||||
```
|
||||
|
||||
Werte entsprechen `LEDAnim` in der Firmware. `FADE_IN` (3) und `FADE_OUT` (4) existieren in der Firmware aber nicht in der GUI (nicht konfigurierbar).
|
||||
@@ -0,0 +1,66 @@
|
||||
# TrayApp
|
||||
|
||||
**Datei:** `TrayApp.cs`
|
||||
|
||||
## Verantwortung
|
||||
|
||||
- App-Lebenszyklus als `ApplicationContext` (kein Hauptfenster)
|
||||
- Tray-Icon mit Verbindungsstatus und Kontextmenü
|
||||
- Empfang und Routing aller Board-Events (via `SerialManager.PacketReceived`)
|
||||
- Config/Makro-Dump-Empfang (chunked, via `_rxConfigBuf` / `_rxMacroBuf`)
|
||||
- ACK/NACK-Weiterleitung an SerialManager
|
||||
- Öffnen des `ConfigForm` (nur eine Instanz gleichzeitig)
|
||||
|
||||
## Tray-Menü
|
||||
|
||||
```
|
||||
[●/○] VersaPad – verbunden / nicht verbunden (disabled, nur Anzeige)
|
||||
──────────────────────────────────────────────
|
||||
Konfiguration... → OpenConfigForm()
|
||||
──────────────────────────────────────────────
|
||||
Beenden → Application.Exit()
|
||||
```
|
||||
|
||||
Icon und Tooltip spiegeln den Verbindungsstatus:
|
||||
- Verbunden: `SystemIcons.Information`, Text "VersaPad – verbunden"
|
||||
- Getrennt: `SystemIcons.Application`, Text "VersaPad – nicht verbunden"
|
||||
|
||||
(TODO: eigenes Icon einbinden)
|
||||
|
||||
## Board-Event-Handling (OnPacket)
|
||||
|
||||
| Event-ID | Aktion |
|
||||
|---|---|
|
||||
| `EvtConfigBegin` | `_rxConfigBuf = new byte[740]` |
|
||||
| `EvtConfigData` | Chunk in `_rxConfigBuf` eintragen (`KeyId * 6` = Byte-Offset) |
|
||||
| `EvtConfigEnd` | `DeviceConfig.FromBytes()` → `ConfigForm.RefreshAll()` |
|
||||
| `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()` |
|
||||
| `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 |
|
||||
|
||||
ACK/NACK-Events zeigen keine eigene MessageBox mehr — das Ergebnis wird nach Abschluss beider Transfers gebündelt in `ConfigForm.OnSave()` angezeigt.
|
||||
|
||||
## Verbindungslebenszyklus
|
||||
|
||||
```
|
||||
OnConnected():
|
||||
Icon + Text + Menü-Item aktualisieren
|
||||
serial.RequestConfig() → Config-Dump vom Board
|
||||
serial.RequestMacros() → Makro-Dump vom Board
|
||||
|
||||
OnDisconnected():
|
||||
Icon + Text + Menü-Item aktualisieren
|
||||
```
|
||||
|
||||
## TODOs in dieser Klasse
|
||||
|
||||
- HOST_COMMAND-Ausführung: `EvtKeyDown` empfangen → Aktion aus Config laden → URL/Programm starten
|
||||
- Eigenes Tray-Icon statt `SystemIcons.Application`
|
||||
- Debug-Log (`versapad_rx.txt`) entfernen
|
||||
@@ -0,0 +1,75 @@
|
||||
# ConfigForm
|
||||
|
||||
**Datei:** `ConfigForm.cs`
|
||||
|
||||
## Verantwortung
|
||||
|
||||
Hauptkonfigurationsfenster: zeigt alle 20 MX-Buttons und 4 Encoder, öffnet `ActionDialog` bei Klick, speichert Config + Makros auf das Board.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
┌── Tasten (GroupBox) ──────────────────────────────────────────┐
|
||||
│ TableLayoutPanel 4 Spalten × 5 Zeilen │
|
||||
│ Jede Zelle = Button mit LED-Hintergrundfarbe + Aktionstext │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
┌── Encoder (GroupBox) ─────────────────────────────────────────┐
|
||||
│ Header: SW / CW / CCW │
|
||||
│ 4 Zeilen × 3 Buttons (ENC 0–3) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
[Auf Board speichern] [Ping] [Exportieren] [Importieren] [Schließen]
|
||||
```
|
||||
|
||||
`FixedSingle`-Border, `StartPosition = CenterScreen`, kein Maximieren.
|
||||
|
||||
## MX-Button-Darstellung (RefreshMxButton)
|
||||
|
||||
| Animation | Hintergrund | Textfarbe |
|
||||
|---|---|---|
|
||||
| ColorCycle | `(40, 40, 40)` dunkelgrau | Weiß |
|
||||
| Andere | `LedBase[mxIdx]` | Schwarz/Weiß nach Luminanz |
|
||||
|
||||
Text = `action.Display + animName` (z.B. `"Strg+C [Blinken]"`).
|
||||
|
||||
Luminanz-Formel für Textfarben-Kontrast: `(R*299 + G*587 + B*114) / 1000`
|
||||
|
||||
## mx_idx ↔ Physisches Layout
|
||||
|
||||
```
|
||||
mx_idx = col * 5 + row (col=0..3, row=0..4)
|
||||
```
|
||||
|
||||
Entspricht `key_id - 5` in der Firmware. Im TableLayoutPanel: Spalte=col, Zeile=row.
|
||||
|
||||
## Speichern (OnSave)
|
||||
|
||||
```csharp
|
||||
Task.Run(() => {
|
||||
_serial.SendConfig(_config); // ~300 ms
|
||||
Thread.Sleep(50);
|
||||
_serial.SendMacros(_macros); // ~250 ms
|
||||
InvokeOnUi(() => { /* Button-Text + Enabled zurücksetzen */ });
|
||||
});
|
||||
```
|
||||
|
||||
Save-Button wird während der Übertragung deaktiviert, Text wechselt zu "Wird gesendet...".
|
||||
Save-Button ist nur aktiviert wenn Board verbunden (`_serial.IsConnected`).
|
||||
|
||||
## Import / Export
|
||||
|
||||
- **Export**: `ConfigJson.Serialize(_config)` → `SaveFileDialog` → `.json`-Datei
|
||||
- **Import**: `OpenFileDialog` → Datei lesen → `ConfigJson.Deserialize()` → `RefreshAll()`
|
||||
|
||||
Fehler (IO, JSON-Parse, falsche Version) werden per `MessageBox` angezeigt.
|
||||
|
||||
## RefreshAll
|
||||
|
||||
Wird von `TrayApp` nach erfolgreicher Config vom Board aufgerufen (über `_configForm?.RefreshAll()`). Aktualisiert alle 20 MX-Buttons und 12 Encoder-Buttons ohne Dialog.
|
||||
|
||||
## Extensions (in derselben Datei)
|
||||
|
||||
```csharp
|
||||
// Statischer ToolTip für alle Controls
|
||||
public static void ToolTipText(this Control ctrl, string text)
|
||||
public static string TypeDescription(this DeviceAction a)
|
||||
```
|
||||
@@ -0,0 +1,112 @@
|
||||
# ActionDialog
|
||||
|
||||
**Datei:** `ActionDialog.cs`
|
||||
|
||||
## Verantwortung
|
||||
|
||||
Modaler Dialog zum Bearbeiten einer einzelnen Aktion (MX-Button oder Encoder-Slot) inkl. optionaler LED-Einstellungen.
|
||||
|
||||
## Ergebnis-Properties
|
||||
|
||||
```csharp
|
||||
public DeviceAction ResultAction { get; } // Typ + Keycode/Usage/etc.
|
||||
public Color ResultColor { get; } // LED-Basisfarbe
|
||||
public LedAnimType ResultAnim { get; } // Animation
|
||||
public ushort ResultPeriod { get; } // Periode in ms
|
||||
```
|
||||
|
||||
## Panels (je nach Typ ein Panel sichtbar)
|
||||
|
||||
| Typ | Panel | Inhalt |
|
||||
|---|---|---|
|
||||
| HID Tastatur | `_hidKeyPanel` | Capture-Button + Strg/Shift/Alt/Win-Checkboxen |
|
||||
| HID Consumer | `_consumerPanel` | Dropdown mit 12 Medien-Aktionen |
|
||||
| Host Command | `_cmdPanel` | TextBox für numerische Command-ID |
|
||||
| Makro | `_macroPanel` | 8 Step-Buttons + je Strg/Shift/Alt-Checkboxen |
|
||||
| Profil wechseln | `_profilePanel` | Dropdown: "Nächstes Profil (Zyklus)" / "Profil 1" / "Profil 2" / "Profil 3" |
|
||||
| Keine | — | Alle Panels ausgeblendet |
|
||||
|
||||
LED-Panels (`_colorPanel`, `_animPanel`) erscheinen zusätzlich wenn `showColor=true` (nur MX-Buttons, nicht Encoder).
|
||||
|
||||
`UpdateLayout()` repositioniert LED-Panels und passt `ClientSize` dynamisch an wenn der Typ gewechselt wird.
|
||||
|
||||
## Profil-Panel
|
||||
|
||||
```csharp
|
||||
// Items in _profileCombo:
|
||||
// Index 0: "Nächstes Profil (Zyklus)" → Data = 0xFFFF
|
||||
// Index 1: "Profil 1" → Data = 0
|
||||
// Index 2: "Profil 2" → Data = 1
|
||||
// Index 3: "Profil 3" → Data = 2
|
||||
|
||||
// Initialbelegung:
|
||||
_profileCombo.SelectedIndex = action.Data == 0xFFFF ? 0 : action.Data + 1;
|
||||
|
||||
// In OnOk():
|
||||
data = _profileCombo.SelectedIndex == 0
|
||||
? (ushort)0xFFFF
|
||||
: (ushort)(_profileCombo.SelectedIndex - 1);
|
||||
```
|
||||
|
||||
Im Board wird `0xFFFF` als `(uint8_t)0xFF` gespeichert und in der Firmware als "nächstes Profil" interpretiert.
|
||||
|
||||
## Tasten-Capture (HID-Modus)
|
||||
|
||||
1. Benutzer klickt "Taste drücken..."
|
||||
2. `_capturing = true`
|
||||
3. Nächste Taste in `ProcessCmdKey()` → `VkToHid()` → `_hidKeycode` setzen
|
||||
4. Capture-Button zeigt Tastenname (`HidKeyName()`)
|
||||
|
||||
**Warum `ProcessCmdKey` statt `OnKeyDown`?**
|
||||
WinForms behandelt Pfeil- und Enter-Tasten als "Dialog Keys" in `ProcessDialogKey()` — sie erreichen `OnKeyDown` nie. `ProcessCmdKey` wird vor `ProcessDialogKey` aufgerufen und kann diese Tasten abfangen.
|
||||
|
||||
## Makro-Capture
|
||||
|
||||
Jeder der 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]`.
|
||||
|
||||
## Schlüssellookup (layout-unabhängig)
|
||||
|
||||
**Problem**: Windows VK-Codes sind tastaturlayout-abhängig. `Keys.OemQuotes` auf QWERTZ → Ö, auf US → `'`. Würde falsche HID-Codes liefern.
|
||||
|
||||
**Lösung**: PS/2 Scan-Codes sind layout-unabhängig (physische Tastenposition).
|
||||
|
||||
```
|
||||
VK → Scan-Code via MapVirtualKey(vk, MAPVK_VK_TO_VSC)
|
||||
Scan-Code → HID Usage via s_scanToHid[sc]
|
||||
```
|
||||
|
||||
`s_scanToHid`: vollständige PS/2-Set-1 → HID-Tabelle (~60 Einträge).
|
||||
|
||||
**Ausnahme**: Erweiterte Navigationstasten (Pfeiltasten, Pos1, Ende, …) geben via `MapVirtualKey` den Numpad-Scan-Code zurück. Für diese gibt es eine direkte `Keys → HID`-Tabelle (`s_extVkToHid`).
|
||||
|
||||
**Anzeige**: `HidKeyName(hid)` gibt den lokalisierten Tastennamen zurück:
|
||||
- Sondertasten: fest eingetragene deutsche Namen ("Esc", "→", "F5", …)
|
||||
- Zeichentasten: `GetKeyNameText(scanCode << 16, ...)` → Windows gibt den Namen laut aktivem Layout zurück ("Ö" auf QWERTZ, ";" auf US)
|
||||
|
||||
## Consumer-Aktionen
|
||||
|
||||
12 HID Consumer Usage IDs als feste Liste:
|
||||
|
||||
| Usage | Name |
|
||||
|---|---|
|
||||
| 0x00CD | Play / Pause |
|
||||
| 0x00B5 | Nächster Titel |
|
||||
| 0x00B6 | Vorheriger Titel |
|
||||
| 0x00B7 | Stop |
|
||||
| 0x00E9 | Lauter |
|
||||
| 0x00EA | Leiser |
|
||||
| 0x00E2 | Stummschalten |
|
||||
| 0x0192 | Taschenrechner |
|
||||
| 0x0223 | Browser – Startseite |
|
||||
| 0x0224 | Browser – Zurück |
|
||||
| 0x0225 | Browser – Vor |
|
||||
| 0x00B0 | Aufnahme |
|
||||
|
||||
## LED-Einstellungen
|
||||
|
||||
Nur wenn `showColor=true` (MX-Buttons). Enthält:
|
||||
- **Farbpicker**: `ColorDialog` → `_colorBtn.BackColor`
|
||||
- **Animations-Dropdown**: Statisch / Blinken / Pulsieren / Regenbogen
|
||||
- **Periode-Dropdown**: Presets von "Sehr langsam (8 s)" bis "Sehr schnell (250 ms)"
|
||||
|
||||
Bei "Regenbogen" wird der Farbpicker ausgeblendet (Farbe irrelevant).
|
||||
@@ -0,0 +1,48 @@
|
||||
# ConfigJson (Import / Export)
|
||||
|
||||
**Datei:** `ConfigJson.cs`
|
||||
|
||||
## Format
|
||||
|
||||
Menschenlesbares JSON mit `System.Text.Json` (`WriteIndented=true`, Enums als Strings via `JsonStringEnumConverter`).
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"buttons": [
|
||||
{
|
||||
"index": 0,
|
||||
"action": { "type": "HidKey", "data": 260 },
|
||||
"led": { "r": 80, "g": 40, "b": 0, "anim": "ColorCycle", "period_ms": 4000 }
|
||||
},
|
||||
...
|
||||
],
|
||||
"encoders": [
|
||||
{
|
||||
"index": 0,
|
||||
"sw": { "type": "None", "data": 0 },
|
||||
"cw": { "type": "None", "data": 0 },
|
||||
"ccw": { "type": "None", "data": 0 }
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Serialisierung
|
||||
|
||||
`ConfigJson.Serialize(cfg)` → JSON-String. Exportiert alle 20 Buttons + 4 Encoder vollständig.
|
||||
|
||||
## Deserialisierung
|
||||
|
||||
`ConfigJson.Deserialize(json, cfg)`:
|
||||
- Prüft `version` – wirft `InvalidDataException` bei Mismatch
|
||||
- Schreibt in bestehendes `DeviceConfig`-Objekt (kein `new`)
|
||||
- Fehlende `buttons`/`encoders`-Arrays werden ignoriert (partial import möglich)
|
||||
- Ungültige `index`-Werte werden übersprungen
|
||||
|
||||
## Anmerkungen
|
||||
|
||||
- `MacroTable` wird **nicht** exportiert (kein JSON-Format für Makros definiert)
|
||||
- `data` enthält den `ushort`-Wert direkt (für HidKey z.B. `Keycode | (Modifier << 8)`)
|
||||
- Die Datei ist kein Binärformat und kann manuell bearbeitet werden
|
||||
@@ -0,0 +1,25 @@
|
||||
# VersaGUI – Dokumentations-Index
|
||||
|
||||
Jede Datei deckt eine GUI-Komponente ab. Für Claude: die relevante(n) Dateien zu Beginn einer Aufgabe lesen statt die gesamten Quelldateien zu scannen.
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| [00_architecture.md](00_architecture.md) | Threading-Modell, Datenfluss, Verbindungslebenszyklus, globale Constraints (DTR, IOException, Packed-Layout) |
|
||||
| [01_serial_manager.md](01_serial_manager.md) | WMI-Erkennung, TryConnect, ReadLoop, Sende-Methoden, Reconnect-Backoff |
|
||||
| [02_device_config.md](02_device_config.md) | DeviceConfig + MacroTable: Felder, Byte-Layout (223/256 B), CRC16, LedAnimType |
|
||||
| [03_tray_app.md](03_tray_app.md) | ApplicationContext, Tray-Icon, Board-Event-Routing, Config/Makro-Dump-Empfang, TODOs |
|
||||
| [04_config_form.md](04_config_form.md) | Grid-Layout, mx_idx-Formel, RefreshMxButton, OnSave (Task.Run), Import/Export |
|
||||
| [05_action_dialog.md](05_action_dialog.md) | Panels je Typ, ProcessCmdKey-Capture, layout-unabhängiger Scan-Code-Lookup, Consumer-Liste |
|
||||
| [06_config_json.md](06_config_json.md) | JSON-Format, Serialize/Deserialize, Einschränkungen |
|
||||
|
||||
## Schnell-Referenz: Was steht wo?
|
||||
|
||||
- **DtrEnable-Problem** → [00_architecture.md](00_architecture.md), [01_serial_manager.md](01_serial_manager.md)
|
||||
- **IOException ≠ Disconnect (.NET 7)** → [00_architecture.md](00_architecture.md), [01_serial_manager.md](01_serial_manager.md)
|
||||
- **Byte-Layout der 223-Byte-Config** → [02_device_config.md](02_device_config.md)
|
||||
- **Warum ProcessCmdKey statt OnKeyDown?** → [05_action_dialog.md](05_action_dialog.md)
|
||||
- **Warum Scan-Codes statt VK-Codes (Umlaut-Problem)?** → [05_action_dialog.md](05_action_dialog.md)
|
||||
- **mx_idx ↔ key_id-Umrechnung** → [04_config_form.md](04_config_form.md)
|
||||
- **Makro-Slot-Konvention** → [02_device_config.md](02_device_config.md)
|
||||
- **HOST_COMMAND noch nicht implementiert** → [03_tray_app.md](03_tray_app.md)
|
||||
- **Task.Run beim Speichern** → [04_config_form.md](04_config_form.md)
|
||||
+41
-13
@@ -35,6 +35,10 @@ public class ActionDialog : Form
|
||||
private readonly Panel _cmdPanel;
|
||||
private readonly TextBox _cmdBox;
|
||||
|
||||
// Profil wechseln
|
||||
private readonly Panel _profilePanel;
|
||||
private readonly ComboBox _profileCombo;
|
||||
|
||||
// LED-Farbe
|
||||
private readonly Panel _colorPanel;
|
||||
private readonly Button _colorBtn;
|
||||
@@ -50,11 +54,11 @@ public class ActionDialog : Form
|
||||
private readonly MacroTable _macros;
|
||||
private readonly int _macroSlot;
|
||||
// Je Step: [CaptureBtn, CheckCtrl, CheckShift, CheckAlt]
|
||||
private readonly Button[] _stepBtns = new Button[4];
|
||||
private readonly CheckBox[] _stepCtrl = new CheckBox[4];
|
||||
private readonly CheckBox[] _stepShift = new CheckBox[4];
|
||||
private readonly CheckBox[] _stepAlt = new CheckBox[4];
|
||||
private readonly byte[] _stepKeycodes = new byte[4];
|
||||
private readonly Button[] _stepBtns = new Button[8];
|
||||
private readonly CheckBox[] _stepCtrl = new CheckBox[8];
|
||||
private readonly CheckBox[] _stepShift = new CheckBox[8];
|
||||
private readonly CheckBox[] _stepAlt = new CheckBox[8];
|
||||
private readonly byte[] _stepKeycodes = new byte[8];
|
||||
private int _captureStep = -1; // -1 = nicht im Capture-Modus
|
||||
|
||||
// Capture-Zustand
|
||||
@@ -220,7 +224,7 @@ public class ActionDialog : Form
|
||||
_showColor = showColor;
|
||||
|
||||
// Bestehende Makro-Steps aus der Tabelle laden
|
||||
for (int i = 0; i < 4; i++)
|
||||
for (int i = 0; i < 8; i++)
|
||||
_stepKeycodes[i] = _macros.Steps[_macroSlot, i].Keycode;
|
||||
|
||||
// Formular-Grundeinstellungen
|
||||
@@ -240,7 +244,7 @@ public class ActionDialog : Form
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
};
|
||||
_typeCombo.Items.AddRange(new object[]
|
||||
{ "Keine Aktion", "HID Tastatur", "HID Consumer", "Host Command", "Makro" });
|
||||
{ "Keine Aktion", "HID Tastatur", "HID Consumer", "Host Command", "Makro", "Profil wechseln" });
|
||||
_typeCombo.SelectedIndex = (int)action.Type;
|
||||
_typeCombo.SelectedIndexChanged += OnTypeChanged;
|
||||
|
||||
@@ -320,6 +324,22 @@ public class ActionDialog : Form
|
||||
};
|
||||
_cmdPanel.Controls.AddRange(new Control[] { cmdLabel, _cmdBox });
|
||||
|
||||
// ── Profil-Panel ──────────────────────────────────────────────────────
|
||||
_profilePanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 30) };
|
||||
var profileLabel = new Label { Text = "Ziel-Profil:", Location = new Point(0, 8), AutoSize = true };
|
||||
_profileCombo = new ComboBox
|
||||
{
|
||||
Location = new Point(90, 4),
|
||||
Width = 200,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
};
|
||||
_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))
|
||||
: 0;
|
||||
_profilePanel.Controls.AddRange(new Control[] { profileLabel, _profileCombo });
|
||||
|
||||
// ── Farb-Panel ────────────────────────────────────────────────────────
|
||||
_colorPanel = new Panel
|
||||
{
|
||||
@@ -378,8 +398,8 @@ public class ActionDialog : Form
|
||||
_animPanel.Controls.AddRange(new Control[]
|
||||
{ animLabel, _animCombo, periodLabel, _periodCombo });
|
||||
|
||||
// ── Makro-Panel (4 Steps) ─────────────────────────────────────────────
|
||||
_macroPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 4 * 30 + 22) };
|
||||
// ── Makro-Panel (8 Steps) ─────────────────────────────────────────────
|
||||
_macroPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 8 * 30 + 22) };
|
||||
|
||||
var macroHint = new Label
|
||||
{
|
||||
@@ -391,7 +411,7 @@ public class ActionDialog : Form
|
||||
};
|
||||
_macroPanel.Controls.Add(macroHint);
|
||||
|
||||
for (int step = 0; step < 4; step++)
|
||||
for (int step = 0; step < 8; step++)
|
||||
{
|
||||
int s = step;
|
||||
int y2 = 30 + step * 30;
|
||||
@@ -454,7 +474,7 @@ public class ActionDialog : Form
|
||||
Controls.AddRange(new Control[]
|
||||
{
|
||||
typeLabel, _typeCombo,
|
||||
_hidKeyPanel, _consumerPanel, _cmdPanel, _macroPanel,
|
||||
_hidKeyPanel, _consumerPanel, _cmdPanel, _profilePanel, _macroPanel,
|
||||
_colorPanel, _animPanel,
|
||||
_okBtn, _cancelBtn,
|
||||
});
|
||||
@@ -481,6 +501,7 @@ public class ActionDialog : Form
|
||||
_hidKeyPanel.Visible = t == ActionType.HidKey;
|
||||
_consumerPanel.Visible = t == ActionType.HidConsumer;
|
||||
_cmdPanel.Visible = t == ActionType.HostCommand;
|
||||
_profilePanel.Visible = t == ActionType.ProfileSwitch;
|
||||
_macroPanel.Visible = t == ActionType.Macro;
|
||||
_colorPanel.Visible = _showColor;
|
||||
_animPanel.Visible = _showColor;
|
||||
@@ -499,7 +520,8 @@ public class ActionDialog : Form
|
||||
ActionType.HidKey => 110,
|
||||
ActionType.HidConsumer => 30,
|
||||
ActionType.HostCommand => 30,
|
||||
ActionType.Macro => 4 * 30 + 22,
|
||||
ActionType.ProfileSwitch => 30,
|
||||
ActionType.Macro => 8 * 30 + 22,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
@@ -710,12 +732,18 @@ public class ActionDialog : Form
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case ActionType.ProfileSwitch:
|
||||
data = _profileCombo.SelectedIndex == 0
|
||||
? (ushort)0xFFFF // Zyklus: Firmware rechnet (current+1)%3
|
||||
: (ushort)(_profileCombo.SelectedIndex - 1); // Profil 0/1/2
|
||||
break;
|
||||
}
|
||||
|
||||
if (type == ActionType.Macro)
|
||||
{
|
||||
// Steps in die Makro-Tabelle schreiben
|
||||
for (int i = 0; i < 4; i++)
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
byte mod = 0;
|
||||
if (_stepCtrl[i].Checked) mod |= 0x01;
|
||||
|
||||
+48
-3
@@ -26,6 +26,7 @@ public class ConfigForm : Form
|
||||
private readonly Button[] _btnGrid = new Button[20];
|
||||
private readonly Button[,] _encBtns = new Button[4, 3];
|
||||
private Button _saveBtn = null!;
|
||||
private ComboBox _profileCombo = null!;
|
||||
|
||||
public ConfigForm(DeviceConfig config, MacroTable macros, SerialManager serial)
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public class ConfigForm : Form
|
||||
ClientSize = new Size(520, 480);
|
||||
|
||||
int y = 12;
|
||||
y = BuildProfileBar(y);
|
||||
y = BuildButtonGrid(y);
|
||||
y = BuildEncoderPanel(y);
|
||||
BuildFooter(y);
|
||||
@@ -50,6 +52,35 @@ public class ConfigForm : Form
|
||||
_saveBtn.Enabled = _serial.IsConnected;
|
||||
}
|
||||
|
||||
// ── Profil-Leiste ─────────────────────────────────────────────────────────
|
||||
|
||||
private int BuildProfileBar(int startY)
|
||||
{
|
||||
var label = new Label
|
||||
{
|
||||
Text = "Profil:",
|
||||
Location = new Point(12, startY + 4),
|
||||
AutoSize = true,
|
||||
};
|
||||
|
||||
_profileCombo = new ComboBox
|
||||
{
|
||||
Location = new Point(60, startY),
|
||||
Width = 160,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
};
|
||||
_profileCombo.Items.AddRange(new object[] { "Profil 1", "Profil 2", "Profil 3" });
|
||||
_profileCombo.SelectedIndex = _config.ActiveProfileIndex;
|
||||
_profileCombo.SelectedIndexChanged += (_, _) =>
|
||||
{
|
||||
_config.ActiveProfileIndex = (byte)_profileCombo.SelectedIndex;
|
||||
RefreshAll();
|
||||
};
|
||||
|
||||
Controls.AddRange(new Control[] { label, _profileCombo });
|
||||
return startY + 34;
|
||||
}
|
||||
|
||||
// ── Tasten-Grid (4 Spalten × 5 Zeilen) ───────────────────────────────────
|
||||
|
||||
private int BuildButtonGrid(int startY)
|
||||
@@ -260,13 +291,25 @@ public class ConfigForm : Form
|
||||
// SendConfig + SendMacros blockieren ~400ms → Background-Thread
|
||||
Task.Run(() =>
|
||||
{
|
||||
_serial.SendConfig(_config);
|
||||
Thread.Sleep(50); // kurze Pause zwischen Config- und Makro-Transfer
|
||||
_serial.SendMacros(_macros);
|
||||
bool cfgOk = _serial.SendConfig(_config); // wartet intern auf CONFIG_ACK/NACK
|
||||
bool macroOk = _serial.SendMacros(_macros); // wartet intern auf MACRO_ACK/NACK
|
||||
InvokeOnUi(() =>
|
||||
{
|
||||
_saveBtn.Text = "Auf Board speichern";
|
||||
_saveBtn.Enabled = _serial.IsConnected;
|
||||
if (cfgOk && macroOk)
|
||||
{
|
||||
MessageBox.Show("Konfiguration erfolgreich gespeichert.",
|
||||
"VersaPad", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
else
|
||||
{
|
||||
string detail = (!cfgOk && !macroOk) ? "Config und Makros"
|
||||
: !cfgOk ? "Config"
|
||||
: "Makros";
|
||||
MessageBox.Show($"Speichern fehlgeschlagen ({detail}).\nBoard hat NACK gesendet oder nicht geantwortet.",
|
||||
"VersaPad – Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -393,6 +436,8 @@ 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.Macro => "Makro-Sequenz",
|
||||
ActionType.ProfileSwitch => "Profil wechseln",
|
||||
_ => "Keine Aktion",
|
||||
};
|
||||
}
|
||||
|
||||
+12
-3
@@ -1,12 +1,14 @@
|
||||
// ConfigJson.cs
|
||||
// JSON-Import/Export für DeviceConfig.
|
||||
//
|
||||
// Format (menschenlesbar, kommentiert mit Feldnamen):
|
||||
// Format (menschenlesbar):
|
||||
// {
|
||||
// "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 } },
|
||||
// ...
|
||||
// ],
|
||||
// "encoders": [
|
||||
@@ -17,6 +19,8 @@
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// Import/Export bezieht sich immer auf das aktive Profil.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -37,6 +41,7 @@ internal static class ConfigJson
|
||||
var doc = new ConfigJsonDoc
|
||||
{
|
||||
Version = DeviceConfig.Version,
|
||||
Profile = cfg.ActiveProfileIndex,
|
||||
Buttons = new ButtonEntry[20],
|
||||
Encoders = new EncoderEntry[4],
|
||||
};
|
||||
@@ -52,6 +57,7 @@ internal static class ConfigJson
|
||||
R = cfg.LedBase[i].R,
|
||||
G = cfg.LedBase[i].G,
|
||||
B = cfg.LedBase[i].B,
|
||||
Brightness = cfg.LedBrightness[i],
|
||||
Anim = cfg.LedAnim[i],
|
||||
PeriodMs = cfg.LedPeriod[i],
|
||||
},
|
||||
@@ -90,6 +96,7 @@ internal static class ConfigJson
|
||||
if (b.Led != null)
|
||||
{
|
||||
cfg.LedBase[b.Index] = Color.FromArgb(b.Led.R, b.Led.G, b.Led.B);
|
||||
cfg.LedBrightness[b.Index] = b.Led.Brightness;
|
||||
cfg.LedAnim[b.Index] = b.Led.Anim;
|
||||
cfg.LedPeriod[b.Index] = b.Led.PeriodMs;
|
||||
}
|
||||
@@ -125,6 +132,7 @@ internal static class ConfigJson
|
||||
private class ConfigJsonDoc
|
||||
{
|
||||
[JsonPropertyName("version")] public byte Version { get; set; }
|
||||
[JsonPropertyName("profile")] public byte Profile { get; set; }
|
||||
[JsonPropertyName("buttons")] public ButtonEntry[]? Buttons { get; set; }
|
||||
[JsonPropertyName("encoders")] public EncoderEntry[]? Encoders { get; set; }
|
||||
}
|
||||
@@ -155,6 +163,7 @@ internal static class ConfigJson
|
||||
[JsonPropertyName("r")] public byte R { get; set; }
|
||||
[JsonPropertyName("g")] public byte G { get; set; }
|
||||
[JsonPropertyName("b")] public byte B { get; set; }
|
||||
[JsonPropertyName("brightness")] public byte Brightness { get; set; } = 255;
|
||||
[JsonPropertyName("anim")] public LedAnimType Anim { get; set; }
|
||||
[JsonPropertyName("period_ms")] public ushort PeriodMs { get; set; }
|
||||
}
|
||||
|
||||
+143
-87
@@ -1,17 +1,32 @@
|
||||
// DeviceConfig.cs
|
||||
// C#-Spiegel der SDeviceConfig-Struktur aus der Firmware (nvm_config.h).
|
||||
// C#-Spiegel der Firmware-Structs. Muss byte-kompatibel mit SDeviceConfig (nvm_config.h)
|
||||
// und SMacroTable (macro_config.h) sein.
|
||||
//
|
||||
// Serialisiertes Layout (packed, 223 Bytes):
|
||||
// Offset 0 4B Magic 0x56503202
|
||||
// Offset 4 1B Version 2
|
||||
// Offset 5 2B CRC16 (CCITT über Bytes 7–222)
|
||||
// Offset 7 60B mx_actions[20] je 3B: type(1) + data_lo(1) + data_hi(1)
|
||||
// Offset 67 36B enc_actions[4][3] je 3B
|
||||
// Offset103 20B led_r[20]
|
||||
// Offset123 20B led_g[20]
|
||||
// Offset143 20B led_b[20]
|
||||
// Offset163 20B led_anim[20] je 1B (LedAnimType)
|
||||
// Offset183 40B led_period_ms[20] je 2B (uint16, little-endian)
|
||||
// Serialisiertes Config-Layout (740 B, packed):
|
||||
// ── Globaler Header (32B) ──────────────────────────────────────────────────
|
||||
// Offset 0 4B Magic 0x56503203
|
||||
// Offset 4 1B Version = 3
|
||||
// Offset 5 2B CRC16-CCITT über Bytes 7–739
|
||||
// Offset 7 1B ActiveProfile (0–2)
|
||||
// Offset 8 1B GlobalBrightness (0–255)
|
||||
// Offset 9 4B EncSensitivity[4]
|
||||
// Offset 13 19B Reserve
|
||||
// ── Profil 0 (236B, ab Offset 32) ─────────────────────────────────────────
|
||||
// ── Profil 1 (236B, ab Offset 268) ────────────────────────────────────────
|
||||
// ── Profil 2 (236B, ab Offset 504) ────────────────────────────────────────
|
||||
//
|
||||
// Pro Profil (236B):
|
||||
// Offset 0 60B MxActions[20] je 3B: type(1) + data_lo(1) + data_hi(1)
|
||||
// Offset 60 36B EncActions[4][3] je 3B
|
||||
// Offset 96 20B LedBase[i].R
|
||||
// Offset116 20B LedBase[i].G
|
||||
// Offset136 20B LedBase[i].B
|
||||
// Offset156 20B LedBrightness[i] per-LED 0–255
|
||||
// Offset176 20B LedAnim[i]
|
||||
// Offset196 40B LedPeriod[i] uint16 little-endian
|
||||
//
|
||||
// MacroTable-Layout (512 B):
|
||||
// 32 Slots × 8 Steps × 2B = 512B
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
@@ -24,6 +39,7 @@ public enum ActionType : byte
|
||||
HidConsumer = 2, // data = HID Consumer Usage ID
|
||||
HostCommand = 3, // data = Command-ID → App führt aus
|
||||
Macro = 4, // data = Makro-Slot-Index (0–31)
|
||||
ProfileSwitch = 5, // data = Profil-Index (0–2)
|
||||
}
|
||||
|
||||
// Ein Step in einem Makro (keycode=0 → leerer/letzter Step)
|
||||
@@ -36,11 +52,11 @@ public class MacroStep
|
||||
public MacroStep Clone() => new() { Keycode = Keycode, Modifier = Modifier };
|
||||
}
|
||||
|
||||
// Makro-Tabelle: 32 Slots × 4 Steps (spiegelt SMacroTable aus macro_config.h)
|
||||
// Makro-Tabelle: 32 Slots × 8 Steps (spiegelt SMacroTable aus macro_config.h)
|
||||
public class MacroTable
|
||||
{
|
||||
public const int Slots = 32;
|
||||
public const int MaxSteps = 4;
|
||||
public const int MaxSteps = 8;
|
||||
|
||||
// [slot][step]
|
||||
public MacroStep[,] Steps { get; } = new MacroStep[Slots, MaxSteps];
|
||||
@@ -53,13 +69,13 @@ public class MacroTable
|
||||
}
|
||||
|
||||
// Slot-Index für MX-Buttons und Encoder-Aktionen
|
||||
public static int SlotForMx(int mxIdx) => mxIdx; // 0–19
|
||||
public static int SlotForEncoder(int enc, int actIdx) => 20 + enc * 3 + actIdx; // 20–31
|
||||
public static int SlotForMx(int mxIdx) => mxIdx;
|
||||
public static int SlotForEncoder(int enc, int actIdx) => 20 + enc * 3 + actIdx;
|
||||
|
||||
// Serialisierung: 32 × 4 × 2 = 256 Bytes
|
||||
// Serialisierung: 32 × 8 × 2 = 512 Bytes
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var buf = new byte[256];
|
||||
var buf = new byte[512];
|
||||
int pos = 0;
|
||||
for (int s = 0; s < Slots; s++)
|
||||
for (int i = 0; i < MaxSteps; i++)
|
||||
@@ -72,7 +88,7 @@ public class MacroTable
|
||||
|
||||
public void FromBytes(byte[] buf)
|
||||
{
|
||||
if (buf.Length < 256) return;
|
||||
if (buf.Length < 512) return;
|
||||
int pos = 0;
|
||||
for (int s = 0; s < Slots; s++)
|
||||
for (int i = 0; i < MaxSteps; i++)
|
||||
@@ -83,13 +99,13 @@ public class MacroTable
|
||||
}
|
||||
}
|
||||
|
||||
// Spiegelt LEDAnim aus CButton.h – nur die in der GUI konfigurierbaren Werte
|
||||
// Spiegelt LEDAnim aus CButton.h
|
||||
public enum LedAnimType : byte
|
||||
{
|
||||
Static = 0,
|
||||
Blink = 1,
|
||||
Pulse = 2,
|
||||
ColorCycle = 5, // Regenbogen (entspricht LEDAnim::COLOR_CYCLE in Firmware)
|
||||
ColorCycle = 5,
|
||||
}
|
||||
|
||||
public class DeviceAction
|
||||
@@ -130,6 +146,8 @@ public class DeviceAction
|
||||
}
|
||||
if (Type == ActionType.Macro)
|
||||
return $"Makro {Data}";
|
||||
if (Type == ActionType.ProfileSwitch)
|
||||
return Data == 0xFFFF ? "→ Nächstes Profil" : $"→ Profil {Data + 1}";
|
||||
return $"CMD {Data}";
|
||||
}
|
||||
}
|
||||
@@ -152,31 +170,19 @@ public class DeviceAction
|
||||
public DeviceAction Clone() => new() { Type = Type, Data = Data };
|
||||
}
|
||||
|
||||
public class DeviceConfig
|
||||
// ── Pro-Profil-Daten (spiegelt SDeviceProfile) ────────────────────────────────
|
||||
|
||||
public class DeviceProfile
|
||||
{
|
||||
public const uint Magic = 0x56503202;
|
||||
public const byte Version = 2;
|
||||
|
||||
public const int EncSw = 0;
|
||||
public const int EncCw = 1;
|
||||
public const int EncCcw = 2;
|
||||
|
||||
public DeviceAction[] MxActions { get; } =
|
||||
Enumerable.Range(0, 20).Select(_ => new DeviceAction()).ToArray();
|
||||
|
||||
public DeviceAction[,] EncActions { get; } = InitEncActions();
|
||||
|
||||
// Base-LED-Farben (Default: warm-weiß wie Firmware)
|
||||
public Color[] LedBase { get; } =
|
||||
Enumerable.Repeat(Color.FromArgb(80, 40, 0), 20).ToArray();
|
||||
|
||||
// LED-Animationen (Default: Regenbogen)
|
||||
public LedAnimType[] LedAnim { get; } =
|
||||
Enumerable.Repeat(LedAnimType.ColorCycle, 20).ToArray();
|
||||
|
||||
// Animationsperiode in ms (Default: 4000ms)
|
||||
public ushort[] LedPeriod { get; } =
|
||||
Enumerable.Repeat((ushort)4000, 20).ToArray();
|
||||
public Color[] LedBase { get; } = Enumerable.Repeat(Color.FromArgb(80, 40, 0), 20).ToArray();
|
||||
public byte[] LedBrightness { get; } = Enumerable.Repeat((byte)255, 20).ToArray();
|
||||
public LedAnimType[] LedAnim { get; } = Enumerable.Repeat(LedAnimType.ColorCycle, 20).ToArray();
|
||||
public ushort[] LedPeriod { get; } = Enumerable.Repeat((ushort)4000, 20).ToArray();
|
||||
|
||||
private static DeviceAction[,] InitEncActions()
|
||||
{
|
||||
@@ -187,79 +193,43 @@ public class DeviceConfig
|
||||
return a;
|
||||
}
|
||||
|
||||
// ── Serialisierung ────────────────────────────────────────────────────────
|
||||
|
||||
public byte[] ToBytes()
|
||||
// Serialisiert ein Profil in 236 Bytes
|
||||
public void ToBytes(byte[] buf, int offset)
|
||||
{
|
||||
const int size = 223;
|
||||
var buf = new byte[size];
|
||||
int pos = 0;
|
||||
|
||||
WriteU32(buf, ref pos, Magic);
|
||||
buf[pos++] = Version;
|
||||
int crcOffset = pos;
|
||||
pos += 2;
|
||||
|
||||
foreach (var a in MxActions)
|
||||
WriteAction(buf, ref pos, a);
|
||||
|
||||
int pos = offset;
|
||||
foreach (var a in MxActions) WriteAction(buf, ref pos, a);
|
||||
for (int e = 0; e < 4; e++)
|
||||
for (int i = 0; i < 3; i++)
|
||||
WriteAction(buf, ref pos, EncActions[e, i]);
|
||||
|
||||
for (int i = 0; i < 3; i++) WriteAction(buf, ref pos, EncActions[e, i]);
|
||||
for (int i = 0; i < 20; i++) buf[pos++] = LedBase[i].R;
|
||||
for (int i = 0; i < 20; i++) buf[pos++] = LedBase[i].G;
|
||||
for (int i = 0; i < 20; i++) buf[pos++] = LedBase[i].B;
|
||||
|
||||
for (int i = 0; i < 20; i++) buf[pos++] = LedBrightness[i];
|
||||
for (int i = 0; i < 20; i++) buf[pos++] = (byte)LedAnim[i];
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
buf[pos++] = (byte)(LedPeriod[i] & 0xFF);
|
||||
buf[pos++] = (byte)(LedPeriod[i] >> 8);
|
||||
}
|
||||
|
||||
ushort crc = Crc16(buf, 7, size - 7);
|
||||
buf[crcOffset] = (byte)(crc & 0xFF);
|
||||
buf[crcOffset + 1] = (byte)(crc >> 8);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
public bool FromBytes(byte[] buf)
|
||||
// Deserialisiert ein Profil aus 236 Bytes
|
||||
public void FromBytes(byte[] buf, int offset)
|
||||
{
|
||||
if (buf.Length < 223) 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, 223 - 7);
|
||||
if (storedCrc != computedCrc) return false;
|
||||
|
||||
int pos = 7;
|
||||
int pos = offset;
|
||||
for (int i = 0; i < 20; i++) ReadAction(buf, ref pos, MxActions[i]);
|
||||
for (int e = 0; e < 4; e++)
|
||||
for (int i = 0; i < 3; i++)
|
||||
ReadAction(buf, ref pos, EncActions[e, i]);
|
||||
|
||||
for (int i = 0; i < 3; i++) ReadAction(buf, ref pos, EncActions[e, i]);
|
||||
for (int i = 0; i < 20; i++) LedBase[i] = Color.FromArgb(buf[pos + i], buf[pos + 20 + i], buf[pos + 40 + i]);
|
||||
pos += 60;
|
||||
|
||||
for (int i = 0; i < 20; i++) LedBrightness[i] = buf[pos++];
|
||||
for (int i = 0; i < 20; i++) LedAnim[i] = (LedAnimType)buf[pos++];
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
LedPeriod[i] = (ushort)(buf[pos] | (buf[pos + 1] << 8));
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static void WriteAction(byte[] buf, ref int pos, DeviceAction a)
|
||||
{
|
||||
buf[pos++] = (byte)a.Type;
|
||||
@@ -273,6 +243,92 @@ public class DeviceConfig
|
||||
a.Data = (ushort)(buf[pos] | (buf[pos + 1] << 8));
|
||||
pos += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gesamt-Config (spiegelt SDeviceConfig) ────────────────────────────────────
|
||||
|
||||
public class DeviceConfig
|
||||
{
|
||||
public const uint Magic = 0x56503203;
|
||||
public const byte Version = 3;
|
||||
|
||||
public const int EncSw = 0;
|
||||
public const int EncCw = 1;
|
||||
public const int EncCcw = 2;
|
||||
|
||||
// Globale Felder
|
||||
public byte ActiveProfileIndex { get; set; } = 0;
|
||||
public byte GlobalBrightness { get; set; } = 255;
|
||||
public byte[] EncSensitivity { get; } = new byte[] { 1, 1, 1, 1 };
|
||||
|
||||
// 3 Profile
|
||||
public DeviceProfile[] Profiles { get; } =
|
||||
Enumerable.Range(0, 3).Select(_ => new DeviceProfile()).ToArray();
|
||||
|
||||
// ── Shortcuts auf aktives Profil (ConfigForm braucht keine Änderung) ──────
|
||||
|
||||
public DeviceAction[] MxActions => Profiles[ActiveProfileIndex].MxActions;
|
||||
public DeviceAction[,] EncActions => Profiles[ActiveProfileIndex].EncActions;
|
||||
public Color[] LedBase => Profiles[ActiveProfileIndex].LedBase;
|
||||
public byte[] LedBrightness => Profiles[ActiveProfileIndex].LedBrightness;
|
||||
public LedAnimType[] LedAnim => Profiles[ActiveProfileIndex].LedAnim;
|
||||
public ushort[] LedPeriod => Profiles[ActiveProfileIndex].LedPeriod;
|
||||
|
||||
// ── Serialisierung ────────────────────────────────────────────────────────
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
const int size = 740;
|
||||
var buf = new byte[size];
|
||||
int pos = 0;
|
||||
|
||||
WriteU32(buf, ref pos, Magic);
|
||||
buf[pos++] = Version;
|
||||
int crcOffset = pos;
|
||||
pos += 2; // CRC-Platzhalter
|
||||
|
||||
buf[pos++] = ActiveProfileIndex;
|
||||
buf[pos++] = GlobalBrightness;
|
||||
for (int i = 0; i < 4; i++) buf[pos++] = EncSensitivity[i];
|
||||
pos += 19; // Reserve (bleibt 0)
|
||||
|
||||
// 3 Profile à 236B
|
||||
for (int p = 0; p < 3; p++)
|
||||
Profiles[p].ToBytes(buf, pos + p * 236);
|
||||
pos += 3 * 236;
|
||||
|
||||
ushort crc = Crc16(buf, 7, size - 7);
|
||||
buf[crcOffset] = (byte)(crc & 0xFF);
|
||||
buf[crcOffset + 1] = (byte)(crc >> 8);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
public bool FromBytes(byte[] buf)
|
||||
{
|
||||
if (buf.Length < 740) 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);
|
||||
if (storedCrc != computedCrc) return false;
|
||||
|
||||
int pos = 7;
|
||||
ActiveProfileIndex = Math.Min(buf[pos++], (byte)2);
|
||||
GlobalBrightness = buf[pos++];
|
||||
for (int i = 0; i < 4; i++) EncSensitivity[i] = buf[pos++];
|
||||
pos += 19; // Reserve überspringen
|
||||
|
||||
for (int p = 0; p < 3; p++)
|
||||
Profiles[p].FromBytes(buf, pos + p * 236);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static void WriteU32(byte[] buf, ref int pos, uint v)
|
||||
{
|
||||
|
||||
@@ -51,6 +51,7 @@ public static class Protocol
|
||||
public const byte EvtMacroBegin = 0x96; // Beginn Makro-Dump (Data[1] = Chunks)
|
||||
public const byte EvtMacroData = 0x97; // Makro-Chunk (Data[1] = Index, Data[2..7] = 6B)
|
||||
public const byte EvtMacroEnd = 0x98; // Makro-Dump vollständig
|
||||
public const byte EvtMacroNack = 0x99; // Makro-Tabelle: NVM-Fehler
|
||||
}
|
||||
|
||||
// Ein 8-Byte-Paket mit benannten Accessoren.
|
||||
|
||||
+70
-5
@@ -39,6 +39,12 @@ public class SerialManager : IDisposable
|
||||
private bool _disposed;
|
||||
private 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;
|
||||
|
||||
public bool IsConnected => _port?.IsOpen == true;
|
||||
|
||||
public SerialManager()
|
||||
@@ -221,13 +227,43 @@ public class SerialManager : IDisposable
|
||||
public void RequestMacros()
|
||||
=> Send(new SerialPacket(Protocol.CmdMacroRead));
|
||||
|
||||
// Wird von TrayApp aufgerufen wenn das Board CONFIG_ACK/NACK oder MACRO_ACK/NACK sendet.
|
||||
// Gibt den wartenden SendConfig()/SendMacros()-Thread frei.
|
||||
public void SignalConfigAck()
|
||||
{
|
||||
_configAckOk = true;
|
||||
if (_configAckGate.CurrentCount == 0) _configAckGate.Release();
|
||||
}
|
||||
|
||||
public void SignalConfigNack()
|
||||
{
|
||||
_configAckOk = false;
|
||||
if (_configAckGate.CurrentCount == 0) _configAckGate.Release();
|
||||
}
|
||||
|
||||
public void SignalMacroAck()
|
||||
{
|
||||
_macroAckOk = true;
|
||||
if (_macroAckGate.CurrentCount == 0) _macroAckGate.Release();
|
||||
}
|
||||
|
||||
public void SignalMacroNack()
|
||||
{
|
||||
_macroAckOk = false;
|
||||
if (_macroAckGate.CurrentCount == 0) _macroAckGate.Release();
|
||||
}
|
||||
|
||||
// Makro-Tabelle (256 Bytes) in 6-Byte-Chunks senden.
|
||||
public void SendMacros(MacroTable macros)
|
||||
// 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);
|
||||
|
||||
@@ -240,24 +276,40 @@ public class SerialManager : IDisposable
|
||||
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);
|
||||
}
|
||||
|
||||
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.
|
||||
// Protokoll: BEGIN → n×DATA → COMMIT
|
||||
// Board schreibt nach COMMIT in den NVM (Firmware-Seite noch TODO).
|
||||
public void SendConfig(DeviceConfig config)
|
||||
// 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}\n");
|
||||
$"{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);
|
||||
@@ -271,14 +323,27 @@ public class SerialManager : IDisposable
|
||||
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
|
||||
}
|
||||
|
||||
File.AppendAllText(Path.Combine(Path.GetTempPath(), "versapad_rx.txt"),
|
||||
$"{DateTime.Now:HH:mm:ss.fff} TX ConfigCommit\n");
|
||||
$"{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 ───────────────────────────────────────────────────────────
|
||||
|
||||
+9
-8
@@ -128,18 +128,16 @@ public class TrayApp : ApplicationContext
|
||||
break;
|
||||
|
||||
case Protocol.EvtConfigAck:
|
||||
MessageBox.Show("Config erfolgreich gespeichert!",
|
||||
"VersaPad", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
_serial.SignalConfigAck(); // SendConfig()-Thread freigeben
|
||||
break;
|
||||
|
||||
case Protocol.EvtConfigNack:
|
||||
MessageBox.Show("Config FEHLER: CRC/Magic ungültig.\nÜbertragung fehlgeschlagen.",
|
||||
"VersaPad", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
_serial.SignalConfigNack(); // SendConfig()-Thread freigeben (Fehler)
|
||||
break;
|
||||
|
||||
// ── Config-Dump vom Board ─────────────────────────────────────────
|
||||
case Protocol.EvtConfigBegin:
|
||||
_rxConfigBuf = new byte[223]; // sizeof(SDeviceConfig) = 223 (Version 2)
|
||||
_rxConfigBuf = new byte[740]; // sizeof(SDeviceConfig) = 740 (Version 3)
|
||||
break;
|
||||
|
||||
case Protocol.EvtConfigData:
|
||||
@@ -163,12 +161,15 @@ public class TrayApp : ApplicationContext
|
||||
|
||||
// ── Makro-Dump vom Board ──────────────────────────────────────────
|
||||
case Protocol.EvtMacroAck:
|
||||
MessageBox.Show("Makros erfolgreich gespeichert!",
|
||||
"VersaPad", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
_serial.SignalMacroAck(); // SendMacros()-Thread freigeben
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroNack:
|
||||
_serial.SignalMacroNack(); // SendMacros()-Thread freigeben (Fehler)
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroBegin:
|
||||
_rxMacroBuf = new byte[256];
|
||||
_rxMacroBuf = new byte[512]; // sizeof(SMacroTable) = 512 (8 Steps)
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroData:
|
||||
|
||||
Reference in New Issue
Block a user