8 Step macro and profile switching fully working

This commit is contained in:
2026-04-13 22:34:49 +02:00
parent e01666ab59
commit 24fe162833
8 changed files with 185 additions and 58 deletions
+30 -11
View File
@@ -8,6 +8,7 @@
- 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
@@ -52,20 +53,38 @@ Bei IOException:
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 | Funktion |
|---|---|
| `Send(pkt)` | Rohe 8-Byte-Übertragung (fire-and-forget, ignoriert Sendefehler) |
| `SetLedOverride(keyId, r, g, b)` | CMD 0x01 |
| `ClearLedOverride(keyId)` | CMD 0x02 |
| `SetLedBase(keyId, r, g, b)` | CMD 0x03 |
| `RequestConfig()` | CMD 0x13 Board antwortet mit Config-Dump |
| `RequestMacros()` | CMD 0x23 Board antwortet mit Makro-Dump |
| `SendConfig(cfg)` | BEGIN(0x10) → 38×DATA(0x11) → COMMIT(0x12), 5 ms zwischen Chunks |
| `SendMacros(macros)` | BEGIN(0x20) → 43×DATA(0x21) → COMMIT(0x22), 5 ms zwischen Chunks |
| 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 ~400 ms → werden in `Task.Run()` aus `ConfigForm.OnSave()` aufgerufen.
`SendConfig` und `SendMacros` blockieren ~1,5 s (Chunks + NVM-Zeit) → werden in `Task.Run()` aus `ConfigForm.OnSave()` aufgerufen.
## Events
+44 -28
View File
@@ -14,55 +14,71 @@ C#-Spiegel der Firmware-Structs. Muss byte-kompatibel mit `SDeviceConfig` (nvm_c
| Feld | Typ | Inhalt |
|---|---|---|
| `MxActions[20]` | `DeviceAction[]` | Aktionen für MX-Buttons 019 |
| `EncActions[4,3]` | `DeviceAction[,]` | Encoder [03][SW=0/CW=1/CCW=2] |
| `LedBase[20]` | `Color[]` | RGB-Basis-LED-Farbe je Button |
| `LedAnim[20]` | `LedAnimType[]` | Animation je Button |
| `LedPeriod[20]` | `ushort[]` | Animationsperiode in ms |
| `ActiveProfileIndex` | `byte` | Aktives Profil (02) |
| `GlobalBrightness` | `byte` | Globale LED-Helligkeit (0255) |
| `MxActions[20]` | `DeviceAction[]` | Aktionen für MX-Buttons 019 (aus aktivem Profil) |
| `EncActions[4,3]` | `DeviceAction[,]` | Encoder [03][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, 223 B)
### Serialisierungs-Layout (ToBytes / FromBytes, 740 B)
```
Offset 0 4B Magic 0x56503202 (little-endian)
Offset 4 1B Version = 2
Offset 5 2B CRC16-CCITT über Bytes 7222 (little-endian)
Offset 7 60B MxActions[20] je 3B: type(1) + data_lo(1) + data_hi(1)
Offset 67 36B EncActions[4][3] je 3B
Offset103 20B LedBase[i].R
Offset123 20B LedBase[i].G
Offset143 20B LedBase[i].B
Offset163 20B LedAnim[i] als byte
Offset183 40B LedPeriod[i] als uint16 little-endian
Offset 0 4B Magic 0x56503203 (little-endian)
Offset 4 1B Version = 3
Offset 5 2B CRC16-CCITT über Bytes 7739 (little-endian)
Offset 7 1B active_profile (02)
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 7222 (nach dem CRC-Feld selbst). Muss identisch mit Firmware-Implementierung sein. `DeviceConfig.Crc16()` ist statisch und direkt testbar.
Polynom `0x1021`, Init `0xFFFF`, über Bytes 7739 (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)
- 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 }
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 031
// HidKey: Low-Byte = HID Keycode, High-Byte = Modifier
// HidConsumer: Consumer Usage ID
// HostCommand: Command-ID
// Macro: Slot-Index 031
// ProfileSwitch: 02 = Ziel-Profil, 0xFFFF = nächstes Profil (Zyklus)
}
```
`DeviceAction.Display` gibt einen lesbaren String zurück (z.B. `"Strg+C"`, `"Play/Pause"`, `"Makro 3"`).
`DeviceAction.Display` gibt einen lesbaren String zurück (z.B. `"Strg+C"`, `"Play/Pause"`, `"→ Profil 2"`, `"→ Nächstes Profil"`).
---
@@ -71,7 +87,7 @@ public class DeviceAction {
```csharp
public class MacroTable {
public const int Slots = 32;
public const int MaxSteps = 4;
public const int MaxSteps = 8;
public MacroStep[,] Steps { get; } // [slot][step]
}
public class MacroStep {
@@ -87,9 +103,9 @@ public class MacroStep {
| 019 | MX-Button `mxIdx` (`MacroTable.SlotForMx(mxIdx)`) |
| 2031 | Encoder: `20 + enc * 3 + actIdx` (`MacroTable.SlotForEncoder(enc, actIdx)`) |
### Serialisierung (256 B)
### Serialisierung (512 B)
32 Slots × 4 Steps × 2 B = 256 B. Keycode zuerst, dann Modifier. Kein Magic/CRC (Board akzeptiert jeden Inhalt).
32 Slots × 8 Steps × 2 B = 512 B. Keycode zuerst, dann Modifier. Kein Magic/CRC (Board akzeptiert jeden Inhalt).
---
@@ -101,4 +117,4 @@ public enum LedAnimType : byte {
}
```
Werte entsprechen `LEDAnim` in der Firmware. `FADE_IN` (3) und `FADE_OUT` (4) existieren in der Firmware aber nicht in der GUI (nicht konfigurierbar, nur `COLOR_FADE` intern).
Werte entsprechen `LEDAnim` in der Firmware. `FADE_IN` (3) und `FADE_OUT` (4) existieren in der Firmware aber nicht in der GUI (nicht konfigurierbar).
+9 -5
View File
@@ -8,6 +8,7 @@
- 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ü
@@ -30,19 +31,22 @@ Icon und Tooltip spiegeln den Verbindungsstatus:
| Event-ID | Aktion |
|---|---|
| `EvtConfigBegin` | `_rxConfigBuf = new byte[223]` |
| `EvtConfigBegin` | `_rxConfigBuf = new byte[740]` |
| `EvtConfigData` | Chunk in `_rxConfigBuf` eintragen (`KeyId * 6` = Byte-Offset) |
| `EvtConfigEnd` | `DeviceConfig.FromBytes()``ConfigForm.RefreshAll()` |
| `EvtMacroBegin` | `_rxMacroBuf = new byte[256]` |
| `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" |
| `EvtConfigAck` | MessageBox "Config gespeichert" |
| `EvtConfigNack` | MessageBox "Config FEHLER" |
| `EvtMacroAck` | MessageBox "Makros gespeichert" |
| `EvtKeyDown` | TODO: HOST_COMMAND-Aktion ausführen |
| `EvtEncCw/Ccw` | TODO: Encoder HOST_COMMAND |
ACK/NACK-Events zeigen keine eigene MessageBox mehr — das Ergebnis wird nach Abschluss beider Transfers gebündelt in `ConfigForm.OnSave()` angezeigt.
## Verbindungslebenszyklus
```
+23 -2
View File
@@ -22,13 +22,34 @@ public ushort ResultPeriod { get; } // Periode in ms
| HID Tastatur | `_hidKeyPanel` | Capture-Button + Strg/Shift/Alt/Win-Checkboxen |
| HID Consumer | `_consumerPanel` | Dropdown mit 12 Medien-Aktionen |
| Host Command | `_cmdPanel` | TextBox für numerische Command-ID |
| Makro | `_macroPanel` | 4 Step-Buttons + je Strg/Shift/Alt-Checkboxen |
| 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..."
@@ -41,7 +62,7 @@ WinForms behandelt Pfeil- und Enter-Tasten als "Dialog Keys" in `ProcessDialogKe
## Makro-Capture
Jeder der 4 Steps hat einen eigenen Capture-Button. `_captureStep` (03, -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` (07, -1 = inaktiv) zeigt welcher Step gerade aufnimmt. Capture-Logik identisch mit HID-Modus, schreibt in `_stepKeycodes[captureStep]`.
## Schlüssellookup (layout-unabhängig)