Added hold function and updated doc

This commit is contained in:
Julian Appel 2026-03-31 21:46:20 +02:00
parent d6ed7cb81f
commit 3e83758f05
8 changed files with 141 additions and 105 deletions

View File

@ -1,6 +1,6 @@
# Aktions-Engine # 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 ## SAction-Struct
@ -24,47 +24,59 @@ struct __attribute__((packed)) SAction {
| `HOST_COMMAND` | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) | | `HOST_COMMAND` | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) |
| `MACRO` | Makro-Sequenz aus NVM-Tabelle | Slot-Index 031 | | `MACRO` | Makro-Sequenz aus NVM-Tabelle | Slot-Index 031 |
## Ausführung (execute_action) ## execute_action_down() — Taste gedrückt (Hold-Start)
**HID_KEY:** | ActionType | Verhalten |
``` |---|---|
usb_hid_send_key(keycode, modifier) | `HID_KEY` | `usb_hid_send_key(keycode, modifier)` — Taste bleibt gedrückt bis `execute_action_up()` |
delay(10 ms) | `HID_CONSUMER` | `usb_hid_send_consumer(usage_id)` — bleibt aktiv bis `execute_action_up()` |
usb_hid_release_key() | `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) |
→ Tap-Only-Modell. Kein Hold-Support. KEY_UP löst kein HID-Release aus. | `NONE` | nop |
**HID_CONSUMER:** ## execute_action_up() — Taste losgelassen (Hold-Ende)
```
usb_hid_send_consumer(usage_id)
usb_hid_release_consumer()
```
→ Kein Delay nötig (Consumer-Keys sind Edge-getriggert).
**HOST_COMMAND:** | ActionType | Verhalten |
`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, …). |---|---|
| `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 [03] im Slot: KEY_DOWN-Event vom Board → execute_action_down() → HID Key-Down senden
if keycode == 0: abbrechen [Taste bleibt physisch gedrückt...]
usb_hid_send_key(keycode, modifier) KEY_UP-Event vom Board → execute_action_up() → HID Key-Up senden
delay(10 ms)
usb_hid_release_key()
delay(20 ms) // Pause damit der Host den Step verarbeiten kann
``` ```
## 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()` Encoder-Bewegungen sind diskret (kein Halten möglich) und verwenden das **Tap-Modell**:
- `KEY_UP``CButton.on_release()` (aktuell leer)
- `ENC_CW/CCW``execute_action(m_enc_cw/ccw[enc_id])`
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. ## Work-Loop-Reihenfolge
- **HOST_COMMAND KEY_UP**: Board sendet derzeit kein `USB_EVT_KEY_UP` für KEY_UP-Events (nur KEY_DOWN wird gemeldet).
```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()`

View File

@ -32,7 +32,7 @@ Beide Rows sind im Linkerscript vom Code-Bereich ausgeschlossen.
## CRC16-CCITT ## CRC16-CCITT
- Polynom: `0x1021`, Init: `0xFFFF` - Polynom: `0x1021`, Init: `0xFFFF`
- Berechnet über Bytes 7222 (ab `mx_actions`, nach dem `crc`-Feld selbst) - Berechnet über Bytes 7248 (ab `mx_actions`, nach dem `crc`-Feld selbst)
- Sichert Datenintegrität nach NVM-Schreiben und bei Versionswechsel - Sichert Datenintegrität nach NVM-Schreiben und bei Versionswechsel
## Lese-Logik ## Lese-Logik

View File

@ -64,25 +64,13 @@ CButton::CButton()
void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base) void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base)
{ {
m_key_id = key_id; m_key_id = key_id;
m_led_index = led_index; m_led_index = led_index;
m_action = action; m_action = action;
m_base = base; m_base = base;
m_override_active = false; m_override_active = false;
m_anim = LEDAnim::STATIC; m_anim = LEDAnim::STATIC;
m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben 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
} }
// ── LED Layer 1: base ───────────────────────────────────────────────────────── // ── LED Layer 1: base ─────────────────────────────────────────────────────────

View File

@ -61,13 +61,8 @@ public:
CButton(); CButton();
// Initialisierung (ersetzt Konstruktor-Parameter). // Initialisierung (ersetzt Konstruktor-Parameter).
// led_index = -1 → kein LED (Encoder-SW-Buttons). void init(uint8_t key_id, int8_t led_index, SAction action,
void init(uint8_t key_id, int8_t led_index, SAction action, RGB base = RGB()); RGB base = RGB());
// Hooks für Tastendruck/-loslassen.
// Reserviert für zukünftige Logik (Hold-Aktionen, Toggle-Modus, …).
void on_press();
void on_release();
// ── LED Layer 1: base ───────────────────────────────────────────────────── // ── LED Layer 1: base ─────────────────────────────────────────────────────
// Idle-Farbe, aus NVM geladen oder von Windows-App gesetzt. // Idle-Farbe, aus NVM geladen oder von Windows-App gesetzt.

View File

@ -103,7 +103,7 @@ void CMainController::init_buttons()
// Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1) // Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1)
for (uint8_t enc = 0; enc < 4; enc++) { 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, // MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen,
@ -115,7 +115,7 @@ void CMainController::init_buttons()
uint8_t row = key % MATRIX_ROWS; uint8_t row = key % MATRIX_ROWS;
int8_t led = static_cast<int8_t>(LED_INDEX(col, row)); int8_t led = static_cast<int8_t>(LED_INDEX(col, row));
uint8_t mx_idx = key - 5; 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); m_buttons[key].init(key, led, cfg.mx_actions[mx_idx], base);
LEDAnim anim = static_cast<LEDAnim>(cfg.led_anim[mx_idx]); LEDAnim anim = static_cast<LEDAnim>(cfg.led_anim[mx_idx]);
@ -142,10 +142,10 @@ void CMainController::init_buttons()
void CMainController::work() void CMainController::work()
{ {
matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue
poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten
processEvents(); // 3. Queue leeren: Aktionen ausführen, Buttons benachrichtigen processEvents(); // 3. Queue leeren, Aktionen ausführen
updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show() updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
} }
// ─── Vendor-Kommunikation (PC → Board) ─────────────────────────────────────── // ─── Vendor-Kommunikation (PC → Board) ───────────────────────────────────────
@ -212,9 +212,9 @@ void CMainController::poll_vendor()
SDeviceConfig cfg; SDeviceConfig cfg;
nvm_config_load(cfg); // ungültige NVM → Defaults nvm_config_load(cfg); // ungültige NVM → Defaults
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg); const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg);
const uint8_t sz = sizeof(SDeviceConfig); // 163 const uint8_t sz = sizeof(SDeviceConfig); // 223
const uint8_t payload = 6; 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); 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. // Verarbeitet alle Events in der Queue bis sie leer ist.
// Reihenfolge: ältestes Event zuerst (FIFO). // Reihenfolge: ältestes Event zuerst (FIFO).
// //
// HOST_COMMAND-Aktionen werden zusätzlich über Serial an die Windows-App // KEY_DOWN: execute_action_down() HID-Taste wird gedrückt, bleibt aktiv bis KEY_UP.
// gemeldet die App entscheidet dann was passiert (URL öffnen, Programm starten…). // 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() void CMainController::processEvents()
{ {
SEvent ev; SEvent ev;
while (m_queue.pop(ev)) { while (m_queue.pop(ev)) {
switch (ev.type) { switch (ev.type) {
case EventType::KEY_DOWN: case EventType::KEY_DOWN:
if (ev.key_id < MATRIX_KEYS) { if (ev.key_id < MATRIX_KEYS)
m_buttons[ev.key_id].on_press(); execute_action_down(m_buttons[ev.key_id].action(), ev.key_id);
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);
}
break; break;
case EventType::KEY_UP: case EventType::KEY_UP:
if (ev.key_id < MATRIX_KEYS) 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; break;
case EventType::ENC_CW: case EventType::ENC_CW:
if (ev.key_id < 4) { if (ev.key_id < 4) {
execute_action(m_enc_cw[ev.key_id]); execute_action_down(m_enc_cw[ev.key_id], ev.key_id);
if (m_enc_cw[ev.key_id].type == ActionType::HOST_COMMAND) delay(10);
usb_serial_send(USB_EVT_ENC_CW, ev.key_id); execute_action_up(m_enc_cw[ev.key_id], ev.key_id);
} }
break; break;
case EventType::ENC_CCW: case EventType::ENC_CCW:
if (ev.key_id < 4) { if (ev.key_id < 4) {
execute_action(m_enc_ccw[ev.key_id]); execute_action_down(m_enc_ccw[ev.key_id], ev.key_id);
if (m_enc_ccw[ev.key_id].type == ActionType::HOST_COMMAND) delay(10);
usb_serial_send(USB_EVT_ENC_CCW, ev.key_id); execute_action_up(m_enc_ccw[ev.key_id], ev.key_id);
} }
break; break;
@ -362,35 +359,51 @@ void CMainController::processEvents()
} }
} }
// Führt eine einzelne Aktion aus. // ─── Aktions-Ausführung ───────────────────────────────────────────────────────
// 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 // execute_action_down(): Taste wird gedrückt (Hold-Start).
// usb_serial_send() an die Windows-App weitergeleitet. // HID_KEY: sendet Key-Down, bleibt aktiv.
void CMainController::execute_action(SAction action) // 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) { switch (action.type) {
case ActionType::HID_KEY: case ActionType::HID_KEY:
{
// data-Encoding: Low-Byte = Keycode, High-Byte = Modifier // data-Encoding: Low-Byte = Keycode, High-Byte = Modifier
usb_hid_send_key(static_cast<uint8_t>(action.data & 0xFF), uint8_t keycode = static_cast<uint8_t>(action.data & 0xFF);
static_cast<uint8_t>(action.data >> 8)); uint8_t modifier = static_cast<uint8_t>(action.data >> 8);
delay(10); // Host braucht kurz Zeit zwischen Key-Down und Key-Up usb_hid_send_key(keycode, modifier);
usb_hid_release_key(); // Taste bleibt gedrückt bis execute_action_up() aufgerufen wird
break; break;
}
case ActionType::HID_CONSUMER: case ActionType::HID_CONSUMER:
{
usb_hid_send_consumer(action.data); usb_hid_send_consumer(action.data);
usb_hid_release_consumer(); // Consumer-Control bleibt aktiv bis execute_action_up() aufgerufen wird
break; break;
}
case ActionType::HOST_COMMAND: 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; break;
case ActionType::MACRO: case ActionType::MACRO:
{ {
// Makro-Slot aus dem RAM ausführen (bei setup() aus NVM geladen). // Makros sind Sequenzen Steps mit keycode=0 werden übersprungen;
// Steps mit keycode=0 werden übersprungen; erstes leeres Step stoppt. // erstes leeres Step stoppt.
uint8_t slot = static_cast<uint8_t>(action.data); uint8_t slot = static_cast<uint8_t>(action.data);
if (slot >= MACRO_SLOTS) break; if (slot >= MACRO_SLOTS) break;
for (uint8_t i = 0; i < MACRO_MAX_STEPS; i++) { 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 ──────────────────────────────────────────────────────────── // ─── LED-Rendering ────────────────────────────────────────────────────────────
// //
// Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt // Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt

View File

@ -35,11 +35,12 @@ private:
SAction m_enc_cw[4]; SAction m_enc_cw[4];
SAction m_enc_ccw[4]; SAction m_enc_ccw[4];
void init_buttons(); // Buttons aus NVM-Config initialisieren void init_buttons(); // Buttons aus NVM-Config initialisieren
void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten
void processEvents(); // Queue leeren, Aktionen ausführen void processEvents(); // Queue leeren, Aktionen ausführen
void execute_action(SAction); // Einzelne Aktion ausführen (HID / Serial) void execute_action_down(SAction action, uint8_t key_id); // Taste drücken (Hold-Start)
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben void execute_action_up(SAction action, uint8_t key_id); // Taste losgelassen (Hold-Ende)
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben
// ── Config-Empfangspuffer ───────────────────────────────────────────────── // ── Config-Empfangspuffer ─────────────────────────────────────────────────
uint8_t m_cfg_buf[223]; // sizeof(SDeviceConfig) = 223 Bytes uint8_t m_cfg_buf[223]; // sizeof(SDeviceConfig) = 223 Bytes

View File

@ -106,8 +106,10 @@ bool nvm_config_load(SDeviceConfig& cfg)
// ── Speichern ───────────────────────────────────────────────────────────────── // ── Speichern ─────────────────────────────────────────────────────────────────
void nvm_config_save(const SDeviceConfig& cfg) void nvm_config_save(const SDeviceConfig& cfg)
{ {
// Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist // Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist.
uint8_t row[256]; // __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)); memset(row, 0xFF, sizeof(row));
memcpy(row, &cfg, sizeof(cfg)); memcpy(row, &cfg, sizeof(cfg));

View File

@ -21,7 +21,7 @@
// Gesamt genutzt: 223 Bytes (sizeof SDeviceConfig mit packed SAction) // Gesamt genutzt: 223 Bytes (sizeof SDeviceConfig mit packed SAction)
#define NVM_CONFIG_MAGIC 0x56503202UL #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[]) // Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[])
// Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW // Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW