From 3e83758f05115e24718f1738ca9f4d21c4b9657d Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Tue, 31 Mar 2026 21:46:20 +0200 Subject: [PATCH] Added hold function and updated doc --- doc/03_action_engine.md | 78 +++++++++++++++----------- doc/06_nvm_config.md | 2 +- src/CButton.cpp | 26 +++------ src/CButton.h | 9 +-- src/CMainController.cpp | 112 +++++++++++++++++++++++++------------- src/CMainController.h | 11 ++-- src/config/nvm_config.cpp | 6 +- src/config/nvm_config.h | 2 +- 8 files changed, 141 insertions(+), 105 deletions(-) diff --git a/doc/03_action_engine.md b/doc/03_action_engine.md index 44944b5..8f848cc 100644 --- a/doc/03_action_engine.md +++ b/doc/03_action_engine.md @@ -1,6 +1,6 @@ # Aktions-Engine -**Dateien:** `config/action.h`, `CMainController.cpp` (`processEvents`, `execute_action`) +**Dateien:** `config/action.h`, `CButton.h/.cpp`, `CMainController.cpp` (`processEvents`, `execute_action_down`, `execute_action_up`) ## SAction-Struct @@ -24,47 +24,59 @@ struct __attribute__((packed)) SAction { | `HOST_COMMAND` | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) | | `MACRO` | Makro-Sequenz aus NVM-Tabelle | Slot-Index 0–31 | -## Ausführung (execute_action) +## execute_action_down() — Taste gedrückt (Hold-Start) -**HID_KEY:** -``` -usb_hid_send_key(keycode, modifier) -delay(10 ms) -usb_hid_release_key() -``` -→ Tap-Only-Modell. Kein Hold-Support. KEY_UP löst kein HID-Release aus. +| ActionType | Verhalten | +|---|---| +| `HID_KEY` | `usb_hid_send_key(keycode, modifier)` — Taste bleibt gedrückt bis `execute_action_up()` | +| `HID_CONSUMER` | `usb_hid_send_consumer(usage_id)` — bleibt aktiv bis `execute_action_up()` | +| `HOST_COMMAND` | `usb_serial_send(USB_EVT_KEY_DOWN, key_id)` | +| `MACRO` | Volle Sequenz ausführen (Steps[slot], keycode==0 = Ende, delay 10+20 ms) | +| `NONE` | nop | -**HID_CONSUMER:** -``` -usb_hid_send_consumer(usage_id) -usb_hid_release_consumer() -``` -→ Kein Delay nötig (Consumer-Keys sind Edge-getriggert). +## execute_action_up() — Taste losgelassen (Hold-Ende) -**HOST_COMMAND:** -`execute_action()` macht nichts. `processEvents()` sendet zusätzlich `USB_EVT_KEY_DOWN` via CDC Serial an VersaGUI. Die App entscheidet was passiert (URL öffnen, Programm starten, …). +| ActionType | Verhalten | +|---|---| +| `HID_KEY` | `usb_hid_release_key()` | +| `HID_CONSUMER` | `usb_hid_release_consumer()` | +| `HOST_COMMAND` | — (optional: könnte `USB_EVT_KEY_UP` senden) | +| `MACRO`/`NONE` | nop | + +## Hold-Modell (HID-Keys und Consumer Controls) + +Normale Tasten- und Media-Aktionen folgen dem **Hold-Modell**: -**MACRO:** ``` -für jeden Step [0–3] im Slot: - if keycode == 0: abbrechen - usb_hid_send_key(keycode, modifier) - delay(10 ms) - usb_hid_release_key() - delay(20 ms) // Pause damit der Host den Step verarbeiten kann +KEY_DOWN-Event vom Board → execute_action_down() → HID Key-Down senden +[Taste bleibt physisch gedrückt...] +KEY_UP-Event vom Board → execute_action_up() → HID Key-Up senden ``` -## Event-Verarbeitung +Das OS erkennt die gedrückte Taste und startet sein eigenes Key-Repeat nach ~500 ms — wie auf einer normalen Tastatur. -Events werden in `processEvents()` aus `CEventQueue` konsumiert (FIFO): +## Tap-Modell (Encoder CW/CCW) -- `KEY_DOWN` → `CButton.on_press()` (aktuell leer, Erweiterungspunkt) + `execute_action()` -- `KEY_UP` → `CButton.on_release()` (aktuell leer) -- `ENC_CW/CCW` → `execute_action(m_enc_cw/ccw[enc_id])` +Encoder-Bewegungen sind diskret (kein Halten möglich) und verwenden das **Tap-Modell**: -Encoder CW/CCW-Aktionen sind in `CMainController` direkt als `SAction`-Arrays gehalten (kein CButton-Objekt, da Encoder keine LED haben). +``` +ENC_CW/ENC_CCW-Event → execute_action_down() + delay(10) + execute_action_up() +``` -## Bekannte Einschränkungen +(Atomare Sequenz für jeden Encoder-Schritt.) -- **Kein Hold**: `execute_action` bei KEY_DOWN sendet sofort Key-Down + Key-Up. Halten der Taste löst keine Wiederholung aus. -- **HOST_COMMAND KEY_UP**: Board sendet derzeit kein `USB_EVT_KEY_UP` für KEY_UP-Events (nur KEY_DOWN wird gemeldet). +## Work-Loop-Reihenfolge + +```cpp +void work() { + matrix_scan(); // → Events in Queue (KEY_DOWN, KEY_UP, ENC_CW, ENC_CCW) + poll_vendor(); // Serial-Pakete verarbeiten (PC↔Board Kommandos) + processEvents(); // → execute_action_down/up() aufrufen + updateLEDs(); // Dirty-LEDs aktualisieren +} +``` + +**processEvents() verarbeitet:** +- `KEY_DOWN` → `execute_action_down()` +- `KEY_UP` → `execute_action_up()` +- `ENC_CW` / `ENC_CCW` → `execute_action_down()` + `delay(10)` + `execute_action_up()` diff --git a/doc/06_nvm_config.md b/doc/06_nvm_config.md index d7c626e..a1460c8 100644 --- a/doc/06_nvm_config.md +++ b/doc/06_nvm_config.md @@ -32,7 +32,7 @@ Beide Rows sind im Linkerscript vom Code-Bereich ausgeschlossen. ## CRC16-CCITT - Polynom: `0x1021`, Init: `0xFFFF` -- Berechnet über Bytes 7–222 (ab `mx_actions`, nach dem `crc`-Feld selbst) +- Berechnet über Bytes 7–248 (ab `mx_actions`, nach dem `crc`-Feld selbst) - Sichert Datenintegrität nach NVM-Schreiben und bei Versionswechsel ## Lese-Logik diff --git a/src/CButton.cpp b/src/CButton.cpp index 469fc14..cc87b06 100644 --- a/src/CButton.cpp +++ b/src/CButton.cpp @@ -64,25 +64,13 @@ CButton::CButton() void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base) { - m_key_id = key_id; - m_led_index = led_index; - m_action = action; - m_base = base; - m_override_active = false; - m_anim = LEDAnim::STATIC; - m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben -} - -// ── Tastendruck-Hooks ───────────────────────────────────────────────────────── - -void CButton::on_press() -{ - // Reserviert für zukünftige Button-Logik (Hold, Toggle, Doppelklick …) -} - -void CButton::on_release() -{ - // Reserviert für zukünftige Button-Logik + m_key_id = key_id; + m_led_index = led_index; + m_action = action; + m_base = base; + m_override_active = false; + m_anim = LEDAnim::STATIC; + m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben } // ── LED Layer 1: base ───────────────────────────────────────────────────────── diff --git a/src/CButton.h b/src/CButton.h index c6f0cdd..9432846 100644 --- a/src/CButton.h +++ b/src/CButton.h @@ -61,13 +61,8 @@ public: CButton(); // Initialisierung (ersetzt Konstruktor-Parameter). - // led_index = -1 → kein LED (Encoder-SW-Buttons). - void init(uint8_t key_id, int8_t led_index, SAction action, RGB base = RGB()); - - // Hooks für Tastendruck/-loslassen. - // Reserviert für zukünftige Logik (Hold-Aktionen, Toggle-Modus, …). - void on_press(); - void on_release(); + void init(uint8_t key_id, int8_t led_index, SAction action, + RGB base = RGB()); // ── LED Layer 1: base ───────────────────────────────────────────────────── // Idle-Farbe, aus NVM geladen oder von Windows-App gesetzt. diff --git a/src/CMainController.cpp b/src/CMainController.cpp index ad3ebcf..2b5eac7 100644 --- a/src/CMainController.cpp +++ b/src/CMainController.cpp @@ -103,7 +103,7 @@ void CMainController::init_buttons() // Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1) for (uint8_t enc = 0; enc < 4; enc++) { - m_buttons[enc].init(enc, -1, cfg.enc_actions[enc][ENC_ACTION_SW]); + m_buttons[enc].init(enc, -1, cfg.enc_actions[enc][ENC_ACTION_SW], RGB()); } // MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen, @@ -115,7 +115,7 @@ void CMainController::init_buttons() uint8_t row = key % MATRIX_ROWS; int8_t led = static_cast(LED_INDEX(col, row)); uint8_t mx_idx = key - 5; - RGB base(cfg.led_r[mx_idx], cfg.led_g[mx_idx], cfg.led_b[mx_idx]); + RGB base(cfg.led_r[mx_idx], cfg.led_g[mx_idx], cfg.led_b[mx_idx]); m_buttons[key].init(key, led, cfg.mx_actions[mx_idx], base); LEDAnim anim = static_cast(cfg.led_anim[mx_idx]); @@ -142,10 +142,10 @@ void CMainController::init_buttons() void CMainController::work() { - matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue - poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten - processEvents(); // 3. Queue leeren: Aktionen ausführen, Buttons benachrichtigen - updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show() + matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue + poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten + processEvents(); // 3. Queue leeren, Aktionen ausführen + updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show() } // ─── Vendor-Kommunikation (PC → Board) ─────────────────────────────────────── @@ -212,9 +212,9 @@ void CMainController::poll_vendor() SDeviceConfig cfg; nvm_config_load(cfg); // ungültige NVM → Defaults const uint8_t* raw = reinterpret_cast(&cfg); - const uint8_t sz = sizeof(SDeviceConfig); // 163 + const uint8_t sz = sizeof(SDeviceConfig); // 223 const uint8_t payload = 6; - uint8_t chunks = (sz + payload - 1) / payload; // 28 + uint8_t chunks = (sz + payload - 1) / payload; // 38 usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks); @@ -316,43 +316,40 @@ void CMainController::poll_vendor() // Verarbeitet alle Events in der Queue bis sie leer ist. // Reihenfolge: ältestes Event zuerst (FIFO). // -// HOST_COMMAND-Aktionen werden zusätzlich über Serial an die Windows-App -// gemeldet – die App entscheidet dann was passiert (URL öffnen, Programm starten…). +// KEY_DOWN: execute_action_down() – HID-Taste wird gedrückt, bleibt aktiv bis KEY_UP. +// KEY_UP: execute_action_up() – HID-Taste wird losgelassen. +// Encoder CW/CCW: execute_action_down() + execute_action_up() für atomare TAP-Sequenz. void CMainController::processEvents() { SEvent ev; + while (m_queue.pop(ev)) { switch (ev.type) { case EventType::KEY_DOWN: - if (ev.key_id < MATRIX_KEYS) { - m_buttons[ev.key_id].on_press(); - execute_action(m_buttons[ev.key_id].action()); - // Bei HOST_COMMAND: Event-ID an Windows-App senden - if (m_buttons[ev.key_id].action().type == ActionType::HOST_COMMAND) - usb_serial_send(USB_EVT_KEY_DOWN, ev.key_id); - } + if (ev.key_id < MATRIX_KEYS) + execute_action_down(m_buttons[ev.key_id].action(), ev.key_id); break; case EventType::KEY_UP: if (ev.key_id < MATRIX_KEYS) - m_buttons[ev.key_id].on_release(); + execute_action_up(m_buttons[ev.key_id].action(), ev.key_id); break; case EventType::ENC_CW: if (ev.key_id < 4) { - execute_action(m_enc_cw[ev.key_id]); - if (m_enc_cw[ev.key_id].type == ActionType::HOST_COMMAND) - usb_serial_send(USB_EVT_ENC_CW, ev.key_id); + execute_action_down(m_enc_cw[ev.key_id], ev.key_id); + delay(10); + execute_action_up(m_enc_cw[ev.key_id], ev.key_id); } break; case EventType::ENC_CCW: if (ev.key_id < 4) { - execute_action(m_enc_ccw[ev.key_id]); - if (m_enc_ccw[ev.key_id].type == ActionType::HOST_COMMAND) - usb_serial_send(USB_EVT_ENC_CCW, ev.key_id); + execute_action_down(m_enc_ccw[ev.key_id], ev.key_id); + delay(10); + execute_action_up(m_enc_ccw[ev.key_id], ev.key_id); } break; @@ -362,35 +359,51 @@ void CMainController::processEvents() } } -// Führt eine einzelne Aktion aus. -// HID_KEY / HID_CONSUMER: direkt über USB HID gesendet (funktioniert ohne Windows-App). -// HOST_COMMAND: kein direkter Aufruf hier – das Event wird in processEvents() via -// usb_serial_send() an die Windows-App weitergeleitet. -void CMainController::execute_action(SAction action) +// ─── Aktions-Ausführung ─────────────────────────────────────────────────────── +// +// execute_action_down(): Taste wird gedrückt (Hold-Start). +// HID_KEY: sendet Key-Down, bleibt aktiv. +// HID_CONSUMER: sendet Consumer-Down, bleibt aktiv. +// HOST_COMMAND: sendet KEY_DOWN-Event an Windows-App. +// MACRO: führt volle Sequenz aus (Key-Down/Up jeweils mit Pause). +// NONE: keine Aktion. +// +// execute_action_up(): Taste wird losgelassen (Hold-Ende). +// HID_KEY: sendet Key-Up. +// HID_CONSUMER: sendet Consumer-Up. +// HOST_COMMAND: kann USB_EVT_KEY_UP senden. +// MACRO/NONE: keine Aktion. + +void CMainController::execute_action_down(SAction action, uint8_t key_id) { switch (action.type) { case ActionType::HID_KEY: + { // data-Encoding: Low-Byte = Keycode, High-Byte = Modifier - usb_hid_send_key(static_cast(action.data & 0xFF), - static_cast(action.data >> 8)); - delay(10); // Host braucht kurz Zeit zwischen Key-Down und Key-Up - usb_hid_release_key(); + uint8_t keycode = static_cast(action.data & 0xFF); + uint8_t modifier = static_cast(action.data >> 8); + usb_hid_send_key(keycode, modifier); + // Taste bleibt gedrückt bis execute_action_up() aufgerufen wird break; + } case ActionType::HID_CONSUMER: + { usb_hid_send_consumer(action.data); - usb_hid_release_consumer(); + // Consumer-Control bleibt aktiv bis execute_action_up() aufgerufen wird break; + } case ActionType::HOST_COMMAND: - // Wird in processEvents() über Serial gesendet + // Windows-App übernimmt Ausführung; KEY_DOWN-Event senden + usb_serial_send(USB_EVT_KEY_DOWN, key_id); break; case ActionType::MACRO: { - // Makro-Slot aus dem RAM ausführen (bei setup() aus NVM geladen). - // Steps mit keycode=0 werden übersprungen; erstes leeres Step stoppt. + // Makros sind Sequenzen – Steps mit keycode=0 werden übersprungen; + // erstes leeres Step stoppt. uint8_t slot = static_cast(action.data); if (slot >= MACRO_SLOTS) break; for (uint8_t i = 0; i < MACRO_MAX_STEPS; i++) { @@ -410,6 +423,31 @@ void CMainController::execute_action(SAction action) } } +void CMainController::execute_action_up(SAction action, uint8_t key_id) +{ + switch (action.type) { + + case ActionType::HID_KEY: + usb_hid_release_key(); + break; + + case ActionType::HID_CONSUMER: + usb_hid_release_consumer(); + break; + + case ActionType::HOST_COMMAND: + // Optional: USB_EVT_KEY_UP senden (aktuell nicht implementiert) + break; + + case ActionType::MACRO: + case ActionType::NONE: + default: + // MACRO: Sequenz ist in execute_action_down() komplett abgelaufen, nop hier + // NONE: keine Aktion + break; + } +} + // ─── LED-Rendering ──────────────────────────────────────────────────────────── // // Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt diff --git a/src/CMainController.h b/src/CMainController.h index b95887a..8979ac6 100644 --- a/src/CMainController.h +++ b/src/CMainController.h @@ -35,11 +35,12 @@ private: SAction m_enc_cw[4]; SAction m_enc_ccw[4]; - void init_buttons(); // Buttons aus NVM-Config initialisieren - void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten - void processEvents(); // Queue leeren, Aktionen ausführen - void execute_action(SAction); // Einzelne Aktion ausführen (HID / Serial) - void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben + void init_buttons(); // Buttons aus NVM-Config initialisieren + void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten + void processEvents(); // Queue leeren, Aktionen ausführen + void execute_action_down(SAction action, uint8_t key_id); // Taste drücken (Hold-Start) + void execute_action_up(SAction action, uint8_t key_id); // Taste losgelassen (Hold-Ende) + void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben // ── Config-Empfangspuffer ───────────────────────────────────────────────── uint8_t m_cfg_buf[223]; // sizeof(SDeviceConfig) = 223 Bytes diff --git a/src/config/nvm_config.cpp b/src/config/nvm_config.cpp index d8564a8..6b63bde 100644 --- a/src/config/nvm_config.cpp +++ b/src/config/nvm_config.cpp @@ -106,8 +106,10 @@ bool nvm_config_load(SDeviceConfig& cfg) // ── Speichern ───────────────────────────────────────────────────────────────── void nvm_config_save(const SDeviceConfig& cfg) { - // Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist - uint8_t row[256]; + // Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist. + // __attribute__((aligned(4))) ist zwingend: nvm_write_page castet data zu + // const uint32_t*, und unaligned 32-Bit-Zugriffe sind HardFaults auf Cortex-M0+. + uint8_t row[256] __attribute__((aligned(4))); memset(row, 0xFF, sizeof(row)); memcpy(row, &cfg, sizeof(cfg)); diff --git a/src/config/nvm_config.h b/src/config/nvm_config.h index d232be7..8858a02 100644 --- a/src/config/nvm_config.h +++ b/src/config/nvm_config.h @@ -21,7 +21,7 @@ // Gesamt genutzt: 223 Bytes (sizeof SDeviceConfig mit packed SAction) #define NVM_CONFIG_MAGIC 0x56503202UL -#define NVM_CONFIG_VERSION 2 // Version 2: led_anim + led_period_ms hinzugefügt +#define NVM_CONFIG_VERSION 2 // Version 2 // Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[]) // Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW