Added Macro functionality, udpated readme
This commit is contained in:
parent
747bec985d
commit
2eb43826ce
122
README.md
122
README.md
@ -36,6 +36,97 @@ Die App erscheint als Icon in der Windows-Taskleiste (System Tray). Kein Hauptfe
|
||||
4. **Auf Board speichern** – überträgt Config in den NVM des Boards
|
||||
5. Beim nächsten Verbinden wird die Config automatisch vom Board geladen
|
||||
|
||||
---
|
||||
|
||||
## Funktionsumfang (Anforderungskatalog)
|
||||
|
||||
### 1 Verbindung & Geräteerkennung
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 1.1 | App läuft als **Windows Tray-Anwendung** ohne sichtbares Hauptfenster | ✅ |
|
||||
| 1.2 | Automatische Board-Erkennung per **WMI / VID+PID** (`0x239A / 0x0042`) | ✅ |
|
||||
| 1.3 | **Automatischer Reconnect** alle 3 s; 5 s Backoff nach Verbindungsverlust | ✅ |
|
||||
| 1.4 | Verbindungsstatus im Tray-Icon (Symbol + Tooltip) sichtbar | ✅ |
|
||||
| 1.5 | Beim Verbinden wird die gespeicherte Config **automatisch vom Board gelesen** | ✅ |
|
||||
| 1.6 | Beim Verbinden wird die **Makro-Tabelle automatisch vom Board gelesen** | ✅ |
|
||||
|
||||
### 2 Tastenbelegung – HID Tastatur
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 2.1 | Taste durch **Drücken erfassen** (kein manuelles HID-ID-Eingeben) | ✅ |
|
||||
| 2.2 | Modifier-Kombination: **Strg / Shift / Alt / Win** einzeln oder kombiniert | ✅ |
|
||||
| 2.3 | **Pfeiltasten, Enter, Escape, F1–F12, Numpad** erfassbar | ✅ |
|
||||
| 2.4 | **Layout-unabhängige Erfassung** via PS/2-Scan-Code → HID-Usage; Ö/Ä/Ü auf QWERTZ korrekt | ✅ |
|
||||
| 2.5 | Taste-Name wird laut **aktivem Windows-Layout** angezeigt (`GetKeyNameText`) | ✅ |
|
||||
| 2.6 | Board führt Tastendruck als **USB-HID-Tastatureingabe** aus (funktioniert ohne laufende App) | ✅ |
|
||||
|
||||
### 3 Tastenbelegung – HID Consumer / Medientasten
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 3.1 | Auswahl per **Dropdown** mit Klartext-Namen | ✅ |
|
||||
| 3.2 | Unterstützte Aktionen: Play/Pause, Nächster/Vorheriger Titel, Stop, Lauter/Leiser, Mute, Taschenrechner, Browser Zurück/Vor | ✅ |
|
||||
|
||||
### 4 Tastenbelegung – Makros
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 4.1 | Bis zu **4 Schritte** pro Makro (je 1 Taste + Modifier) | ✅ |
|
||||
| 4.2 | Jeder Schritt per **Taste drücken** erfassen, inkl. Sondertasten | ✅ |
|
||||
| 4.3 | Leere Schritte werden übersprungen (kürzere Makros möglich) | ✅ |
|
||||
| 4.4 | **32 Makro-Slots** – je ein Slot pro MX-Button (0–19) und Encoder-Aktion (20–31) | ✅ |
|
||||
| 4.5 | Makro-Tabelle wird **separat** vom Board gelesen und geschrieben (NVM Row 1) | ✅ |
|
||||
| 4.6 | Board führt Makro-Schritte mit 10 ms Key-Down + 20 ms Pause **ohne laufende App** aus | ✅ |
|
||||
|
||||
### 5 Encoder-Belegung
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 5.1 | **4 Encoder**, je 3 Aktionen: SW (Drücken), CW (Rechts), CCW (Links) | ✅ |
|
||||
| 5.2 | Gleiche Aktionstypen wie Tasten (HID Key, Consumer, Makro, Host Command) | ✅ |
|
||||
|
||||
### 6 LED-Konfiguration
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 6.1 | **Basis-LED-Farbe** pro MX-Button via Farbpicker (RGB) | ✅ |
|
||||
| 6.2 | **Animationsmodus** pro Button wählbar: Statisch, Blinken, Pulsieren, Regenbogen | ✅ |
|
||||
| 6.3 | **Animations-Tempo**: Schnell (0,5 s) / Mittel (1 s) / Langsam (2 s) / Sehr langsam (4 s) | ✅ |
|
||||
| 6.4 | Regenbogen-Modus: Board berechnet Hue-Sweep lokal, gleichmäßige Phasenverteilung | ✅ |
|
||||
| 6.5 | LED-Config und Aktionstyp im **gleichen Dialog** bearbeitbar | ✅ |
|
||||
|
||||
### 7 Konfiguration speichern & laden
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 7.1 | Config und Makros werden gleichzeitig per **„Auf Board speichern"** übertragen | ✅ |
|
||||
| 7.2 | Board validiert Config mit **CRC16-CCITT + Magic + Version**; NACK bei Fehler | ✅ |
|
||||
| 7.3 | Config bleibt nach Stromverlust im **NVM** erhalten (kein Flash-Verschleiß bei nur Lesen) | ✅ |
|
||||
| 7.4 | **JSON-Export** der gesamten Config (Aktionen + LED) in Datei | ✅ |
|
||||
| 7.5 | **JSON-Import** mit Versionscheck; ungültige Dateien werden abgelehnt | ✅ |
|
||||
|
||||
### 8 Verbindungsprotokoll
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 8.1 | **8-Byte-Festlängen-Pakete** über CDC Serial (kein Treiber nötig) | ✅ |
|
||||
| 8.2 | **Ping/Pong** zur manuellen Verbindungsdiagnose im Konfigurations-Fenster | ✅ |
|
||||
| 8.3 | Config-Transfer: BEGIN → n×DATA(6 Byte) → COMMIT → ACK/NACK | ✅ |
|
||||
| 8.4 | Makro-Transfer: gleiche Struktur, 43 Chunks à 6 Byte (256 Byte Tabelle) | ✅ |
|
||||
| 8.5 | Debug-Logging aller RX-Pakete in `%TEMP%\versapad_rx.txt` | ✅ |
|
||||
|
||||
### 9 Nicht implementiert / Roadmap
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 9.1 | **Host Command**: URL/Programm öffnen wenn Board-Taste gedrückt | 🔲 TODO |
|
||||
| 9.2 | Eigenes **Tray-Icon** (aktuell: Windows-Standard-Icon) | 🔲 TODO |
|
||||
| 9.3 | **Fader/Potentiometer**-Unterstützung (3× ADC-Achsen auf Board vorhanden) | 🔲 TODO |
|
||||
|
||||
---
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
@ -44,10 +135,11 @@ VersaGUI/
|
||||
├── Program.cs – Einstiegspunkt, startet TrayApp
|
||||
├── TrayApp.cs – ApplicationContext: Tray-Icon, Menü, Paket-Routing
|
||||
├── ConfigForm.cs – Konfigurations-Fenster (4×5 Grid + Encoder-Panel)
|
||||
├── ActionDialog.cs – Dialog: Taste erfassen / Consumer auswählen / Host-Command
|
||||
├── DeviceConfig.cs – C#-Spiegel von SDeviceConfig (163B Serialisierung, CRC16)
|
||||
├── ActionDialog.cs – Dialog: Taste erfassen / Consumer / Makro / LED-Animation
|
||||
├── DeviceConfig.cs – C#-Spiegel von SDeviceConfig (223 B, Version 2) + MacroTable (256 B)
|
||||
├── ConfigJson.cs – JSON-Export/Import der DeviceConfig
|
||||
├── Protocol.cs – Protokoll-Konstanten (Commands + Events)
|
||||
└── SerialManager.cs – COM-Port-Erkennung, Read-Loop, Config senden/empfangen
|
||||
└── SerialManager.cs – COM-Port-Erkennung, Read-Loop, Config + Makros senden/empfangen
|
||||
```
|
||||
|
||||
## Architektur
|
||||
@ -71,22 +163,32 @@ TrayApp
|
||||
|
||||
### DeviceConfig / Serialisierung
|
||||
|
||||
`DeviceConfig.ToBytes()` erzeugt den exakt 163-Byte-Puffer der `SDeviceConfig`-Struct in der Firmware entspricht (little-endian, packed). `CRC16-CCITT` (Poly 0x1021, Init 0xFFFF) über Bytes 7–162 – identisch zur Firmware-Implementierung.
|
||||
`DeviceConfig.ToBytes()` erzeugt den exakt **223-Byte**-Puffer der `SDeviceConfig`-Struct (Version 2, little-endian, packed). `CRC16-CCITT` (Poly 0x1021, Init 0xFFFF) über Bytes 7–222 – identisch zur Firmware-Implementierung.
|
||||
|
||||
`MacroTable.ToBytes()` erzeugt **256 Byte** (32 Slots × 4 Steps × 2 Byte).
|
||||
|
||||
Config-Übertragung Board→PC (Lesen):
|
||||
```
|
||||
PC sendet: CMD_CONFIG_READ (0x13)
|
||||
Board sendet: EVT_CONFIG_BEGIN (Chunk-Anzahl)
|
||||
EVT_CONFIG_DATA × 28 (je 6 Nutzbytes)
|
||||
Board sendet: EVT_CONFIG_BEGIN (Chunk-Anzahl = 38)
|
||||
EVT_CONFIG_DATA × 38 (je 6 Nutzbytes)
|
||||
EVT_CONFIG_END
|
||||
```
|
||||
|
||||
Config-Übertragung PC→Board (Schreiben):
|
||||
```
|
||||
PC sendet: CMD_CONFIG_BEGIN (Chunk-Anzahl)
|
||||
CMD_CONFIG_DATA × 28
|
||||
CMD_CONFIG_COMMIT
|
||||
Board sendet: EVT_CONFIG_ACK oder EVT_CONFIG_NACK
|
||||
PC sendet: CMD_CONFIG_BEGIN (0x10, Chunk-Anzahl)
|
||||
CMD_CONFIG_DATA × 38
|
||||
CMD_CONFIG_COMMIT (0x12)
|
||||
Board sendet: EVT_CONFIG_ACK (0x90) oder EVT_CONFIG_NACK (0x91)
|
||||
```
|
||||
|
||||
Makro-Übertragung (analog, separate Kommandos 0x20–0x23):
|
||||
```
|
||||
PC sendet: CMD_MACRO_BEGIN (0x20, Chunk-Anzahl = 43)
|
||||
CMD_MACRO_DATA × 43
|
||||
CMD_MACRO_COMMIT (0x22)
|
||||
Board sendet: EVT_MACRO_ACK (0x95)
|
||||
```
|
||||
|
||||
### Bekannte .NET-Eigenheiten
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
// Host Command → Zahlen-Eingabe (Command-ID)
|
||||
// Keine Aktion → nichts
|
||||
//
|
||||
// Optional (nur MX-Buttons): LED-Basisfarbe
|
||||
// Optional (nur MX-Buttons): LED-Basisfarbe + LED-Animation + Geschwindigkeit
|
||||
|
||||
namespace VersaGUI;
|
||||
|
||||
@ -16,6 +16,8 @@ public class ActionDialog : Form
|
||||
// ── Ergebnis ──────────────────────────────────────────────────────────────
|
||||
public DeviceAction ResultAction { get; private set; }
|
||||
public Color ResultColor { get; private set; }
|
||||
public LedAnimType ResultAnim { get; private set; }
|
||||
public ushort ResultPeriod { get; private set; }
|
||||
|
||||
// ── Controls ──────────────────────────────────────────────────────────────
|
||||
private readonly ComboBox _typeCombo;
|
||||
@ -38,85 +40,131 @@ public class ActionDialog : Form
|
||||
private readonly Button _colorBtn;
|
||||
private Color _color;
|
||||
|
||||
// LED-Animation
|
||||
private readonly Panel _animPanel;
|
||||
private readonly ComboBox _animCombo;
|
||||
private readonly ComboBox _periodCombo;
|
||||
|
||||
// Makro
|
||||
private readonly Panel _macroPanel;
|
||||
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 int _captureStep = -1; // -1 = nicht im Capture-Modus
|
||||
|
||||
// Capture-Zustand
|
||||
private bool _capturing;
|
||||
private byte _hidKeycode; // 0 = nicht belegt
|
||||
|
||||
// Layout
|
||||
private readonly bool _showColor;
|
||||
private Button _okBtn = null!;
|
||||
private Button _cancelBtn = null!;
|
||||
|
||||
// ── Lookup-Tabellen ───────────────────────────────────────────────────────
|
||||
|
||||
// Windows VK → HID Keyboard Usage (USB HID Usage Table 0x07)
|
||||
private static readonly Dictionary<Keys, byte> s_vkToHid = new()
|
||||
// P/Invoke: Windows-API für layout-unabhängige Scan-Code-Konvertierung
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
private static extern uint MapVirtualKey(uint uCode, uint uMapType);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll",
|
||||
CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
|
||||
private static extern int GetKeyNameText(int lParam,
|
||||
System.Text.StringBuilder lpString, int nSize);
|
||||
|
||||
// Scan-Code (PS/2 Set 1) → HID Usage (physische Tastenposition, Layout-unabhängig).
|
||||
// Korrekt für QWERTZ, AZERTY und andere Layouts: Ö = Scan 0x27 → HID 0x33 (;-Position)
|
||||
private static readonly Dictionary<byte, byte> s_scanToHid = new()
|
||||
{
|
||||
// Buchstaben
|
||||
[Keys.A]=0x04,[Keys.B]=0x05,[Keys.C]=0x06,[Keys.D]=0x07,
|
||||
[Keys.E]=0x08,[Keys.F]=0x09,[Keys.G]=0x0A,[Keys.H]=0x0B,
|
||||
[Keys.I]=0x0C,[Keys.J]=0x0D,[Keys.K]=0x0E,[Keys.L]=0x0F,
|
||||
[Keys.M]=0x10,[Keys.N]=0x11,[Keys.O]=0x12,[Keys.P]=0x13,
|
||||
[Keys.Q]=0x14,[Keys.R]=0x15,[Keys.S]=0x16,[Keys.T]=0x17,
|
||||
[Keys.U]=0x18,[Keys.V]=0x19,[Keys.W]=0x1A,[Keys.X]=0x1B,
|
||||
[Keys.Y]=0x1C,[Keys.Z]=0x1D,
|
||||
// Ziffernreihe
|
||||
[Keys.D1]=0x1E,[Keys.D2]=0x1F,[Keys.D3]=0x20,[Keys.D4]=0x21,
|
||||
[Keys.D5]=0x22,[Keys.D6]=0x23,[Keys.D7]=0x24,[Keys.D8]=0x25,
|
||||
[Keys.D9]=0x26,[Keys.D0]=0x27,
|
||||
// Steuerung
|
||||
[Keys.Return]=0x28,[Keys.Escape]=0x29,[Keys.Back]=0x2A,
|
||||
[Keys.Tab]=0x2B, [Keys.Space]=0x2C, [Keys.Delete]=0x4C,
|
||||
[Keys.Insert]=0x49,[Keys.Home]=0x4A, [Keys.PageUp]=0x4B,
|
||||
[Keys.End]=0x4D, [Keys.PageDown]=0x4E,
|
||||
[Keys.Right]=0x4F,[Keys.Left]=0x50, [Keys.Down]=0x51,[Keys.Up]=0x52,
|
||||
[Keys.CapsLock]=0x39,
|
||||
[Keys.PrintScreen]=0x46,[Keys.Scroll]=0x47,[Keys.Pause]=0x48,
|
||||
[Keys.NumLock]=0x53,
|
||||
// F-Tasten
|
||||
[Keys.F1]=0x3A,[Keys.F2]=0x3B,[Keys.F3]=0x3C,[Keys.F4]=0x3D,
|
||||
[Keys.F5]=0x3E,[Keys.F6]=0x3F,[Keys.F7]=0x40,[Keys.F8]=0x41,
|
||||
[Keys.F9]=0x42,[Keys.F10]=0x43,[Keys.F11]=0x44,[Keys.F12]=0x45,
|
||||
// Sonderzeichen (QWERTZ-kompatibel, basiert auf US-HID-Positionen)
|
||||
[Keys.OemMinus]=0x2D, [Keys.Oemplus]=0x2E,
|
||||
[Keys.OemOpenBrackets]=0x2F, [Keys.OemCloseBrackets]=0x30,
|
||||
[Keys.OemPipe]=0x31, [Keys.OemSemicolon]=0x33,
|
||||
[Keys.OemQuotes]=0x34, [Keys.Oemtilde]=0x35,
|
||||
[Keys.Oemcomma]=0x36, [Keys.OemPeriod]=0x37,
|
||||
[Keys.OemQuestion]=0x38,
|
||||
// Numpad
|
||||
[Keys.NumPad1]=0x59,[Keys.NumPad2]=0x5A,[Keys.NumPad3]=0x5B,
|
||||
[Keys.NumPad4]=0x5C,[Keys.NumPad5]=0x5D,[Keys.NumPad6]=0x5E,
|
||||
[Keys.NumPad7]=0x5F,[Keys.NumPad8]=0x60,[Keys.NumPad9]=0x61,
|
||||
[Keys.NumPad0]=0x62,[Keys.Decimal]=0x63,
|
||||
[Keys.Multiply]=0x55,[Keys.Add]=0x57,
|
||||
[Keys.Subtract]=0x56,[Keys.Divide]=0x54,
|
||||
[0x01]=0x29, // Escape
|
||||
[0x02]=0x1E,[0x03]=0x1F,[0x04]=0x20,[0x05]=0x21,[0x06]=0x22,
|
||||
[0x07]=0x23,[0x08]=0x24,[0x09]=0x25,[0x0A]=0x26,[0x0B]=0x27, // 1–0
|
||||
[0x0C]=0x2D,[0x0D]=0x2E, // -/ß =/´
|
||||
[0x0E]=0x2A, // Backspace
|
||||
[0x0F]=0x2B, // Tab
|
||||
[0x10]=0x14,[0x11]=0x1A,[0x12]=0x08,[0x13]=0x15,[0x14]=0x17, // Q W E R T
|
||||
[0x15]=0x1C,[0x16]=0x18,[0x17]=0x0C,[0x18]=0x12,[0x19]=0x13, // Z/Y U I O P
|
||||
[0x1A]=0x2F,[0x1B]=0x30, // Ü +
|
||||
[0x1C]=0x28, // Enter
|
||||
[0x1E]=0x04,[0x1F]=0x16,[0x20]=0x07,[0x21]=0x09,[0x22]=0x0A, // A S D F G
|
||||
[0x23]=0x0B,[0x24]=0x0D,[0x25]=0x0E,[0x26]=0x0F, // H J K L
|
||||
[0x27]=0x33,[0x28]=0x34,[0x29]=0x35, // Ö Ä ^
|
||||
[0x2B]=0x31, // #
|
||||
[0x2C]=0x1D,[0x2D]=0x1B,[0x2E]=0x06,[0x2F]=0x19,[0x30]=0x05, // Y/Z X C V B
|
||||
[0x31]=0x11,[0x32]=0x10,[0x33]=0x36,[0x34]=0x37,[0x35]=0x38, // N M , . -
|
||||
[0x37]=0x55, // Numpad *
|
||||
[0x39]=0x2C, // Space
|
||||
[0x3A]=0x39, // CapsLock
|
||||
[0x3B]=0x3A,[0x3C]=0x3B,[0x3D]=0x3C,[0x3E]=0x3D,[0x3F]=0x3E, // F1–F5
|
||||
[0x40]=0x3F,[0x41]=0x40,[0x42]=0x41,[0x43]=0x42,[0x44]=0x43, // F6–F10
|
||||
[0x45]=0x53,[0x46]=0x47, // NumLock ScrollLock
|
||||
[0x47]=0x5F,[0x48]=0x60,[0x49]=0x61,[0x4A]=0x56, // Num7 8 9 -
|
||||
[0x4B]=0x5C,[0x4C]=0x5D,[0x4D]=0x5E,[0x4E]=0x57, // Num4 5 6 +
|
||||
[0x4F]=0x59,[0x50]=0x5A,[0x51]=0x5B,[0x52]=0x62,[0x53]=0x63, // Num1 2 3 0 .
|
||||
[0x56]=0x64, // Non-US \ (< > auf QWERTZ)
|
||||
[0x57]=0x44,[0x58]=0x45, // F11 F12
|
||||
};
|
||||
|
||||
// HID-Code → lesbarer Name (für Capture-Button-Beschriftung)
|
||||
private static readonly Dictionary<byte, string> s_hidNames = new()
|
||||
// HID → Scan-Code (umgekehrte Tabelle, für Anzeigenamen)
|
||||
private static readonly Dictionary<byte, byte> s_hidToScan =
|
||||
s_scanToHid.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
|
||||
// Direkte VK→HID-Zuordnung nur für erweiterte Navigationstasten.
|
||||
// MapVirtualKey gibt für diese den Numpad-Scan-Code zurück (falsch).
|
||||
private static readonly Dictionary<Keys, byte> s_extVkToHid = new()
|
||||
{
|
||||
[0x04]="A",[0x05]="B",[0x06]="C",[0x07]="D",[0x08]="E",[0x09]="F",
|
||||
[0x0A]="G",[0x0B]="H",[0x0C]="I",[0x0D]="J",[0x0E]="K",[0x0F]="L",
|
||||
[0x10]="M",[0x11]="N",[0x12]="O",[0x13]="P",[0x14]="Q",[0x15]="R",
|
||||
[0x16]="S",[0x17]="T",[0x18]="U",[0x19]="V",[0x1A]="W",[0x1B]="X",
|
||||
[0x1C]="Y",[0x1D]="Z",
|
||||
[0x1E]="1",[0x1F]="2",[0x20]="3",[0x21]="4",[0x22]="5",
|
||||
[0x23]="6",[0x24]="7",[0x25]="8",[0x26]="9",[0x27]="0",
|
||||
[0x28]="Enter", [0x29]="Escape", [0x2A]="Backspace",
|
||||
[0x2B]="Tab", [0x2C]="Leertaste", [0x2D]="-",
|
||||
[0x2E]="=", [0x2F]="[", [0x30]="]",
|
||||
[0x31]="\\", [0x33]=";", [0x34]="'",
|
||||
[0x35]="`", [0x36]=",", [0x37]=".",
|
||||
[0x38]="/", [0x39]="CapsLock",
|
||||
[0x3A]="F1", [0x3B]="F2", [0x3C]="F3", [0x3D]="F4",
|
||||
[0x3E]="F5", [0x3F]="F6", [0x40]="F7", [0x41]="F8",
|
||||
[0x42]="F9", [0x43]="F10",[0x44]="F11",[0x45]="F12",
|
||||
[0x46]="Druck", [0x47]="Rollen", [0x48]="Pause",
|
||||
[0x49]="Einfg", [0x4A]="Pos1", [0x4B]="Bild Auf",
|
||||
[0x4C]="Entf", [0x4D]="Ende", [0x4E]="Bild Ab",
|
||||
[0x4F]="Rechts", [0x50]="Links", [0x51]="Runter", [0x52]="Hoch",
|
||||
[0x53]="NumLock",[0x54]="Num/", [0x55]="Num*",
|
||||
[0x56]="Num-", [0x57]="Num+",
|
||||
[Keys.Up]=0x52, [Keys.Down]=0x51, [Keys.Left]=0x50,
|
||||
[Keys.Right]=0x4F, [Keys.Home]=0x4A, [Keys.End]=0x4D,
|
||||
[Keys.PageUp]=0x4B, [Keys.PageDown]=0x4E, [Keys.Insert]=0x49,
|
||||
[Keys.Delete]=0x4C, [Keys.Pause]=0x48, [Keys.Scroll]=0x47,
|
||||
[Keys.PrintScreen]=0x46,
|
||||
};
|
||||
|
||||
// Windows VK → HID via Scan-Code (layout-unabhängig).
|
||||
// Ö auf QWERTZ → Scan 0x27 → HID 0x33 (;-Position auf US) → Board sendet HID 0x33
|
||||
// → Zielrechner mit QWERTZ-Layout erzeugt korrekt Ö.
|
||||
private static byte VkToHid(Keys vk)
|
||||
{
|
||||
if (s_extVkToHid.TryGetValue(vk, out byte hExt)) return hExt;
|
||||
byte sc = (byte)MapVirtualKey((uint)(vk & Keys.KeyCode), 0u /* VK→SC */);
|
||||
return s_scanToHid.TryGetValue(sc, out byte hid) ? hid : (byte)0;
|
||||
}
|
||||
|
||||
// HID-Code → lesbarer Tastenname laut aktivem Windows-Tastaturlayout.
|
||||
// Gibt auf QWERTZ "Ö" für HID 0x33, auf US-Layout ";" zurück.
|
||||
private static string HidKeyName(byte hid)
|
||||
{
|
||||
// Sondertasten ohne Scan-Code-Eintrag
|
||||
var special = (Dictionary<byte, string>)new Dictionary<byte, string>
|
||||
{
|
||||
[0x28]="Enter", [0x29]="Escape", [0x2A]="Backspace", [0x2B]="Tab",
|
||||
[0x2C]="Leer", [0x39]="Caps", [0x46]="Druck", [0x47]="Rollen",
|
||||
[0x48]="Pause", [0x49]="Einfg", [0x4A]="Pos1", [0x4B]="Bild↑",
|
||||
[0x4C]="Entf", [0x4D]="Ende", [0x4E]="Bild↓",
|
||||
[0x4F]="→", [0x50]="←", [0x51]="↓", [0x52]="↑",
|
||||
[0x53]="NumLock",[0x54]="Num/", [0x55]="Num*", [0x56]="Num-",
|
||||
[0x57]="Num+", [0x58]="NumEnter",
|
||||
[0x59]="Num1", [0x5A]="Num2", [0x5B]="Num3", [0x5C]="Num4",
|
||||
[0x5D]="Num5", [0x5E]="Num6", [0x5F]="Num7", [0x60]="Num8",
|
||||
[0x61]="Num9", [0x62]="Num0", [0x63]="Num.",
|
||||
[0x3A]="F1",[0x3B]="F2",[0x3C]="F3",[0x3D]="F4",[0x3E]="F5",
|
||||
[0x3F]="F6",[0x40]="F7",[0x41]="F8",[0x42]="F9",[0x43]="F10",
|
||||
[0x44]="F11",[0x45]="F12",
|
||||
};
|
||||
if (special.TryGetValue(hid, out string? name)) return name;
|
||||
|
||||
// Für Zeichen-Tasten: GetKeyNameText gibt den lokalisierten Namen zurück
|
||||
if (!s_hidToScan.TryGetValue(hid, out byte sc)) return $"0x{hid:X2}";
|
||||
int lParam = sc << 16;
|
||||
var sb = new System.Text.StringBuilder(32);
|
||||
GetKeyNameText(lParam, sb, sb.Capacity);
|
||||
return sb.Length > 0 ? sb.ToString() : $"0x{hid:X2}";
|
||||
}
|
||||
|
||||
// HID Consumer Usage IDs (Usage Page 0x0C)
|
||||
private static readonly (ushort Usage, string Name)[] s_consumer =
|
||||
@ -137,11 +185,43 @@ public class ActionDialog : Form
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public ActionDialog(DeviceAction action, Color ledColor, bool showColor = true)
|
||||
// Animations-ComboBox-Einträge: (Anzeigename, LedAnimType, Farbe sinnvoll?)
|
||||
private static readonly (string Name, LedAnimType Type, bool ShowColor)[] s_anims =
|
||||
{
|
||||
("Statisch", LedAnimType.Static, true),
|
||||
("Blinken", LedAnimType.Blink, true),
|
||||
("Pulsieren", LedAnimType.Pulse, true),
|
||||
("Regenbogen", LedAnimType.ColorCycle, false),
|
||||
};
|
||||
|
||||
// Geschwindigkeits-Presets: (Anzeigename, Millisekunden)
|
||||
private static readonly (string Name, ushort Ms)[] s_periods =
|
||||
{
|
||||
("Schnell (0.5s)", 500),
|
||||
("Mittel (1s)", 1000),
|
||||
("Langsam (2s)", 2000),
|
||||
("Sehr langsam (4s)", 4000),
|
||||
};
|
||||
|
||||
public ActionDialog(DeviceAction action, Color ledColor,
|
||||
LedAnimType ledAnim = LedAnimType.ColorCycle,
|
||||
ushort ledPeriod = 4000,
|
||||
MacroTable? macros = null,
|
||||
int macroSlot = 0,
|
||||
bool showColor = true)
|
||||
{
|
||||
ResultAction = action.Clone();
|
||||
ResultColor = ledColor;
|
||||
ResultAnim = ledAnim;
|
||||
ResultPeriod = ledPeriod;
|
||||
_color = ledColor;
|
||||
_macros = macros ?? new MacroTable();
|
||||
_macroSlot = macroSlot;
|
||||
_showColor = showColor;
|
||||
|
||||
// Bestehende Makro-Steps aus der Tabelle laden
|
||||
for (int i = 0; i < 4; i++)
|
||||
_stepKeycodes[i] = _macros.Steps[_macroSlot, i].Keycode;
|
||||
|
||||
// Formular-Grundeinstellungen
|
||||
Text = "Aktion bearbeiten";
|
||||
@ -156,20 +236,20 @@ public class ActionDialog : Form
|
||||
_typeCombo = new ComboBox
|
||||
{
|
||||
Location = new Point(80, 12),
|
||||
Width = 208,
|
||||
Width = 316,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
};
|
||||
_typeCombo.Items.AddRange(new object[]
|
||||
{ "Keine Aktion", "HID Tastatur", "HID Consumer", "Host Command" });
|
||||
{ "Keine Aktion", "HID Tastatur", "HID Consumer", "Host Command", "Makro" });
|
||||
_typeCombo.SelectedIndex = (int)action.Type;
|
||||
_typeCombo.SelectedIndexChanged += OnTypeChanged;
|
||||
|
||||
// ── HID-Tastatur-Panel ────────────────────────────────────────────────
|
||||
_hidKeyPanel = new Panel { Location = new Point(12, 48), Size = new Size(280, 110) };
|
||||
_hidKeyPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 110) };
|
||||
|
||||
_captureBtn = new Button
|
||||
{
|
||||
Size = new Size(280, 48),
|
||||
Size = new Size(396, 48),
|
||||
Location = new Point(0, 0),
|
||||
FlatStyle = FlatStyle.Flat,
|
||||
Font = new Font(Font.FontFamily, 12, FontStyle.Regular),
|
||||
@ -213,11 +293,11 @@ public class ActionDialog : Form
|
||||
RefreshCaptureBtn(); // erst nach Checkbox-Initialisierung aufrufen
|
||||
|
||||
// ── Consumer-Panel ────────────────────────────────────────────────────
|
||||
_consumerPanel = new Panel { Location = new Point(12, 48), Size = new Size(280, 30) };
|
||||
_consumerPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 30) };
|
||||
_consumerCombo = new ComboBox
|
||||
{
|
||||
Location = new Point(0, 0),
|
||||
Width = 280,
|
||||
Width = 396,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
};
|
||||
foreach (var (_, name) in s_consumer)
|
||||
@ -230,12 +310,12 @@ public class ActionDialog : Form
|
||||
_consumerPanel.Controls.Add(_consumerCombo);
|
||||
|
||||
// ── Host-Command-Panel ────────────────────────────────────────────────
|
||||
_cmdPanel = new Panel { Location = new Point(12, 48), Size = new Size(280, 30) };
|
||||
_cmdPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 30) };
|
||||
var cmdLabel = new Label { Text = "Command-ID:", Location = new Point(0, 8), AutoSize = true };
|
||||
_cmdBox = new TextBox
|
||||
{
|
||||
Location = new Point(90, 4),
|
||||
Width = 190,
|
||||
Width = 306,
|
||||
Text = action.Type == ActionType.HostCommand ? $"{action.Data}" : "0",
|
||||
};
|
||||
_cmdPanel.Controls.AddRange(new Control[] { cmdLabel, _cmdBox });
|
||||
@ -244,14 +324,14 @@ public class ActionDialog : Form
|
||||
_colorPanel = new Panel
|
||||
{
|
||||
Location = new Point(12, 168),
|
||||
Size = new Size(280, 30),
|
||||
Size = new Size(396, 30),
|
||||
Visible = showColor,
|
||||
};
|
||||
var colorLabel = new Label { Text = "LED-Farbe:", Location = new Point(0, 8), AutoSize = true };
|
||||
_colorBtn = new Button
|
||||
{
|
||||
Location = new Point(80, 2),
|
||||
Size = new Size(200, 26),
|
||||
Size = new Size(316, 26),
|
||||
BackColor = ledColor,
|
||||
FlatStyle = FlatStyle.Flat,
|
||||
Text = string.Empty,
|
||||
@ -260,37 +340,126 @@ public class ActionDialog : Form
|
||||
_colorBtn.Click += OnColorClick;
|
||||
_colorPanel.Controls.AddRange(new Control[] { colorLabel, _colorBtn });
|
||||
|
||||
// ── Buttons Fußzeile ──────────────────────────────────────────────────
|
||||
int footerY = showColor ? 208 : 168;
|
||||
var okBtn = new Button
|
||||
// ── Animation-Panel ───────────────────────────────────────────────────
|
||||
_animPanel = new Panel
|
||||
{
|
||||
Location = new Point(12, 208),
|
||||
Size = new Size(396, 58),
|
||||
Visible = showColor,
|
||||
};
|
||||
|
||||
var animLabel = new Label { Text = "Animation:", Location = new Point(0, 8), AutoSize = true };
|
||||
_animCombo = new ComboBox
|
||||
{
|
||||
Location = new Point(80, 4),
|
||||
Width = 316,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
};
|
||||
foreach (var (name, _, _) in s_anims)
|
||||
_animCombo.Items.Add(name);
|
||||
|
||||
int animIdx = Array.FindIndex(s_anims, a => a.Type == ledAnim);
|
||||
_animCombo.SelectedIndex = animIdx >= 0 ? animIdx : 0;
|
||||
_animCombo.SelectedIndexChanged += OnAnimChanged;
|
||||
|
||||
var periodLabel = new Label { Text = "Tempo:", Location = new Point(0, 36), AutoSize = true };
|
||||
_periodCombo = new ComboBox
|
||||
{
|
||||
Location = new Point(80, 32),
|
||||
Width = 316,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
};
|
||||
foreach (var (name, _) in s_periods)
|
||||
_periodCombo.Items.Add(name);
|
||||
|
||||
int periodIdx = Array.FindIndex(s_periods, p => p.Ms == ledPeriod);
|
||||
_periodCombo.SelectedIndex = periodIdx >= 0 ? periodIdx : 3; // Default: sehr langsam
|
||||
|
||||
_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) };
|
||||
|
||||
var macroHint = new Label
|
||||
{
|
||||
Text = "Klicke einen Step, dann Taste drücken. Leere Steps werden übersprungen.",
|
||||
Location = new Point(0, 0),
|
||||
Size = new Size(396, 28),
|
||||
ForeColor = SystemColors.GrayText,
|
||||
Font = new Font(Font.FontFamily, 8),
|
||||
};
|
||||
_macroPanel.Controls.Add(macroHint);
|
||||
|
||||
for (int step = 0; step < 4; step++)
|
||||
{
|
||||
int s = step;
|
||||
int y2 = 30 + step * 30;
|
||||
|
||||
var stepLabel = new Label
|
||||
{
|
||||
Text = $"Step {step + 1}:",
|
||||
Location = new Point(0, y2 + 6),
|
||||
AutoSize = true,
|
||||
};
|
||||
|
||||
_stepBtns[step] = new Button
|
||||
{
|
||||
Location = new Point(50, y2),
|
||||
Size = new Size(160, 24),
|
||||
FlatStyle = FlatStyle.Flat,
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
_stepBtns[step].FlatAppearance.BorderColor = Color.DarkGray;
|
||||
_stepBtns[step].Click += (_, _) => StartMacroCapture(s);
|
||||
|
||||
_stepCtrl[step] = new CheckBox { Text = "Strg", Location = new Point(218, y2 + 4), AutoSize = true };
|
||||
_stepShift[step] = new CheckBox { Text = "Shift", Location = new Point(268, y2 + 4), AutoSize = true };
|
||||
_stepAlt[step] = new CheckBox { Text = "Alt", Location = new Point(326, y2 + 4), AutoSize = true };
|
||||
|
||||
// Bestehende Werte aus Makro-Tabelle laden
|
||||
_stepCtrl[step].Checked = (_macros.Steps[_macroSlot, step].Modifier & 0x01) != 0;
|
||||
_stepShift[step].Checked = (_macros.Steps[_macroSlot, step].Modifier & 0x02) != 0;
|
||||
_stepAlt[step].Checked = (_macros.Steps[_macroSlot, step].Modifier & 0x04) != 0;
|
||||
|
||||
_stepCtrl[step].CheckedChanged += (_, _) => RefreshStepBtn(s);
|
||||
_stepShift[step].CheckedChanged += (_, _) => RefreshStepBtn(s);
|
||||
_stepAlt[step].CheckedChanged += (_, _) => RefreshStepBtn(s);
|
||||
|
||||
_macroPanel.Controls.AddRange(new Control[]
|
||||
{ stepLabel, _stepBtns[step], _stepCtrl[step], _stepShift[step], _stepAlt[step] });
|
||||
|
||||
RefreshStepBtn(step);
|
||||
}
|
||||
|
||||
// ── Buttons Fußzeile ─────────────────────────────────────────────────
|
||||
_okBtn = new Button
|
||||
{
|
||||
Text = "OK",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new Point(112, footerY),
|
||||
Width = 80,
|
||||
};
|
||||
okBtn.Click += OnOk;
|
||||
_okBtn.Click += OnOk;
|
||||
|
||||
var cancelBtn = new Button
|
||||
_cancelBtn = new Button
|
||||
{
|
||||
Text = "Abbrechen",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(200, footerY),
|
||||
Width = 92,
|
||||
};
|
||||
|
||||
AcceptButton = okBtn;
|
||||
CancelButton = cancelBtn;
|
||||
ClientSize = new Size(304, footerY + 44);
|
||||
AcceptButton = _okBtn;
|
||||
CancelButton = _cancelBtn;
|
||||
|
||||
Controls.AddRange(new Control[]
|
||||
{
|
||||
typeLabel, _typeCombo,
|
||||
_hidKeyPanel, _consumerPanel, _cmdPanel, _colorPanel,
|
||||
okBtn, cancelBtn,
|
||||
_hidKeyPanel, _consumerPanel, _cmdPanel, _macroPanel,
|
||||
_colorPanel, _animPanel,
|
||||
_okBtn, _cancelBtn,
|
||||
});
|
||||
|
||||
UpdatePanelVisibility();
|
||||
UpdatePanelVisibility(); // setzt Visibility + ruft UpdateLayout auf
|
||||
}
|
||||
|
||||
// ── Panel-Sichtbarkeit ────────────────────────────────────────────────────
|
||||
@ -301,12 +470,62 @@ public class ActionDialog : Form
|
||||
UpdatePanelVisibility();
|
||||
}
|
||||
|
||||
private void OnAnimChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateAnimVisibility();
|
||||
}
|
||||
|
||||
private void UpdatePanelVisibility()
|
||||
{
|
||||
var t = (ActionType)_typeCombo.SelectedIndex;
|
||||
_hidKeyPanel.Visible = t == ActionType.HidKey;
|
||||
_consumerPanel.Visible = t == ActionType.HidConsumer;
|
||||
_cmdPanel.Visible = t == ActionType.HostCommand;
|
||||
_macroPanel.Visible = t == ActionType.Macro;
|
||||
_colorPanel.Visible = _showColor;
|
||||
_animPanel.Visible = _showColor;
|
||||
UpdateAnimVisibility();
|
||||
UpdateLayout();
|
||||
}
|
||||
|
||||
// Positioniert LED-Panels dynamisch unterhalb des aktiven Typ-Panels
|
||||
// und passt die Formulargröße an.
|
||||
private void UpdateLayout()
|
||||
{
|
||||
var t = (ActionType)_typeCombo.SelectedIndex;
|
||||
|
||||
int contentH = t switch
|
||||
{
|
||||
ActionType.HidKey => 110,
|
||||
ActionType.HidConsumer => 30,
|
||||
ActionType.HostCommand => 30,
|
||||
ActionType.Macro => 4 * 30 + 22,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
int ledY = 48 + contentH + 10;
|
||||
int footerY = ledY;
|
||||
|
||||
if (_showColor)
|
||||
{
|
||||
_colorPanel.Location = new Point(12, ledY);
|
||||
_animPanel.Location = new Point(12, ledY + 38);
|
||||
footerY = ledY + 38 + 62;
|
||||
}
|
||||
|
||||
_okBtn.Location = new Point(224, footerY);
|
||||
_cancelBtn.Location = new Point(312, footerY);
|
||||
ClientSize = new Size(420, footerY + 44);
|
||||
}
|
||||
|
||||
private void UpdateAnimVisibility()
|
||||
{
|
||||
if (_animPanel.Visible && _animCombo.SelectedIndex >= 0)
|
||||
{
|
||||
var (_, animType, showColorFlag) = s_anims[_animCombo.SelectedIndex];
|
||||
_colorBtn.Enabled = showColorFlag;
|
||||
_periodCombo.Enabled = animType != LedAnimType.Static;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tasten-Erfassung ──────────────────────────────────────────────────────
|
||||
@ -317,6 +536,40 @@ public class ActionDialog : Form
|
||||
else StartCapture();
|
||||
}
|
||||
|
||||
// ── Makro-Step-Capture ────────────────────────────────────────────────────
|
||||
|
||||
private void StartMacroCapture(int step)
|
||||
{
|
||||
_captureStep = step;
|
||||
_stepBtns[step].Text = "Taste drücken...";
|
||||
_stepBtns[step].BackColor = Color.FromArgb(255, 220, 80);
|
||||
_stepBtns[step].ForeColor = Color.Black;
|
||||
}
|
||||
|
||||
private void RefreshStepBtn(int step)
|
||||
{
|
||||
if (_captureStep == step) return; // gerade im Capture-Modus
|
||||
byte kc = _stepKeycodes[step];
|
||||
if (kc == 0)
|
||||
{
|
||||
_stepBtns[step].Text = "— (leer)";
|
||||
_stepBtns[step].BackColor = SystemColors.Control;
|
||||
_stepBtns[step].ForeColor = SystemColors.GrayText;
|
||||
}
|
||||
else
|
||||
{
|
||||
string name = HidKeyName(kc);
|
||||
var parts = new List<string>();
|
||||
if (_stepCtrl[step].Checked) parts.Add("Strg");
|
||||
if (_stepShift[step].Checked) parts.Add("Shift");
|
||||
if (_stepAlt[step].Checked) parts.Add("Alt");
|
||||
parts.Add(name);
|
||||
_stepBtns[step].Text = string.Join("+", parts);
|
||||
_stepBtns[step].BackColor = SystemColors.Control;
|
||||
_stepBtns[step].ForeColor = SystemColors.ControlText;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCapture()
|
||||
{
|
||||
_capturing = true;
|
||||
@ -334,14 +587,14 @@ public class ActionDialog : Form
|
||||
|
||||
private void RefreshCaptureBtn()
|
||||
{
|
||||
if (_hidKeycode != 0 && s_hidNames.TryGetValue(_hidKeycode, out string? kn))
|
||||
if (_hidKeycode != 0)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (_chkCtrl.Checked) parts.Add("Strg");
|
||||
if (_chkShift.Checked) parts.Add("Shift");
|
||||
if (_chkAlt.Checked) parts.Add("Alt");
|
||||
if (_chkWin.Checked) parts.Add("Win");
|
||||
parts.Add(kn);
|
||||
parts.Add(HidKeyName(_hidKeycode));
|
||||
_captureBtn.Text = string.Join(" + ", parts);
|
||||
}
|
||||
else
|
||||
@ -353,41 +606,65 @@ public class ActionDialog : Form
|
||||
_captureBtn.FlatAppearance.BorderColor = Color.DarkGray;
|
||||
}
|
||||
|
||||
// Tasten werden form-weit abgefangen (KeyPreview = true)
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
// ProcessCmdKey wird vor jeder Dialog-Verarbeitung aufgerufen (Enter, Pfeiltasten,
|
||||
// Tab usw. werden normalerweise als "Dialog-Tasten" verschluckt, bevor OnKeyDown
|
||||
// feuert). Daher hier die Capture-Logik – so kommen auch ↑↓←→ und Enter an.
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
if (!_capturing) { base.OnKeyDown(e); return; }
|
||||
const int WM_KEYDOWN = 0x0100;
|
||||
const int WM_SYSKEYDOWN = 0x0104;
|
||||
if (msg.Msg != WM_KEYDOWN && msg.Msg != WM_SYSKEYDOWN)
|
||||
return base.ProcessCmdKey(ref msg, keyData);
|
||||
|
||||
if (!_capturing && _captureStep < 0)
|
||||
return base.ProcessCmdKey(ref msg, keyData);
|
||||
|
||||
Keys vk = keyData & Keys.KeyCode;
|
||||
bool ctrl = (keyData & Keys.Control) != 0;
|
||||
bool shift = (keyData & Keys.Shift) != 0;
|
||||
bool alt = (keyData & Keys.Alt) != 0;
|
||||
|
||||
// Reine Modifier-Tasten ignorieren – auf echte Taste warten
|
||||
if (e.KeyCode is Keys.ControlKey or Keys.LControlKey or Keys.RControlKey
|
||||
if (vk is Keys.ControlKey or Keys.LControlKey or Keys.RControlKey
|
||||
or Keys.ShiftKey or Keys.LShiftKey or Keys.RShiftKey
|
||||
or Keys.Menu or Keys.LMenu or Keys.RMenu
|
||||
or Keys.LWin or Keys.RWin or Keys.Apps)
|
||||
return true; // schlucken, aber Capture läuft weiter
|
||||
|
||||
// ── Makro-Step-Capture ────────────────────────────────────────────────
|
||||
if (_captureStep >= 0)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
int s = _captureStep;
|
||||
_captureStep = -1;
|
||||
if (vk == Keys.Escape)
|
||||
{
|
||||
_stepKeycodes[s] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_stepKeycodes[s] = VkToHid(vk);
|
||||
_stepCtrl[s].Checked = ctrl;
|
||||
_stepShift[s].Checked = shift;
|
||||
_stepAlt[s].Checked = alt;
|
||||
}
|
||||
RefreshStepBtn(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Escape bricht Erfassung ab (ohne Wert zu ändern)
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
// ── HID-Tastatur-Capture ──────────────────────────────────────────────
|
||||
if (vk == Keys.Escape)
|
||||
{
|
||||
StopCapture();
|
||||
e.Handled = true;
|
||||
e.SuppressKeyPress = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Modifikatoren übernehmen
|
||||
_chkCtrl.Checked = e.Control;
|
||||
_chkShift.Checked = e.Shift;
|
||||
_chkAlt.Checked = e.Alt;
|
||||
|
||||
// VK → HID-Code
|
||||
_hidKeycode = s_vkToHid.TryGetValue(e.KeyCode, out byte hid) ? hid : (byte)0;
|
||||
|
||||
else
|
||||
{
|
||||
_chkCtrl.Checked = ctrl;
|
||||
_chkShift.Checked = shift;
|
||||
_chkAlt.Checked = alt;
|
||||
_hidKeycode = VkToHid(vk);
|
||||
StopCapture();
|
||||
e.Handled = true;
|
||||
e.SuppressKeyPress = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Farbe ─────────────────────────────────────────────────────────────────
|
||||
@ -435,7 +712,30 @@ public class ActionDialog : Form
|
||||
break;
|
||||
}
|
||||
|
||||
if (type == ActionType.Macro)
|
||||
{
|
||||
// Steps in die Makro-Tabelle schreiben
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
byte mod = 0;
|
||||
if (_stepCtrl[i].Checked) mod |= 0x01;
|
||||
if (_stepShift[i].Checked) mod |= 0x02;
|
||||
if (_stepAlt[i].Checked) mod |= 0x04;
|
||||
_macros.Steps[_macroSlot, i].Keycode = _stepKeycodes[i];
|
||||
_macros.Steps[_macroSlot, i].Modifier = mod;
|
||||
}
|
||||
data = (ushort)_macroSlot;
|
||||
}
|
||||
|
||||
ResultAction = new DeviceAction { Type = type, Data = data };
|
||||
ResultColor = _color;
|
||||
|
||||
if (_animPanel.Visible && _animCombo.SelectedIndex >= 0)
|
||||
{
|
||||
ResultAnim = s_anims[_animCombo.SelectedIndex].Type;
|
||||
ResultPeriod = _periodCombo.SelectedIndex >= 0
|
||||
? s_periods[_periodCombo.SelectedIndex].Ms
|
||||
: (ushort)4000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,19 +20,17 @@ namespace VersaGUI;
|
||||
public class ConfigForm : Form
|
||||
{
|
||||
private readonly DeviceConfig _config;
|
||||
private readonly MacroTable _macros;
|
||||
private readonly SerialManager _serial;
|
||||
|
||||
// Grid-Buttons für die 20 MX-Buttons (Index = mx_idx = key_id - 5)
|
||||
private readonly Button[] _btnGrid = new Button[20];
|
||||
|
||||
// Encoder-Buttons [enc][SW/CW/CCW]
|
||||
private readonly Button[,] _encBtns = new Button[4, 3];
|
||||
|
||||
private Button _saveBtn = null!;
|
||||
|
||||
public ConfigForm(DeviceConfig config, SerialManager serial)
|
||||
public ConfigForm(DeviceConfig config, MacroTable macros, SerialManager serial)
|
||||
{
|
||||
_config = config;
|
||||
_macros = macros;
|
||||
_serial = serial;
|
||||
|
||||
Text = "VersaPad – Konfiguration";
|
||||
@ -182,6 +180,22 @@ public class ConfigForm : Form
|
||||
};
|
||||
pingBtn.Click += (_, _) => _serial.Send(new SerialPacket(Protocol.CmdPing));
|
||||
|
||||
var exportBtn = new Button
|
||||
{
|
||||
Text = "Exportieren",
|
||||
Location = new Point(256, startY),
|
||||
Size = new Size(80, 30),
|
||||
};
|
||||
exportBtn.Click += OnExport;
|
||||
|
||||
var importBtn = new Button
|
||||
{
|
||||
Text = "Importieren",
|
||||
Location = new Point(340, startY),
|
||||
Size = new Size(80, 30),
|
||||
};
|
||||
importBtn.Click += OnImport;
|
||||
|
||||
var closeBtn = new Button
|
||||
{
|
||||
Text = "Schließen",
|
||||
@ -190,7 +204,7 @@ public class ConfigForm : Form
|
||||
};
|
||||
closeBtn.Click += (_, _) => Close();
|
||||
|
||||
Controls.AddRange(new Control[] { _saveBtn, pingBtn, closeBtn });
|
||||
Controls.AddRange(new Control[] { _saveBtn, pingBtn, exportBtn, importBtn, closeBtn });
|
||||
|
||||
// Fensterhöhe anpassen
|
||||
ClientSize = new Size(ClientSize.Width, startY + 50);
|
||||
@ -202,15 +216,21 @@ public class ConfigForm : Form
|
||||
{
|
||||
if (sender is not Button btn || btn.Tag is not int mxIdx) return;
|
||||
|
||||
int slot = MacroTable.SlotForMx(mxIdx);
|
||||
using var dlg = new ActionDialog(
|
||||
_config.MxActions[mxIdx],
|
||||
_config.LedBase[mxIdx],
|
||||
_config.LedAnim[mxIdx],
|
||||
_config.LedPeriod[mxIdx],
|
||||
_macros, slot,
|
||||
showColor: true);
|
||||
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
|
||||
_config.MxActions[mxIdx] = dlg.ResultAction;
|
||||
_config.LedBase[mxIdx] = dlg.ResultColor;
|
||||
_config.LedAnim[mxIdx] = dlg.ResultAnim;
|
||||
_config.LedPeriod[mxIdx] = dlg.ResultPeriod;
|
||||
RefreshMxButton(mxIdx);
|
||||
}
|
||||
|
||||
@ -218,9 +238,12 @@ public class ConfigForm : Form
|
||||
{
|
||||
if (sender is not Button btn || btn.Tag is not (int enc, int act)) return;
|
||||
|
||||
int slot = MacroTable.SlotForEncoder(enc, act);
|
||||
using var dlg = new ActionDialog(
|
||||
_config.EncActions[enc, act],
|
||||
Color.Black,
|
||||
LedAnimType.Static, 0,
|
||||
_macros, slot,
|
||||
showColor: false);
|
||||
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
@ -234,15 +257,16 @@ public class ConfigForm : Form
|
||||
_saveBtn.Enabled = false;
|
||||
_saveBtn.Text = "Wird gesendet...";
|
||||
|
||||
// SendConfig blockiert ~180ms (Delays zwischen Paketen) → Background-Thread
|
||||
// SendConfig + SendMacros blockieren ~400ms → Background-Thread
|
||||
Task.Run(() =>
|
||||
{
|
||||
_serial.SendConfig(_config);
|
||||
Thread.Sleep(50); // kurze Pause zwischen Config- und Makro-Transfer
|
||||
_serial.SendMacros(_macros);
|
||||
InvokeOnUi(() =>
|
||||
{
|
||||
_saveBtn.Text = "Auf Board speichern";
|
||||
_saveBtn.Enabled = _serial.IsConnected;
|
||||
// Ergebnis kommt als ACK/NACK-Event vom Board → TrayApp zeigt Balloon
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -263,11 +287,28 @@ public class ConfigForm : Form
|
||||
var btn = _btnGrid[mxIdx];
|
||||
var action = _config.MxActions[mxIdx];
|
||||
var color = _config.LedBase[mxIdx];
|
||||
var anim = _config.LedAnim[mxIdx];
|
||||
|
||||
// Regenbogen: Hintergrund als Verlauf andeuten (dunkles Grau als Neutral)
|
||||
if (anim == LedAnimType.ColorCycle)
|
||||
{
|
||||
btn.BackColor = Color.FromArgb(40, 40, 40);
|
||||
btn.ForeColor = Color.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.BackColor = color;
|
||||
// Textfarbe kontrastreich zur Hintergrundfarbe wählen
|
||||
btn.ForeColor = Luminance(color) > 128 ? Color.Black : Color.White;
|
||||
btn.Text = action.Display;
|
||||
}
|
||||
|
||||
string animName = anim switch
|
||||
{
|
||||
LedAnimType.Blink => " [Blinken]",
|
||||
LedAnimType.Pulse => " [Pulsieren]",
|
||||
LedAnimType.ColorCycle => " [Regenbogen]",
|
||||
_ => "",
|
||||
};
|
||||
btn.Text = action.Display + animName;
|
||||
btn.ToolTipText(action.TypeDescription());
|
||||
}
|
||||
|
||||
@ -282,6 +323,53 @@ public class ConfigForm : Form
|
||||
private static int Luminance(Color c)
|
||||
=> (c.R * 299 + c.G * 587 + c.B * 114) / 1000;
|
||||
|
||||
// ── Import / Export ───────────────────────────────────────────────────────
|
||||
|
||||
private void OnExport(object? sender, EventArgs e)
|
||||
{
|
||||
using var dlg = new SaveFileDialog
|
||||
{
|
||||
Title = "Konfiguration exportieren",
|
||||
Filter = "VersaPad Config (*.json)|*.json",
|
||||
DefaultExt = "json",
|
||||
FileName = "versapad_config.json",
|
||||
};
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
|
||||
try
|
||||
{
|
||||
string json = ConfigJson.Serialize(_config);
|
||||
File.WriteAllText(dlg.FileName, json, System.Text.Encoding.UTF8);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Export fehlgeschlagen:\n{ex.Message}",
|
||||
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnImport(object? sender, EventArgs e)
|
||||
{
|
||||
using var dlg = new OpenFileDialog
|
||||
{
|
||||
Title = "Konfiguration importieren",
|
||||
Filter = "VersaPad Config (*.json)|*.json",
|
||||
};
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(dlg.FileName, System.Text.Encoding.UTF8);
|
||||
ConfigJson.Deserialize(json, _config);
|
||||
RefreshAll();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Import fehlgeschlagen:\n{ex.Message}",
|
||||
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokeOnUi(Action a)
|
||||
{
|
||||
if (InvokeRequired) Invoke(a);
|
||||
|
||||
161
src/ConfigJson.cs
Normal file
161
src/ConfigJson.cs
Normal file
@ -0,0 +1,161 @@
|
||||
// ConfigJson.cs
|
||||
// JSON-Import/Export für DeviceConfig.
|
||||
//
|
||||
// Format (menschenlesbar, kommentiert mit Feldnamen):
|
||||
// {
|
||||
// "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 } },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Drawing;
|
||||
|
||||
namespace VersaGUI;
|
||||
|
||||
internal static class ConfigJson
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_opts = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
public static string Serialize(DeviceConfig cfg)
|
||||
{
|
||||
var doc = new ConfigJsonDoc
|
||||
{
|
||||
Version = DeviceConfig.Version,
|
||||
Buttons = new ButtonEntry[20],
|
||||
Encoders = new EncoderEntry[4],
|
||||
};
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
doc.Buttons[i] = new ButtonEntry
|
||||
{
|
||||
Index = i,
|
||||
Action = ActionToJson(cfg.MxActions[i]),
|
||||
Led = new LedEntry
|
||||
{
|
||||
R = cfg.LedBase[i].R,
|
||||
G = cfg.LedBase[i].G,
|
||||
B = cfg.LedBase[i].B,
|
||||
Anim = cfg.LedAnim[i],
|
||||
PeriodMs = cfg.LedPeriod[i],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
for (int e = 0; e < 4; e++)
|
||||
{
|
||||
doc.Encoders[e] = new EncoderEntry
|
||||
{
|
||||
Index = e,
|
||||
Sw = ActionToJson(cfg.EncActions[e, DeviceConfig.EncSw]),
|
||||
Cw = ActionToJson(cfg.EncActions[e, DeviceConfig.EncCw]),
|
||||
Ccw = ActionToJson(cfg.EncActions[e, DeviceConfig.EncCcw]),
|
||||
};
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(doc, s_opts);
|
||||
}
|
||||
|
||||
public static void Deserialize(string json, DeviceConfig cfg)
|
||||
{
|
||||
var doc = JsonSerializer.Deserialize<ConfigJsonDoc>(json, s_opts)
|
||||
?? throw new InvalidDataException("Ungültiges JSON-Format.");
|
||||
|
||||
if (doc.Version != DeviceConfig.Version)
|
||||
throw new InvalidDataException(
|
||||
$"Config-Version {doc.Version} wird nicht unterstützt (erwartet {DeviceConfig.Version}).");
|
||||
|
||||
if (doc.Buttons != null)
|
||||
{
|
||||
foreach (var b in doc.Buttons)
|
||||
{
|
||||
if (b.Index < 0 || b.Index >= 20) continue;
|
||||
ActionFromJson(b.Action, cfg.MxActions[b.Index]);
|
||||
if (b.Led != null)
|
||||
{
|
||||
cfg.LedBase[b.Index] = Color.FromArgb(b.Led.R, b.Led.G, b.Led.B);
|
||||
cfg.LedAnim[b.Index] = b.Led.Anim;
|
||||
cfg.LedPeriod[b.Index] = b.Led.PeriodMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.Encoders != null)
|
||||
{
|
||||
foreach (var enc in doc.Encoders)
|
||||
{
|
||||
if (enc.Index < 0 || enc.Index >= 4) continue;
|
||||
ActionFromJson(enc.Sw, cfg.EncActions[enc.Index, DeviceConfig.EncSw]);
|
||||
ActionFromJson(enc.Cw, cfg.EncActions[enc.Index, DeviceConfig.EncCw]);
|
||||
ActionFromJson(enc.Ccw, cfg.EncActions[enc.Index, DeviceConfig.EncCcw]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static ActionEntry ActionToJson(DeviceAction a)
|
||||
=> new() { Type = a.Type, Data = a.Data };
|
||||
|
||||
private static void ActionFromJson(ActionEntry? src, DeviceAction dst)
|
||||
{
|
||||
if (src == null) return;
|
||||
dst.Type = src.Type;
|
||||
dst.Data = src.Data;
|
||||
}
|
||||
|
||||
// ── JSON-Datenklassen ─────────────────────────────────────────────────────
|
||||
|
||||
private class ConfigJsonDoc
|
||||
{
|
||||
[JsonPropertyName("version")] public byte Version { get; set; }
|
||||
[JsonPropertyName("buttons")] public ButtonEntry[]? Buttons { get; set; }
|
||||
[JsonPropertyName("encoders")] public EncoderEntry[]? Encoders { get; set; }
|
||||
}
|
||||
|
||||
private class ButtonEntry
|
||||
{
|
||||
[JsonPropertyName("index")] public int Index { get; set; }
|
||||
[JsonPropertyName("action")] public ActionEntry? Action { get; set; }
|
||||
[JsonPropertyName("led")] public LedEntry? Led { get; set; }
|
||||
}
|
||||
|
||||
private class EncoderEntry
|
||||
{
|
||||
[JsonPropertyName("index")] public int Index { get; set; }
|
||||
[JsonPropertyName("sw")] public ActionEntry? Sw { get; set; }
|
||||
[JsonPropertyName("cw")] public ActionEntry? Cw { get; set; }
|
||||
[JsonPropertyName("ccw")] public ActionEntry? Ccw { get; set; }
|
||||
}
|
||||
|
||||
private class ActionEntry
|
||||
{
|
||||
[JsonPropertyName("type")] public ActionType Type { get; set; }
|
||||
[JsonPropertyName("data")] public ushort Data { get; set; }
|
||||
}
|
||||
|
||||
private class LedEntry
|
||||
{
|
||||
[JsonPropertyName("r")] public byte R { get; set; }
|
||||
[JsonPropertyName("g")] public byte G { get; set; }
|
||||
[JsonPropertyName("b")] public byte B { get; set; }
|
||||
[JsonPropertyName("anim")] public LedAnimType Anim { get; set; }
|
||||
[JsonPropertyName("period_ms")] public ushort PeriodMs { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,17 @@
|
||||
// DeviceConfig.cs
|
||||
// C#-Spiegel der SDeviceConfig-Struktur aus der Firmware (nvm_config.h).
|
||||
//
|
||||
// Serialisiertes Layout (packed, 163 Bytes):
|
||||
// Serialisiertes Layout (packed, 223 Bytes):
|
||||
// Offset 0 4B Magic 0x56503202
|
||||
// Offset 4 1B Version 1
|
||||
// Offset 5 2B CRC16 (CCITT über Bytes 7–162)
|
||||
// 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)
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
@ -21,6 +23,73 @@ public enum ActionType : byte
|
||||
HidKey = 1, // data = Keycode (Low-Byte) + Modifier (High-Byte)
|
||||
HidConsumer = 2, // data = HID Consumer Usage ID
|
||||
HostCommand = 3, // data = Command-ID → App führt aus
|
||||
Macro = 4, // data = Makro-Slot-Index (0–31)
|
||||
}
|
||||
|
||||
// Ein Step in einem Makro (keycode=0 → leerer/letzter Step)
|
||||
public class MacroStep
|
||||
{
|
||||
public byte Keycode { get; set; } = 0;
|
||||
public byte Modifier { get; set; } = 0;
|
||||
|
||||
public bool IsEmpty => Keycode == 0;
|
||||
public MacroStep Clone() => new() { Keycode = Keycode, Modifier = Modifier };
|
||||
}
|
||||
|
||||
// Makro-Tabelle: 32 Slots × 4 Steps (spiegelt SMacroTable aus macro_config.h)
|
||||
public class MacroTable
|
||||
{
|
||||
public const int Slots = 32;
|
||||
public const int MaxSteps = 4;
|
||||
|
||||
// [slot][step]
|
||||
public MacroStep[,] Steps { get; } = new MacroStep[Slots, MaxSteps];
|
||||
|
||||
public MacroTable()
|
||||
{
|
||||
for (int s = 0; s < Slots; s++)
|
||||
for (int i = 0; i < MaxSteps; i++)
|
||||
Steps[s, i] = new MacroStep();
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Serialisierung: 32 × 4 × 2 = 256 Bytes
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var buf = new byte[256];
|
||||
int pos = 0;
|
||||
for (int s = 0; s < Slots; s++)
|
||||
for (int i = 0; i < MaxSteps; i++)
|
||||
{
|
||||
buf[pos++] = Steps[s, i].Keycode;
|
||||
buf[pos++] = Steps[s, i].Modifier;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public void FromBytes(byte[] buf)
|
||||
{
|
||||
if (buf.Length < 256) return;
|
||||
int pos = 0;
|
||||
for (int s = 0; s < Slots; s++)
|
||||
for (int i = 0; i < MaxSteps; i++)
|
||||
{
|
||||
Steps[s, i].Keycode = buf[pos++];
|
||||
Steps[s, i].Modifier = buf[pos++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spiegelt LEDAnim aus CButton.h – nur die in der GUI konfigurierbaren Werte
|
||||
public enum LedAnimType : byte
|
||||
{
|
||||
Static = 0,
|
||||
Blink = 1,
|
||||
Pulse = 2,
|
||||
ColorCycle = 5, // Regenbogen (entspricht LEDAnim::COLOR_CYCLE in Firmware)
|
||||
}
|
||||
|
||||
public class DeviceAction
|
||||
@ -28,7 +97,6 @@ public class DeviceAction
|
||||
public ActionType Type { get; set; } = ActionType.None;
|
||||
public ushort Data { get; set; } = 0;
|
||||
|
||||
// Kurzanzeige für Buttons und Tooltips
|
||||
public string Display
|
||||
{
|
||||
get
|
||||
@ -60,6 +128,8 @@ public class DeviceAction
|
||||
_ => $"C:{Data:X3}",
|
||||
};
|
||||
}
|
||||
if (Type == ActionType.Macro)
|
||||
return $"Makro {Data}";
|
||||
return $"CMD {Data}";
|
||||
}
|
||||
}
|
||||
@ -85,24 +155,29 @@ public class DeviceAction
|
||||
public class DeviceConfig
|
||||
{
|
||||
public const uint Magic = 0x56503202;
|
||||
public const byte Version = 1;
|
||||
public const byte Version = 2;
|
||||
|
||||
// Encoder-Aktions-Indizes (Spalte in enc_actions[enc, idx])
|
||||
public const int EncSw = 0;
|
||||
public const int EncCw = 1;
|
||||
public const int EncCcw = 2;
|
||||
|
||||
// 20 MX-Button-Aktionen (key_id 5–24, mx_idx = key_id - 5)
|
||||
public DeviceAction[] MxActions { get; } =
|
||||
Enumerable.Range(0, 20).Select(_ => new DeviceAction()).ToArray();
|
||||
|
||||
// 4 Encoder × 3 Aktionen [enc][SW/CW/CCW]
|
||||
public DeviceAction[,] EncActions { get; } = InitEncActions();
|
||||
|
||||
// Base-LED-Farben für die 20 MX-Buttons (Default: warm-weiß wie Firmware)
|
||||
// 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();
|
||||
|
||||
private static DeviceAction[,] InitEncActions()
|
||||
{
|
||||
var a = new DeviceAction[4, 3];
|
||||
@ -114,36 +189,36 @@ public class DeviceConfig
|
||||
|
||||
// ── Serialisierung ────────────────────────────────────────────────────────
|
||||
|
||||
// Erzeugt den 163-Byte-Puffer der 1:1 SDeviceConfig entspricht.
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
const int size = 163;
|
||||
const int size = 223;
|
||||
var buf = new byte[size];
|
||||
int pos = 0;
|
||||
|
||||
// Magic (little-endian)
|
||||
WriteU32(buf, ref pos, Magic);
|
||||
// Version
|
||||
buf[pos++] = Version;
|
||||
// CRC-Platzhalter (wird am Ende eingetragen)
|
||||
int crcOffset = pos;
|
||||
pos += 2;
|
||||
|
||||
// mx_actions[20]
|
||||
foreach (var a in MxActions)
|
||||
WriteAction(buf, ref pos, a);
|
||||
|
||||
// enc_actions[4][3]
|
||||
for (int e = 0; e < 4; e++)
|
||||
for (int i = 0; i < 3; i++)
|
||||
WriteAction(buf, ref pos, EncActions[e, i]);
|
||||
|
||||
// led_r / led_g / led_b
|
||||
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;
|
||||
|
||||
// CRC über alles ab mx_actions (Offset 7)
|
||||
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);
|
||||
@ -151,18 +226,16 @@ public class DeviceConfig
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Lädt Config aus einem 163-Byte-Puffer (empfangen vom Board).
|
||||
// Gibt false zurück wenn Magic/Version/CRC ungültig.
|
||||
public bool FromBytes(byte[] buf)
|
||||
{
|
||||
if (buf.Length < 163) return false;
|
||||
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, 163 - 7);
|
||||
ushort computedCrc = Crc16(buf, 7, 223 - 7);
|
||||
if (storedCrc != computedCrc) return false;
|
||||
|
||||
int pos = 7;
|
||||
@ -172,6 +245,15 @@ public class DeviceConfig
|
||||
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++) 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;
|
||||
}
|
||||
@ -203,7 +285,6 @@ public class DeviceConfig
|
||||
private static uint ReadU32(byte[] buf, int pos)
|
||||
=> (uint)(buf[pos] | (buf[pos+1] << 8) | (buf[pos+2] << 16) | (buf[pos+3] << 24));
|
||||
|
||||
// CRC16-CCITT (Poly 0x1021, Init 0xFFFF) – identisch zur Firmware
|
||||
public static ushort Crc16(byte[] data, int offset, int length)
|
||||
{
|
||||
ushort crc = 0xFFFF;
|
||||
|
||||
@ -31,6 +31,10 @@ public static class Protocol
|
||||
public const byte CmdConfigData = 0x11;
|
||||
public const byte CmdConfigCommit = 0x12;
|
||||
public const byte CmdConfigRead = 0x13; // Board sendet aktuelle NVM-Config zurück
|
||||
public const byte CmdMacroBegin = 0x20; // Makro-Tabelle übertragen: Data[1] = Chunks
|
||||
public const byte CmdMacroData = 0x21; // Makro-Chunk: Data[1] = Index, Data[2..7] = 6B
|
||||
public const byte CmdMacroCommit = 0x22; // NVM schreiben
|
||||
public const byte CmdMacroRead = 0x23; // Board sendet aktuelle Makro-Tabelle zurück
|
||||
|
||||
// ── Events: Board → App (0x81–0xFF) ──────────────────────────────────────
|
||||
public const byte EvtKeyDown = 0x81; // key_id → HOST_COMMAND-Button gedrückt
|
||||
@ -43,6 +47,10 @@ public static class Protocol
|
||||
public const byte EvtConfigBegin = 0x92; // Beginn Config-Dump (Data[1] = Chunks)
|
||||
public const byte EvtConfigData = 0x93; // Config-Chunk (Data[1] = Index, Data[2..7] = 6B)
|
||||
public const byte EvtConfigEnd = 0x94; // Config-Dump vollständig
|
||||
public const byte EvtMacroAck = 0x95; // Makro-Tabelle erfolgreich gespeichert
|
||||
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
|
||||
}
|
||||
|
||||
// Ein 8-Byte-Paket mit benannten Accessoren.
|
||||
|
||||
@ -217,6 +217,36 @@ public class SerialManager : IDisposable
|
||||
public void RequestConfig()
|
||||
=> Send(new SerialPacket(Protocol.CmdConfigRead));
|
||||
|
||||
// Makro-Tabelle vom Board anfordern
|
||||
public void RequestMacros()
|
||||
=> Send(new SerialPacket(Protocol.CmdMacroRead));
|
||||
|
||||
// Makro-Tabelle (256 Bytes) in 6-Byte-Chunks senden.
|
||||
public void SendMacros(MacroTable macros)
|
||||
{
|
||||
byte[] data = macros.ToBytes();
|
||||
const int payload = 6;
|
||||
int chunks = (data.Length + payload - 1) / payload; // 43
|
||||
|
||||
Send(new SerialPacket(Protocol.CmdMacroBegin, (byte)chunks));
|
||||
Thread.Sleep(10);
|
||||
|
||||
for (int i = 0; i < chunks; i++)
|
||||
{
|
||||
var pkt = new SerialPacket();
|
||||
pkt.Data[0] = Protocol.CmdMacroData;
|
||||
pkt.Data[1] = (byte)i;
|
||||
int offset = i * payload;
|
||||
int count = Math.Min(payload, data.Length - offset);
|
||||
Buffer.BlockCopy(data, offset, pkt.Data, 2, count);
|
||||
Send(pkt);
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
|
||||
Thread.Sleep(10);
|
||||
Send(new SerialPacket(Protocol.CmdMacroCommit));
|
||||
}
|
||||
|
||||
// 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).
|
||||
|
||||
@ -20,12 +20,14 @@ public class TrayApp : ApplicationContext
|
||||
private readonly NotifyIcon _tray;
|
||||
private readonly SerialManager _serial;
|
||||
private readonly DeviceConfig _config = new();
|
||||
private readonly MacroTable _macros = new();
|
||||
|
||||
private ToolStripMenuItem _statusItem = null!;
|
||||
private ConfigForm? _configForm;
|
||||
|
||||
// Empfangspuffer für eingehenden Config-Dump vom Board
|
||||
// Empfangspuffer für eingehende Dumps vom Board
|
||||
private byte[]? _rxConfigBuf;
|
||||
private byte[]? _rxMacroBuf;
|
||||
|
||||
public TrayApp()
|
||||
{
|
||||
@ -78,7 +80,7 @@ public class TrayApp : ApplicationContext
|
||||
_configForm.BringToFront();
|
||||
return;
|
||||
}
|
||||
_configForm = new ConfigForm(_config, _serial);
|
||||
_configForm = new ConfigForm(_config, _macros, _serial);
|
||||
_configForm.Show();
|
||||
}
|
||||
|
||||
@ -90,8 +92,9 @@ public class TrayApp : ApplicationContext
|
||||
_tray.Icon = SystemIcons.Information;
|
||||
_statusItem.Text = "● Verbunden";
|
||||
|
||||
// Config sofort vom Board laden
|
||||
// Config und Makro-Tabelle vom Board laden
|
||||
_serial.RequestConfig();
|
||||
_serial.RequestMacros();
|
||||
}
|
||||
|
||||
private void OnDisconnected(object? sender, EventArgs e)
|
||||
@ -136,7 +139,7 @@ public class TrayApp : ApplicationContext
|
||||
|
||||
// ── Config-Dump vom Board ─────────────────────────────────────────
|
||||
case Protocol.EvtConfigBegin:
|
||||
_rxConfigBuf = new byte[163];
|
||||
_rxConfigBuf = new byte[223]; // sizeof(SDeviceConfig) = 223 (Version 2)
|
||||
break;
|
||||
|
||||
case Protocol.EvtConfigData:
|
||||
@ -154,7 +157,35 @@ public class TrayApp : ApplicationContext
|
||||
{
|
||||
_config.FromBytes(_rxConfigBuf);
|
||||
_rxConfigBuf = null;
|
||||
_configForm?.RefreshAll(); // offenes Fenster sofort aktualisieren
|
||||
_configForm?.RefreshAll();
|
||||
}
|
||||
break;
|
||||
|
||||
// ── Makro-Dump vom Board ──────────────────────────────────────────
|
||||
case Protocol.EvtMacroAck:
|
||||
MessageBox.Show("Makros erfolgreich gespeichert!",
|
||||
"VersaPad", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroBegin:
|
||||
_rxMacroBuf = new byte[256];
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroData:
|
||||
if (_rxMacroBuf != null)
|
||||
{
|
||||
int offset = pkt.KeyId * 6;
|
||||
for (int i = 0; i < 6; i++)
|
||||
if (offset + i < _rxMacroBuf.Length)
|
||||
_rxMacroBuf[offset + i] = pkt.Data[2 + i];
|
||||
}
|
||||
break;
|
||||
|
||||
case Protocol.EvtMacroEnd:
|
||||
if (_rxMacroBuf != null)
|
||||
{
|
||||
_macros.FromBytes(_rxMacroBuf);
|
||||
_rxMacroBuf = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user