diff --git a/doc/01_serial_manager.md b/doc/01_serial_manager.md index 97f7eae..38e3cd2 100644 --- a/doc/01_serial_manager.md +++ b/doc/01_serial_manager.md @@ -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 diff --git a/doc/02_device_config.md b/doc/02_device_config.md index b4f3a58..d8dc081 100644 --- a/doc/02_device_config.md +++ b/doc/02_device_config.md @@ -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 0–19 | -| `EncActions[4,3]` | `DeviceAction[,]` | Encoder [0–3][SW=0/CW=1/CCW=2] | -| `LedBase[20]` | `Color[]` | RGB-Basis-LED-Farbe je Button | -| `LedAnim[20]` | `LedAnimType[]` | Animation je Button | -| `LedPeriod[20]` | `ushort[]` | Animationsperiode in ms | +| `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, 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 7–222 (little-endian) -Offset 7 60B MxActions[20] je 3B: type(1) + data_lo(1) + data_hi(1) -Offset 67 36B EncActions[4][3] je 3B -Offset103 20B LedBase[i].R -Offset123 20B LedBase[i].G -Offset143 20B LedBase[i].B -Offset163 20B LedAnim[i] als byte -Offset183 40B LedPeriod[i] als uint16 little-endian +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–222 (nach dem CRC-Feld selbst). Muss identisch mit Firmware-Implementierung sein. `DeviceConfig.Crc16()` ist statisch und direkt testbar. +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) +- 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 0–31 + // 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"`, `"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 { | 0–19 | MX-Button `mxIdx` (`MacroTable.SlotForMx(mxIdx)`) | | 20–31 | 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). diff --git a/doc/03_tray_app.md b/doc/03_tray_app.md index adf582b..1c308a9 100644 --- a/doc/03_tray_app.md +++ b/doc/03_tray_app.md @@ -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 ``` diff --git a/doc/05_action_dialog.md b/doc/05_action_dialog.md index 5d0d645..327f2e7 100644 --- a/doc/05_action_dialog.md +++ b/doc/05_action_dialog.md @@ -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` (0–3, -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]`. ## Schlüssellookup (layout-unabhängig) diff --git a/src/ConfigForm.cs b/src/ConfigForm.cs index e15583b..754ae6c 100644 --- a/src/ConfigForm.cs +++ b/src/ConfigForm.cs @@ -291,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); + } }); }); } diff --git a/src/Protocol.cs b/src/Protocol.cs index f3355d3..a1ee1c1 100644 --- a/src/Protocol.cs +++ b/src/Protocol.cs @@ -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. diff --git a/src/SerialManager.cs b/src/SerialManager.cs index 65e16b5..d119e89 100644 --- a/src/SerialManager.cs +++ b/src/SerialManager.cs @@ -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,8 +227,35 @@ 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; @@ -252,14 +285,24 @@ public class SerialManager : IDisposable 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) @@ -289,8 +332,18 @@ public class SerialManager : IDisposable 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 ─────────────────────────────────────────────────────────── diff --git a/src/TrayApp.cs b/src/TrayApp.cs index b5df7de..4ad487b 100644 --- a/src/TrayApp.cs +++ b/src/TrayApp.cs @@ -128,13 +128,11 @@ 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 ───────────────────────────────────────── @@ -163,8 +161,11 @@ 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: