Added Macro functionality, udpated readme
This commit is contained in:
parent
747bec985d
commit
2eb43826ce
124
README.md
124
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
|
4. **Auf Board speichern** – überträgt Config in den NVM des Boards
|
||||||
5. Beim nächsten Verbinden wird die Config automatisch vom Board geladen
|
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
|
## Projekt-Struktur
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -44,10 +135,11 @@ VersaGUI/
|
|||||||
├── Program.cs – Einstiegspunkt, startet TrayApp
|
├── Program.cs – Einstiegspunkt, startet TrayApp
|
||||||
├── TrayApp.cs – ApplicationContext: Tray-Icon, Menü, Paket-Routing
|
├── TrayApp.cs – ApplicationContext: Tray-Icon, Menü, Paket-Routing
|
||||||
├── ConfigForm.cs – Konfigurations-Fenster (4×5 Grid + Encoder-Panel)
|
├── ConfigForm.cs – Konfigurations-Fenster (4×5 Grid + Encoder-Panel)
|
||||||
├── ActionDialog.cs – Dialog: Taste erfassen / Consumer auswählen / Host-Command
|
├── ActionDialog.cs – Dialog: Taste erfassen / Consumer / Makro / LED-Animation
|
||||||
├── DeviceConfig.cs – C#-Spiegel von SDeviceConfig (163B Serialisierung, CRC16)
|
├── 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)
|
├── 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
|
## Architektur
|
||||||
@ -71,22 +163,32 @@ TrayApp
|
|||||||
|
|
||||||
### DeviceConfig / Serialisierung
|
### 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):
|
Config-Übertragung Board→PC (Lesen):
|
||||||
```
|
```
|
||||||
PC sendet: CMD_CONFIG_READ (0x13)
|
PC sendet: CMD_CONFIG_READ (0x13)
|
||||||
Board sendet: EVT_CONFIG_BEGIN (Chunk-Anzahl)
|
Board sendet: EVT_CONFIG_BEGIN (Chunk-Anzahl = 38)
|
||||||
EVT_CONFIG_DATA × 28 (je 6 Nutzbytes)
|
EVT_CONFIG_DATA × 38 (je 6 Nutzbytes)
|
||||||
EVT_CONFIG_END
|
EVT_CONFIG_END
|
||||||
```
|
```
|
||||||
|
|
||||||
Config-Übertragung PC→Board (Schreiben):
|
Config-Übertragung PC→Board (Schreiben):
|
||||||
```
|
```
|
||||||
PC sendet: CMD_CONFIG_BEGIN (Chunk-Anzahl)
|
PC sendet: CMD_CONFIG_BEGIN (0x10, Chunk-Anzahl)
|
||||||
CMD_CONFIG_DATA × 28
|
CMD_CONFIG_DATA × 38
|
||||||
CMD_CONFIG_COMMIT
|
CMD_CONFIG_COMMIT (0x12)
|
||||||
Board sendet: EVT_CONFIG_ACK oder EVT_CONFIG_NACK
|
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
|
### Bekannte .NET-Eigenheiten
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
// Host Command → Zahlen-Eingabe (Command-ID)
|
// Host Command → Zahlen-Eingabe (Command-ID)
|
||||||
// Keine Aktion → nichts
|
// Keine Aktion → nichts
|
||||||
//
|
//
|
||||||
// Optional (nur MX-Buttons): LED-Basisfarbe
|
// Optional (nur MX-Buttons): LED-Basisfarbe + LED-Animation + Geschwindigkeit
|
||||||
|
|
||||||
namespace VersaGUI;
|
namespace VersaGUI;
|
||||||
|
|
||||||
@ -16,6 +16,8 @@ public class ActionDialog : Form
|
|||||||
// ── Ergebnis ──────────────────────────────────────────────────────────────
|
// ── Ergebnis ──────────────────────────────────────────────────────────────
|
||||||
public DeviceAction ResultAction { get; private set; }
|
public DeviceAction ResultAction { get; private set; }
|
||||||
public Color ResultColor { get; private set; }
|
public Color ResultColor { get; private set; }
|
||||||
|
public LedAnimType ResultAnim { get; private set; }
|
||||||
|
public ushort ResultPeriod { get; private set; }
|
||||||
|
|
||||||
// ── Controls ──────────────────────────────────────────────────────────────
|
// ── Controls ──────────────────────────────────────────────────────────────
|
||||||
private readonly ComboBox _typeCombo;
|
private readonly ComboBox _typeCombo;
|
||||||
@ -38,86 +40,132 @@ public class ActionDialog : Form
|
|||||||
private readonly Button _colorBtn;
|
private readonly Button _colorBtn;
|
||||||
private Color _color;
|
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
|
// Capture-Zustand
|
||||||
private bool _capturing;
|
private bool _capturing;
|
||||||
private byte _hidKeycode; // 0 = nicht belegt
|
private byte _hidKeycode; // 0 = nicht belegt
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
private readonly bool _showColor;
|
||||||
|
private Button _okBtn = null!;
|
||||||
|
private Button _cancelBtn = null!;
|
||||||
|
|
||||||
// ── Lookup-Tabellen ───────────────────────────────────────────────────────
|
// ── Lookup-Tabellen ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Windows VK → HID Keyboard Usage (USB HID Usage Table 0x07)
|
// P/Invoke: Windows-API für layout-unabhängige Scan-Code-Konvertierung
|
||||||
private static readonly Dictionary<Keys, byte> s_vkToHid = new()
|
[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
|
[0x01]=0x29, // Escape
|
||||||
[Keys.A]=0x04,[Keys.B]=0x05,[Keys.C]=0x06,[Keys.D]=0x07,
|
[0x02]=0x1E,[0x03]=0x1F,[0x04]=0x20,[0x05]=0x21,[0x06]=0x22,
|
||||||
[Keys.E]=0x08,[Keys.F]=0x09,[Keys.G]=0x0A,[Keys.H]=0x0B,
|
[0x07]=0x23,[0x08]=0x24,[0x09]=0x25,[0x0A]=0x26,[0x0B]=0x27, // 1–0
|
||||||
[Keys.I]=0x0C,[Keys.J]=0x0D,[Keys.K]=0x0E,[Keys.L]=0x0F,
|
[0x0C]=0x2D,[0x0D]=0x2E, // -/ß =/´
|
||||||
[Keys.M]=0x10,[Keys.N]=0x11,[Keys.O]=0x12,[Keys.P]=0x13,
|
[0x0E]=0x2A, // Backspace
|
||||||
[Keys.Q]=0x14,[Keys.R]=0x15,[Keys.S]=0x16,[Keys.T]=0x17,
|
[0x0F]=0x2B, // Tab
|
||||||
[Keys.U]=0x18,[Keys.V]=0x19,[Keys.W]=0x1A,[Keys.X]=0x1B,
|
[0x10]=0x14,[0x11]=0x1A,[0x12]=0x08,[0x13]=0x15,[0x14]=0x17, // Q W E R T
|
||||||
[Keys.Y]=0x1C,[Keys.Z]=0x1D,
|
[0x15]=0x1C,[0x16]=0x18,[0x17]=0x0C,[0x18]=0x12,[0x19]=0x13, // Z/Y U I O P
|
||||||
// Ziffernreihe
|
[0x1A]=0x2F,[0x1B]=0x30, // Ü +
|
||||||
[Keys.D1]=0x1E,[Keys.D2]=0x1F,[Keys.D3]=0x20,[Keys.D4]=0x21,
|
[0x1C]=0x28, // Enter
|
||||||
[Keys.D5]=0x22,[Keys.D6]=0x23,[Keys.D7]=0x24,[Keys.D8]=0x25,
|
[0x1E]=0x04,[0x1F]=0x16,[0x20]=0x07,[0x21]=0x09,[0x22]=0x0A, // A S D F G
|
||||||
[Keys.D9]=0x26,[Keys.D0]=0x27,
|
[0x23]=0x0B,[0x24]=0x0D,[0x25]=0x0E,[0x26]=0x0F, // H J K L
|
||||||
// Steuerung
|
[0x27]=0x33,[0x28]=0x34,[0x29]=0x35, // Ö Ä ^
|
||||||
[Keys.Return]=0x28,[Keys.Escape]=0x29,[Keys.Back]=0x2A,
|
[0x2B]=0x31, // #
|
||||||
[Keys.Tab]=0x2B, [Keys.Space]=0x2C, [Keys.Delete]=0x4C,
|
[0x2C]=0x1D,[0x2D]=0x1B,[0x2E]=0x06,[0x2F]=0x19,[0x30]=0x05, // Y/Z X C V B
|
||||||
[Keys.Insert]=0x49,[Keys.Home]=0x4A, [Keys.PageUp]=0x4B,
|
[0x31]=0x11,[0x32]=0x10,[0x33]=0x36,[0x34]=0x37,[0x35]=0x38, // N M , . -
|
||||||
[Keys.End]=0x4D, [Keys.PageDown]=0x4E,
|
[0x37]=0x55, // Numpad *
|
||||||
[Keys.Right]=0x4F,[Keys.Left]=0x50, [Keys.Down]=0x51,[Keys.Up]=0x52,
|
[0x39]=0x2C, // Space
|
||||||
[Keys.CapsLock]=0x39,
|
[0x3A]=0x39, // CapsLock
|
||||||
[Keys.PrintScreen]=0x46,[Keys.Scroll]=0x47,[Keys.Pause]=0x48,
|
[0x3B]=0x3A,[0x3C]=0x3B,[0x3D]=0x3C,[0x3E]=0x3D,[0x3F]=0x3E, // F1–F5
|
||||||
[Keys.NumLock]=0x53,
|
[0x40]=0x3F,[0x41]=0x40,[0x42]=0x41,[0x43]=0x42,[0x44]=0x43, // F6–F10
|
||||||
// F-Tasten
|
[0x45]=0x53,[0x46]=0x47, // NumLock ScrollLock
|
||||||
[Keys.F1]=0x3A,[Keys.F2]=0x3B,[Keys.F3]=0x3C,[Keys.F4]=0x3D,
|
[0x47]=0x5F,[0x48]=0x60,[0x49]=0x61,[0x4A]=0x56, // Num7 8 9 -
|
||||||
[Keys.F5]=0x3E,[Keys.F6]=0x3F,[Keys.F7]=0x40,[Keys.F8]=0x41,
|
[0x4B]=0x5C,[0x4C]=0x5D,[0x4D]=0x5E,[0x4E]=0x57, // Num4 5 6 +
|
||||||
[Keys.F9]=0x42,[Keys.F10]=0x43,[Keys.F11]=0x44,[Keys.F12]=0x45,
|
[0x4F]=0x59,[0x50]=0x5A,[0x51]=0x5B,[0x52]=0x62,[0x53]=0x63, // Num1 2 3 0 .
|
||||||
// Sonderzeichen (QWERTZ-kompatibel, basiert auf US-HID-Positionen)
|
[0x56]=0x64, // Non-US \ (< > auf QWERTZ)
|
||||||
[Keys.OemMinus]=0x2D, [Keys.Oemplus]=0x2E,
|
[0x57]=0x44,[0x58]=0x45, // F11 F12
|
||||||
[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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// HID-Code → lesbarer Name (für Capture-Button-Beschriftung)
|
// HID → Scan-Code (umgekehrte Tabelle, für Anzeigenamen)
|
||||||
private static readonly Dictionary<byte, string> s_hidNames = new()
|
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",
|
[Keys.Up]=0x52, [Keys.Down]=0x51, [Keys.Left]=0x50,
|
||||||
[0x0A]="G",[0x0B]="H",[0x0C]="I",[0x0D]="J",[0x0E]="K",[0x0F]="L",
|
[Keys.Right]=0x4F, [Keys.Home]=0x4A, [Keys.End]=0x4D,
|
||||||
[0x10]="M",[0x11]="N",[0x12]="O",[0x13]="P",[0x14]="Q",[0x15]="R",
|
[Keys.PageUp]=0x4B, [Keys.PageDown]=0x4E, [Keys.Insert]=0x49,
|
||||||
[0x16]="S",[0x17]="T",[0x18]="U",[0x19]="V",[0x1A]="W",[0x1B]="X",
|
[Keys.Delete]=0x4C, [Keys.Pause]=0x48, [Keys.Scroll]=0x47,
|
||||||
[0x1C]="Y",[0x1D]="Z",
|
[Keys.PrintScreen]=0x46,
|
||||||
[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+",
|
|
||||||
[0x59]="Num1", [0x5A]="Num2", [0x5B]="Num3", [0x5C]="Num4",
|
|
||||||
[0x5D]="Num5", [0x5E]="Num6", [0x5F]="Num7", [0x60]="Num8",
|
|
||||||
[0x61]="Num9", [0x62]="Num0", [0x63]="Num.",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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)
|
// HID Consumer Usage IDs (Usage Page 0x0C)
|
||||||
private static readonly (ushort Usage, string Name)[] s_consumer =
|
private static readonly (ushort Usage, string Name)[] s_consumer =
|
||||||
{
|
{
|
||||||
@ -137,11 +185,43 @@ public class ActionDialog : Form
|
|||||||
|
|
||||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
// ── 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();
|
ResultAction = action.Clone();
|
||||||
ResultColor = ledColor;
|
ResultColor = ledColor;
|
||||||
|
ResultAnim = ledAnim;
|
||||||
|
ResultPeriod = ledPeriod;
|
||||||
_color = ledColor;
|
_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
|
// Formular-Grundeinstellungen
|
||||||
Text = "Aktion bearbeiten";
|
Text = "Aktion bearbeiten";
|
||||||
@ -156,20 +236,20 @@ public class ActionDialog : Form
|
|||||||
_typeCombo = new ComboBox
|
_typeCombo = new ComboBox
|
||||||
{
|
{
|
||||||
Location = new Point(80, 12),
|
Location = new Point(80, 12),
|
||||||
Width = 208,
|
Width = 316,
|
||||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||||
};
|
};
|
||||||
_typeCombo.Items.AddRange(new object[]
|
_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.SelectedIndex = (int)action.Type;
|
||||||
_typeCombo.SelectedIndexChanged += OnTypeChanged;
|
_typeCombo.SelectedIndexChanged += OnTypeChanged;
|
||||||
|
|
||||||
// ── HID-Tastatur-Panel ────────────────────────────────────────────────
|
// ── 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
|
_captureBtn = new Button
|
||||||
{
|
{
|
||||||
Size = new Size(280, 48),
|
Size = new Size(396, 48),
|
||||||
Location = new Point(0, 0),
|
Location = new Point(0, 0),
|
||||||
FlatStyle = FlatStyle.Flat,
|
FlatStyle = FlatStyle.Flat,
|
||||||
Font = new Font(Font.FontFamily, 12, FontStyle.Regular),
|
Font = new Font(Font.FontFamily, 12, FontStyle.Regular),
|
||||||
@ -213,11 +293,11 @@ public class ActionDialog : Form
|
|||||||
RefreshCaptureBtn(); // erst nach Checkbox-Initialisierung aufrufen
|
RefreshCaptureBtn(); // erst nach Checkbox-Initialisierung aufrufen
|
||||||
|
|
||||||
// ── Consumer-Panel ────────────────────────────────────────────────────
|
// ── 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
|
_consumerCombo = new ComboBox
|
||||||
{
|
{
|
||||||
Location = new Point(0, 0),
|
Location = new Point(0, 0),
|
||||||
Width = 280,
|
Width = 396,
|
||||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||||
};
|
};
|
||||||
foreach (var (_, name) in s_consumer)
|
foreach (var (_, name) in s_consumer)
|
||||||
@ -230,12 +310,12 @@ public class ActionDialog : Form
|
|||||||
_consumerPanel.Controls.Add(_consumerCombo);
|
_consumerPanel.Controls.Add(_consumerCombo);
|
||||||
|
|
||||||
// ── Host-Command-Panel ────────────────────────────────────────────────
|
// ── 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 };
|
var cmdLabel = new Label { Text = "Command-ID:", Location = new Point(0, 8), AutoSize = true };
|
||||||
_cmdBox = new TextBox
|
_cmdBox = new TextBox
|
||||||
{
|
{
|
||||||
Location = new Point(90, 4),
|
Location = new Point(90, 4),
|
||||||
Width = 190,
|
Width = 306,
|
||||||
Text = action.Type == ActionType.HostCommand ? $"{action.Data}" : "0",
|
Text = action.Type == ActionType.HostCommand ? $"{action.Data}" : "0",
|
||||||
};
|
};
|
||||||
_cmdPanel.Controls.AddRange(new Control[] { cmdLabel, _cmdBox });
|
_cmdPanel.Controls.AddRange(new Control[] { cmdLabel, _cmdBox });
|
||||||
@ -244,14 +324,14 @@ public class ActionDialog : Form
|
|||||||
_colorPanel = new Panel
|
_colorPanel = new Panel
|
||||||
{
|
{
|
||||||
Location = new Point(12, 168),
|
Location = new Point(12, 168),
|
||||||
Size = new Size(280, 30),
|
Size = new Size(396, 30),
|
||||||
Visible = showColor,
|
Visible = showColor,
|
||||||
};
|
};
|
||||||
var colorLabel = new Label { Text = "LED-Farbe:", Location = new Point(0, 8), AutoSize = true };
|
var colorLabel = new Label { Text = "LED-Farbe:", Location = new Point(0, 8), AutoSize = true };
|
||||||
_colorBtn = new Button
|
_colorBtn = new Button
|
||||||
{
|
{
|
||||||
Location = new Point(80, 2),
|
Location = new Point(80, 2),
|
||||||
Size = new Size(200, 26),
|
Size = new Size(316, 26),
|
||||||
BackColor = ledColor,
|
BackColor = ledColor,
|
||||||
FlatStyle = FlatStyle.Flat,
|
FlatStyle = FlatStyle.Flat,
|
||||||
Text = string.Empty,
|
Text = string.Empty,
|
||||||
@ -260,37 +340,126 @@ public class ActionDialog : Form
|
|||||||
_colorBtn.Click += OnColorClick;
|
_colorBtn.Click += OnColorClick;
|
||||||
_colorPanel.Controls.AddRange(new Control[] { colorLabel, _colorBtn });
|
_colorPanel.Controls.AddRange(new Control[] { colorLabel, _colorBtn });
|
||||||
|
|
||||||
// ── Buttons Fußzeile ──────────────────────────────────────────────────
|
// ── Animation-Panel ───────────────────────────────────────────────────
|
||||||
int footerY = showColor ? 208 : 168;
|
_animPanel = new Panel
|
||||||
var okBtn = new Button
|
{
|
||||||
|
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",
|
Text = "OK",
|
||||||
DialogResult = DialogResult.OK,
|
DialogResult = DialogResult.OK,
|
||||||
Location = new Point(112, footerY),
|
|
||||||
Width = 80,
|
Width = 80,
|
||||||
};
|
};
|
||||||
okBtn.Click += OnOk;
|
_okBtn.Click += OnOk;
|
||||||
|
|
||||||
var cancelBtn = new Button
|
_cancelBtn = new Button
|
||||||
{
|
{
|
||||||
Text = "Abbrechen",
|
Text = "Abbrechen",
|
||||||
DialogResult = DialogResult.Cancel,
|
DialogResult = DialogResult.Cancel,
|
||||||
Location = new Point(200, footerY),
|
|
||||||
Width = 92,
|
Width = 92,
|
||||||
};
|
};
|
||||||
|
|
||||||
AcceptButton = okBtn;
|
AcceptButton = _okBtn;
|
||||||
CancelButton = cancelBtn;
|
CancelButton = _cancelBtn;
|
||||||
ClientSize = new Size(304, footerY + 44);
|
|
||||||
|
|
||||||
Controls.AddRange(new Control[]
|
Controls.AddRange(new Control[]
|
||||||
{
|
{
|
||||||
typeLabel, _typeCombo,
|
typeLabel, _typeCombo,
|
||||||
_hidKeyPanel, _consumerPanel, _cmdPanel, _colorPanel,
|
_hidKeyPanel, _consumerPanel, _cmdPanel, _macroPanel,
|
||||||
okBtn, cancelBtn,
|
_colorPanel, _animPanel,
|
||||||
|
_okBtn, _cancelBtn,
|
||||||
});
|
});
|
||||||
|
|
||||||
UpdatePanelVisibility();
|
UpdatePanelVisibility(); // setzt Visibility + ruft UpdateLayout auf
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Panel-Sichtbarkeit ────────────────────────────────────────────────────
|
// ── Panel-Sichtbarkeit ────────────────────────────────────────────────────
|
||||||
@ -301,12 +470,62 @@ public class ActionDialog : Form
|
|||||||
UpdatePanelVisibility();
|
UpdatePanelVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnAnimChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
UpdateAnimVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdatePanelVisibility()
|
private void UpdatePanelVisibility()
|
||||||
{
|
{
|
||||||
var t = (ActionType)_typeCombo.SelectedIndex;
|
var t = (ActionType)_typeCombo.SelectedIndex;
|
||||||
_hidKeyPanel.Visible = t == ActionType.HidKey;
|
_hidKeyPanel.Visible = t == ActionType.HidKey;
|
||||||
_consumerPanel.Visible = t == ActionType.HidConsumer;
|
_consumerPanel.Visible = t == ActionType.HidConsumer;
|
||||||
_cmdPanel.Visible = t == ActionType.HostCommand;
|
_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 ──────────────────────────────────────────────────────
|
// ── Tasten-Erfassung ──────────────────────────────────────────────────────
|
||||||
@ -317,6 +536,40 @@ public class ActionDialog : Form
|
|||||||
else StartCapture();
|
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()
|
private void StartCapture()
|
||||||
{
|
{
|
||||||
_capturing = true;
|
_capturing = true;
|
||||||
@ -334,14 +587,14 @@ public class ActionDialog : Form
|
|||||||
|
|
||||||
private void RefreshCaptureBtn()
|
private void RefreshCaptureBtn()
|
||||||
{
|
{
|
||||||
if (_hidKeycode != 0 && s_hidNames.TryGetValue(_hidKeycode, out string? kn))
|
if (_hidKeycode != 0)
|
||||||
{
|
{
|
||||||
var parts = new List<string>();
|
var parts = new List<string>();
|
||||||
if (_chkCtrl.Checked) parts.Add("Strg");
|
if (_chkCtrl.Checked) parts.Add("Strg");
|
||||||
if (_chkShift.Checked) parts.Add("Shift");
|
if (_chkShift.Checked) parts.Add("Shift");
|
||||||
if (_chkAlt.Checked) parts.Add("Alt");
|
if (_chkAlt.Checked) parts.Add("Alt");
|
||||||
if (_chkWin.Checked) parts.Add("Win");
|
if (_chkWin.Checked) parts.Add("Win");
|
||||||
parts.Add(kn);
|
parts.Add(HidKeyName(_hidKeycode));
|
||||||
_captureBtn.Text = string.Join(" + ", parts);
|
_captureBtn.Text = string.Join(" + ", parts);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -353,41 +606,65 @@ public class ActionDialog : Form
|
|||||||
_captureBtn.FlatAppearance.BorderColor = Color.DarkGray;
|
_captureBtn.FlatAppearance.BorderColor = Color.DarkGray;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tasten werden form-weit abgefangen (KeyPreview = true)
|
// ProcessCmdKey wird vor jeder Dialog-Verarbeitung aufgerufen (Enter, Pfeiltasten,
|
||||||
protected override void OnKeyDown(KeyEventArgs e)
|
// 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
|
// 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.ShiftKey or Keys.LShiftKey or Keys.RShiftKey
|
||||||
or Keys.Menu or Keys.LMenu or Keys.RMenu
|
or Keys.Menu or Keys.LMenu or Keys.RMenu
|
||||||
or Keys.LWin or Keys.RWin or Keys.Apps)
|
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;
|
int s = _captureStep;
|
||||||
return;
|
_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)
|
// ── HID-Tastatur-Capture ──────────────────────────────────────────────
|
||||||
if (e.KeyCode == Keys.Escape)
|
if (vk == Keys.Escape)
|
||||||
{
|
{
|
||||||
StopCapture();
|
StopCapture();
|
||||||
e.Handled = true;
|
|
||||||
e.SuppressKeyPress = true;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
// Modifikatoren übernehmen
|
{
|
||||||
_chkCtrl.Checked = e.Control;
|
_chkCtrl.Checked = ctrl;
|
||||||
_chkShift.Checked = e.Shift;
|
_chkShift.Checked = shift;
|
||||||
_chkAlt.Checked = e.Alt;
|
_chkAlt.Checked = alt;
|
||||||
|
_hidKeycode = VkToHid(vk);
|
||||||
// VK → HID-Code
|
StopCapture();
|
||||||
_hidKeycode = s_vkToHid.TryGetValue(e.KeyCode, out byte hid) ? hid : (byte)0;
|
}
|
||||||
|
return true;
|
||||||
StopCapture();
|
|
||||||
e.Handled = true;
|
|
||||||
e.SuppressKeyPress = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Farbe ─────────────────────────────────────────────────────────────────
|
// ── Farbe ─────────────────────────────────────────────────────────────────
|
||||||
@ -435,7 +712,30 @@ public class ActionDialog : Form
|
|||||||
break;
|
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 };
|
ResultAction = new DeviceAction { Type = type, Data = data };
|
||||||
ResultColor = _color;
|
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
|
public class ConfigForm : Form
|
||||||
{
|
{
|
||||||
private readonly DeviceConfig _config;
|
private readonly DeviceConfig _config;
|
||||||
|
private readonly MacroTable _macros;
|
||||||
private readonly SerialManager _serial;
|
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];
|
private readonly Button[] _btnGrid = new Button[20];
|
||||||
|
|
||||||
// Encoder-Buttons [enc][SW/CW/CCW]
|
|
||||||
private readonly Button[,] _encBtns = new Button[4, 3];
|
private readonly Button[,] _encBtns = new Button[4, 3];
|
||||||
|
|
||||||
private Button _saveBtn = null!;
|
private Button _saveBtn = null!;
|
||||||
|
|
||||||
public ConfigForm(DeviceConfig config, SerialManager serial)
|
public ConfigForm(DeviceConfig config, MacroTable macros, SerialManager serial)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_macros = macros;
|
||||||
_serial = serial;
|
_serial = serial;
|
||||||
|
|
||||||
Text = "VersaPad – Konfiguration";
|
Text = "VersaPad – Konfiguration";
|
||||||
@ -182,6 +180,22 @@ public class ConfigForm : Form
|
|||||||
};
|
};
|
||||||
pingBtn.Click += (_, _) => _serial.Send(new SerialPacket(Protocol.CmdPing));
|
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
|
var closeBtn = new Button
|
||||||
{
|
{
|
||||||
Text = "Schließen",
|
Text = "Schließen",
|
||||||
@ -190,7 +204,7 @@ public class ConfigForm : Form
|
|||||||
};
|
};
|
||||||
closeBtn.Click += (_, _) => Close();
|
closeBtn.Click += (_, _) => Close();
|
||||||
|
|
||||||
Controls.AddRange(new Control[] { _saveBtn, pingBtn, closeBtn });
|
Controls.AddRange(new Control[] { _saveBtn, pingBtn, exportBtn, importBtn, closeBtn });
|
||||||
|
|
||||||
// Fensterhöhe anpassen
|
// Fensterhöhe anpassen
|
||||||
ClientSize = new Size(ClientSize.Width, startY + 50);
|
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;
|
if (sender is not Button btn || btn.Tag is not int mxIdx) return;
|
||||||
|
|
||||||
|
int slot = MacroTable.SlotForMx(mxIdx);
|
||||||
using var dlg = new ActionDialog(
|
using var dlg = new ActionDialog(
|
||||||
_config.MxActions[mxIdx],
|
_config.MxActions[mxIdx],
|
||||||
_config.LedBase[mxIdx],
|
_config.LedBase[mxIdx],
|
||||||
|
_config.LedAnim[mxIdx],
|
||||||
|
_config.LedPeriod[mxIdx],
|
||||||
|
_macros, slot,
|
||||||
showColor: true);
|
showColor: true);
|
||||||
|
|
||||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||||
|
|
||||||
_config.MxActions[mxIdx] = dlg.ResultAction;
|
_config.MxActions[mxIdx] = dlg.ResultAction;
|
||||||
_config.LedBase[mxIdx] = dlg.ResultColor;
|
_config.LedBase[mxIdx] = dlg.ResultColor;
|
||||||
|
_config.LedAnim[mxIdx] = dlg.ResultAnim;
|
||||||
|
_config.LedPeriod[mxIdx] = dlg.ResultPeriod;
|
||||||
RefreshMxButton(mxIdx);
|
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;
|
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(
|
using var dlg = new ActionDialog(
|
||||||
_config.EncActions[enc, act],
|
_config.EncActions[enc, act],
|
||||||
Color.Black,
|
Color.Black,
|
||||||
|
LedAnimType.Static, 0,
|
||||||
|
_macros, slot,
|
||||||
showColor: false);
|
showColor: false);
|
||||||
|
|
||||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||||
@ -234,15 +257,16 @@ public class ConfigForm : Form
|
|||||||
_saveBtn.Enabled = false;
|
_saveBtn.Enabled = false;
|
||||||
_saveBtn.Text = "Wird gesendet...";
|
_saveBtn.Text = "Wird gesendet...";
|
||||||
|
|
||||||
// SendConfig blockiert ~180ms (Delays zwischen Paketen) → Background-Thread
|
// SendConfig + SendMacros blockieren ~400ms → Background-Thread
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
_serial.SendConfig(_config);
|
_serial.SendConfig(_config);
|
||||||
|
Thread.Sleep(50); // kurze Pause zwischen Config- und Makro-Transfer
|
||||||
|
_serial.SendMacros(_macros);
|
||||||
InvokeOnUi(() =>
|
InvokeOnUi(() =>
|
||||||
{
|
{
|
||||||
_saveBtn.Text = "Auf Board speichern";
|
_saveBtn.Text = "Auf Board speichern";
|
||||||
_saveBtn.Enabled = _serial.IsConnected;
|
_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 btn = _btnGrid[mxIdx];
|
||||||
var action = _config.MxActions[mxIdx];
|
var action = _config.MxActions[mxIdx];
|
||||||
var color = _config.LedBase[mxIdx];
|
var color = _config.LedBase[mxIdx];
|
||||||
|
var anim = _config.LedAnim[mxIdx];
|
||||||
|
|
||||||
btn.BackColor = color;
|
// Regenbogen: Hintergrund als Verlauf andeuten (dunkles Grau als Neutral)
|
||||||
// Textfarbe kontrastreich zur Hintergrundfarbe wählen
|
if (anim == LedAnimType.ColorCycle)
|
||||||
btn.ForeColor = Luminance(color) > 128 ? Color.Black : Color.White;
|
{
|
||||||
btn.Text = action.Display;
|
btn.BackColor = Color.FromArgb(40, 40, 40);
|
||||||
|
btn.ForeColor = Color.White;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
btn.BackColor = color;
|
||||||
|
btn.ForeColor = Luminance(color) > 128 ? Color.Black : Color.White;
|
||||||
|
}
|
||||||
|
|
||||||
|
string animName = anim switch
|
||||||
|
{
|
||||||
|
LedAnimType.Blink => " [Blinken]",
|
||||||
|
LedAnimType.Pulse => " [Pulsieren]",
|
||||||
|
LedAnimType.ColorCycle => " [Regenbogen]",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
btn.Text = action.Display + animName;
|
||||||
btn.ToolTipText(action.TypeDescription());
|
btn.ToolTipText(action.TypeDescription());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,6 +323,53 @@ public class ConfigForm : Form
|
|||||||
private static int Luminance(Color c)
|
private static int Luminance(Color c)
|
||||||
=> (c.R * 299 + c.G * 587 + c.B * 114) / 1000;
|
=> (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)
|
private void InvokeOnUi(Action a)
|
||||||
{
|
{
|
||||||
if (InvokeRequired) Invoke(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
|
// DeviceConfig.cs
|
||||||
// C#-Spiegel der SDeviceConfig-Struktur aus der Firmware (nvm_config.h).
|
// 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 0 4B Magic 0x56503202
|
||||||
// Offset 4 1B Version 1
|
// Offset 4 1B Version 2
|
||||||
// Offset 5 2B CRC16 (CCITT über Bytes 7–162)
|
// 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 7 60B mx_actions[20] je 3B: type(1) + data_lo(1) + data_hi(1)
|
||||||
// Offset 67 36B enc_actions[4][3] je 3B
|
// Offset 67 36B enc_actions[4][3] je 3B
|
||||||
// Offset103 20B led_r[20]
|
// Offset103 20B led_r[20]
|
||||||
// Offset123 20B led_g[20]
|
// Offset123 20B led_g[20]
|
||||||
// Offset143 20B led_b[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;
|
using System.Drawing;
|
||||||
|
|
||||||
@ -21,6 +23,73 @@ public enum ActionType : byte
|
|||||||
HidKey = 1, // data = Keycode (Low-Byte) + Modifier (High-Byte)
|
HidKey = 1, // data = Keycode (Low-Byte) + Modifier (High-Byte)
|
||||||
HidConsumer = 2, // data = HID Consumer Usage ID
|
HidConsumer = 2, // data = HID Consumer Usage ID
|
||||||
HostCommand = 3, // data = Command-ID → App führt aus
|
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
|
public class DeviceAction
|
||||||
@ -28,7 +97,6 @@ public class DeviceAction
|
|||||||
public ActionType Type { get; set; } = ActionType.None;
|
public ActionType Type { get; set; } = ActionType.None;
|
||||||
public ushort Data { get; set; } = 0;
|
public ushort Data { get; set; } = 0;
|
||||||
|
|
||||||
// Kurzanzeige für Buttons und Tooltips
|
|
||||||
public string Display
|
public string Display
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@ -60,6 +128,8 @@ public class DeviceAction
|
|||||||
_ => $"C:{Data:X3}",
|
_ => $"C:{Data:X3}",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (Type == ActionType.Macro)
|
||||||
|
return $"Makro {Data}";
|
||||||
return $"CMD {Data}";
|
return $"CMD {Data}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,24 +155,29 @@ public class DeviceAction
|
|||||||
public class DeviceConfig
|
public class DeviceConfig
|
||||||
{
|
{
|
||||||
public const uint Magic = 0x56503202;
|
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 EncSw = 0;
|
||||||
public const int EncCw = 1;
|
public const int EncCw = 1;
|
||||||
public const int EncCcw = 2;
|
public const int EncCcw = 2;
|
||||||
|
|
||||||
// 20 MX-Button-Aktionen (key_id 5–24, mx_idx = key_id - 5)
|
|
||||||
public DeviceAction[] MxActions { get; } =
|
public DeviceAction[] MxActions { get; } =
|
||||||
Enumerable.Range(0, 20).Select(_ => new DeviceAction()).ToArray();
|
Enumerable.Range(0, 20).Select(_ => new DeviceAction()).ToArray();
|
||||||
|
|
||||||
// 4 Encoder × 3 Aktionen [enc][SW/CW/CCW]
|
|
||||||
public DeviceAction[,] EncActions { get; } = InitEncActions();
|
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; } =
|
public Color[] LedBase { get; } =
|
||||||
Enumerable.Repeat(Color.FromArgb(80, 40, 0), 20).ToArray();
|
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()
|
private static DeviceAction[,] InitEncActions()
|
||||||
{
|
{
|
||||||
var a = new DeviceAction[4, 3];
|
var a = new DeviceAction[4, 3];
|
||||||
@ -114,36 +189,36 @@ public class DeviceConfig
|
|||||||
|
|
||||||
// ── Serialisierung ────────────────────────────────────────────────────────
|
// ── Serialisierung ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Erzeugt den 163-Byte-Puffer der 1:1 SDeviceConfig entspricht.
|
|
||||||
public byte[] ToBytes()
|
public byte[] ToBytes()
|
||||||
{
|
{
|
||||||
const int size = 163;
|
const int size = 223;
|
||||||
var buf = new byte[size];
|
var buf = new byte[size];
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
|
|
||||||
// Magic (little-endian)
|
|
||||||
WriteU32(buf, ref pos, Magic);
|
WriteU32(buf, ref pos, Magic);
|
||||||
// Version
|
|
||||||
buf[pos++] = Version;
|
buf[pos++] = Version;
|
||||||
// CRC-Platzhalter (wird am Ende eingetragen)
|
|
||||||
int crcOffset = pos;
|
int crcOffset = pos;
|
||||||
pos += 2;
|
pos += 2;
|
||||||
|
|
||||||
// mx_actions[20]
|
|
||||||
foreach (var a in MxActions)
|
foreach (var a in MxActions)
|
||||||
WriteAction(buf, ref pos, a);
|
WriteAction(buf, ref pos, a);
|
||||||
|
|
||||||
// enc_actions[4][3]
|
|
||||||
for (int e = 0; e < 4; e++)
|
for (int e = 0; e < 4; e++)
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
WriteAction(buf, ref pos, EncActions[e, 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].R;
|
||||||
for (int i = 0; i < 20; i++) buf[pos++] = LedBase[i].G;
|
for (int i = 0; i < 20; i++) buf[pos++] = LedBase[i].G;
|
||||||
for (int i = 0; i < 20; i++) buf[pos++] = LedBase[i].B;
|
for (int i = 0; i < 20; i++) buf[pos++] = 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);
|
ushort crc = Crc16(buf, 7, size - 7);
|
||||||
buf[crcOffset] = (byte)(crc & 0xFF);
|
buf[crcOffset] = (byte)(crc & 0xFF);
|
||||||
buf[crcOffset + 1] = (byte)(crc >> 8);
|
buf[crcOffset + 1] = (byte)(crc >> 8);
|
||||||
@ -151,18 +226,16 @@ public class DeviceConfig
|
|||||||
return buf;
|
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)
|
public bool FromBytes(byte[] buf)
|
||||||
{
|
{
|
||||||
if (buf.Length < 163) return false;
|
if (buf.Length < 223) return false;
|
||||||
|
|
||||||
uint magic = ReadU32(buf, 0);
|
uint magic = ReadU32(buf, 0);
|
||||||
if (magic != Magic) return false;
|
if (magic != Magic) return false;
|
||||||
if (buf[4] != Version) return false;
|
if (buf[4] != Version) return false;
|
||||||
|
|
||||||
ushort storedCrc = (ushort)(buf[5] | (buf[6] << 8));
|
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;
|
if (storedCrc != computedCrc) return false;
|
||||||
|
|
||||||
int pos = 7;
|
int pos = 7;
|
||||||
@ -172,6 +245,15 @@ public class DeviceConfig
|
|||||||
ReadAction(buf, ref pos, EncActions[e, i]);
|
ReadAction(buf, ref pos, EncActions[e, i]);
|
||||||
|
|
||||||
for (int i = 0; i < 20; i++) LedBase[i] = Color.FromArgb(buf[pos + i], buf[pos + 20 + i], buf[pos + 40 + i]);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
@ -203,7 +285,6 @@ public class DeviceConfig
|
|||||||
private static uint ReadU32(byte[] buf, int pos)
|
private static uint ReadU32(byte[] buf, int pos)
|
||||||
=> (uint)(buf[pos] | (buf[pos+1] << 8) | (buf[pos+2] << 16) | (buf[pos+3] << 24));
|
=> (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)
|
public static ushort Crc16(byte[] data, int offset, int length)
|
||||||
{
|
{
|
||||||
ushort crc = 0xFFFF;
|
ushort crc = 0xFFFF;
|
||||||
|
|||||||
@ -31,6 +31,10 @@ public static class Protocol
|
|||||||
public const byte CmdConfigData = 0x11;
|
public const byte CmdConfigData = 0x11;
|
||||||
public const byte CmdConfigCommit = 0x12;
|
public const byte CmdConfigCommit = 0x12;
|
||||||
public const byte CmdConfigRead = 0x13; // Board sendet aktuelle NVM-Config zurück
|
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) ──────────────────────────────────────
|
// ── Events: Board → App (0x81–0xFF) ──────────────────────────────────────
|
||||||
public const byte EvtKeyDown = 0x81; // key_id → HOST_COMMAND-Button gedrückt
|
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 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 EvtConfigData = 0x93; // Config-Chunk (Data[1] = Index, Data[2..7] = 6B)
|
||||||
public const byte EvtConfigEnd = 0x94; // Config-Dump vollständig
|
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.
|
// Ein 8-Byte-Paket mit benannten Accessoren.
|
||||||
|
|||||||
@ -217,6 +217,36 @@ public class SerialManager : IDisposable
|
|||||||
public void RequestConfig()
|
public void RequestConfig()
|
||||||
=> Send(new SerialPacket(Protocol.CmdConfigRead));
|
=> 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.
|
// Config in 6-Byte-Chunks an das Board senden.
|
||||||
// Protokoll: BEGIN → n×DATA → COMMIT
|
// Protokoll: BEGIN → n×DATA → COMMIT
|
||||||
// Board schreibt nach COMMIT in den NVM (Firmware-Seite noch TODO).
|
// 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 NotifyIcon _tray;
|
||||||
private readonly SerialManager _serial;
|
private readonly SerialManager _serial;
|
||||||
private readonly DeviceConfig _config = new();
|
private readonly DeviceConfig _config = new();
|
||||||
|
private readonly MacroTable _macros = new();
|
||||||
|
|
||||||
private ToolStripMenuItem _statusItem = null!;
|
private ToolStripMenuItem _statusItem = null!;
|
||||||
private ConfigForm? _configForm;
|
private ConfigForm? _configForm;
|
||||||
|
|
||||||
// Empfangspuffer für eingehenden Config-Dump vom Board
|
// Empfangspuffer für eingehende Dumps vom Board
|
||||||
private byte[]? _rxConfigBuf;
|
private byte[]? _rxConfigBuf;
|
||||||
|
private byte[]? _rxMacroBuf;
|
||||||
|
|
||||||
public TrayApp()
|
public TrayApp()
|
||||||
{
|
{
|
||||||
@ -78,7 +80,7 @@ public class TrayApp : ApplicationContext
|
|||||||
_configForm.BringToFront();
|
_configForm.BringToFront();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_configForm = new ConfigForm(_config, _serial);
|
_configForm = new ConfigForm(_config, _macros, _serial);
|
||||||
_configForm.Show();
|
_configForm.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,8 +92,9 @@ public class TrayApp : ApplicationContext
|
|||||||
_tray.Icon = SystemIcons.Information;
|
_tray.Icon = SystemIcons.Information;
|
||||||
_statusItem.Text = "● Verbunden";
|
_statusItem.Text = "● Verbunden";
|
||||||
|
|
||||||
// Config sofort vom Board laden
|
// Config und Makro-Tabelle vom Board laden
|
||||||
_serial.RequestConfig();
|
_serial.RequestConfig();
|
||||||
|
_serial.RequestMacros();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisconnected(object? sender, EventArgs e)
|
private void OnDisconnected(object? sender, EventArgs e)
|
||||||
@ -136,7 +139,7 @@ public class TrayApp : ApplicationContext
|
|||||||
|
|
||||||
// ── Config-Dump vom Board ─────────────────────────────────────────
|
// ── Config-Dump vom Board ─────────────────────────────────────────
|
||||||
case Protocol.EvtConfigBegin:
|
case Protocol.EvtConfigBegin:
|
||||||
_rxConfigBuf = new byte[163];
|
_rxConfigBuf = new byte[223]; // sizeof(SDeviceConfig) = 223 (Version 2)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Protocol.EvtConfigData:
|
case Protocol.EvtConfigData:
|
||||||
@ -154,7 +157,35 @@ public class TrayApp : ApplicationContext
|
|||||||
{
|
{
|
||||||
_config.FromBytes(_rxConfigBuf);
|
_config.FromBytes(_rxConfigBuf);
|
||||||
_rxConfigBuf = null;
|
_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;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user