VersaMCU/src/CMainController.cpp
cjjohn 13c3e41c91 Add lightweight READ_STATUS command to avoid blocking LED updates during polling
CONFIG_READ was being (ab)used by the Windows viewer's Live-Sync feature
to poll just the active profile every 1.5s, but the handler sends the
full 740-byte config as ~124 blocking chunk packets from inside
poll_vendor() -- which runs before updateLEDs() in the same loop
iteration (see the loop-order comment at the top of
CMainController.cpp). Every poll cycle stalled updateLEDs() long enough
that running Pulse/Blink animations visibly stuttered, since their
brightness is computed from an absolute millis() timestamp and jumps
forward once the stall clears instead of catching up smoothly.

Added USB_CMD_READ_STATUS (0x06) / USB_EVT_STATUS (0x86): a single NVM
read (no serial I/O) and one 8-byte reply packet with the active
profile in Data[1], no chunking. Documented in
doc/07_serial_protocol.md alongside why CONFIG_READ is unsuitable for
polling. CONFIG_READ stays as-is for actual full-dump use (e.g. "Vom
Board laden").

Verified on hardware after flashing via env:versapad_usb: READ_STATUS
returns the correct profile in ~well under CONFIG_READ's dump time,
Live-Sync no longer visibly disturbs LED animations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 15:37:21 +02:00

746 lines
28 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// CMainController.cpp
// Zentraler Orchestrator des VersaPad v2.
//
// Aufgaben:
// 1. Alle Hardware-Peripherie initialisieren (Matrix, Encoder, USB)
// 2. Pro Loop-Durchlauf:
// a) matrix_scan() → Callback → Events in Queue
// b) Encoder-ISRs laufen asynchron → Events in Queue
// c) poll_vendor() → eingehende Serial-Pakete (PC→Board) direkt verarbeiten
// d) processEvents() → Queue leeren, Aktionen ausführen
// e) updateLEDs() → dirty CButtons in WS2812-Buffer schreiben + show()
//
// Datenfluss:
// HAL (matrix_cb / encoder_cb)
// └─► CEventQueue
// └─► processEvents()
// ├─► CButton.on_press() / on_release()
// ├─► execute_action() → USB HID / Serial
// └─► usb_serial_send() (nur bei HOST_COMMAND)
//
// SerialUSB (PC→Board)
// └─► poll_vendor()
// └─► CButton.set_override() / set_base() / clear_override()
#include <Arduino.h>
#include <string.h>
#include "CMainController.h"
#include "hal/ws2812.h"
#include "hal/matrix.h"
#include "hal/encoder.h"
#include "hal/usb_serial.h"
#include "config/pins.h"
#include "config/nvm_config.h"
#include "config/macro_config.h"
static constexpr uint8_t FACTORY_RESET_LEFT_KEY = 9;
static constexpr uint8_t FACTORY_RESET_RIGHT_KEY = 24;
static constexpr uint32_t FACTORY_RESET_HOLD_MS = 5000;
// ─── Static Bridge: HAL-Callbacks → EventQueue ───────────────────────────────
//
// matrix_init() und encoder_init() erwarten einfache Funktionszeiger (kein
// Lambda mit Capture möglich auf Cortex-M0+). Der Queue-Pointer wird einmalig
// in setup() gesetzt, bevor die Callbacks registriert werden.
static CEventQueue* s_queue = nullptr;
// Wird von matrix_scan() aufgerufen wenn sich ein Tasten-Zustand ändert.
// Läuft im Loop-Kontext (kein ISR).
static void matrix_cb(uint8_t key, bool pressed)
{
if (!s_queue) return;
SEvent ev;
ev.type = pressed ? EventType::KEY_DOWN : EventType::KEY_UP;
ev.key_id = key;
ev.payload = 0;
// Encoder-ISRs benutzen dieselbe Queue. Den Loop-Producer kurz gegen einen
// dazwischenlaufenden ISR-Push schützen; Encoder-Pushes selbst laufen
// bereits mit maskierten gleichpriorisierten Interrupts.
noInterrupts();
s_queue->push(ev);
interrupts();
}
// Wird von handle_encoder() aufgerufen läuft im ISR-Kontext (EIC-Interrupt).
// Der Matrix-Producer maskiert Interrupts während seines Queue-Pushs.
static void encoder_cb(uint8_t enc, int8_t dir)
{
if (!s_queue) return;
SEvent ev;
ev.type = (dir > 0) ? EventType::ENC_CW : EventType::ENC_CCW;
ev.key_id = enc;
ev.payload = 0;
s_queue->push(ev);
}
// ─── Konstruktor / Setup ──────────────────────────────────────────────────────
CMainController::CMainController()
: m_cfg_chunks_expected(0)
, m_cfg_receiving(false)
, m_cfg_transfer_valid(false)
, m_macro_chunks_expected(0)
, m_macro_receiving(false)
, m_macro_transfer_valid(false)
, m_factory_left_held(false)
, m_factory_right_held(false)
, m_factory_reset_armed(false)
, m_factory_reset_done(false)
, m_factory_hold_started_ms(0)
{
memset(m_cfg_buf, 0, sizeof(m_cfg_buf));
memset(m_cfg_received, 0, sizeof(m_cfg_received));
memset(m_macro_buf, 0, sizeof(m_macro_buf));
memset(m_macro_received, 0, sizeof(m_macro_received));
memset(&m_macros, 0, sizeof(m_macros));
}
bool CMainController::all_chunks_received(
const uint8_t* received, uint8_t count)
{
for (uint8_t i = 0; i < count; i++) {
if (received[i] == 0) return false;
}
return true;
}
void CMainController::setup()
{
macro_config_load(m_macros); // Makro-Tabelle aus NVM laden (oder leere Tabelle)
init_buttons(); // Buttons aus NVM laden (oder Defaults)
s_queue = &m_queue; // Queue-Pointer setzen bevor Callbacks registriert werden
usb_hid_init(); // HID-Descriptor registriert sich via globalem Konstruktor,
// usb_hid_init() ist hier ein No-Op aber verdeutlicht die Abhängigkeit
usb_serial_init(); // CDC Serial öffnen
matrix_init(matrix_cb); // Matrix-Scan initialisieren + Callback registrieren
encoder_init(encoder_cb);// EIC-Interrupts für alle 4 Encoder einrichten
}
// Lädt Config aus NVM und initialisiert alle CButton-Instanzen.
// key_id-Mapping:
// 03 : Encoder-SW-Buttons (COL_0 × ROW_03), kein LED
// 4 : nicht belegt (COL_0 × ROW_4)
// 524 : Cherry MX Buttons (COL_14 × ROW_04), je ein WS2812-LED
void CMainController::init_buttons()
{
SDeviceConfig cfg;
bool valid = nvm_config_load(cfg);
(void)valid; // false = keine gültige Config → Defaults wurden bereits geladen
// Aktives Profil auswählen (load() sichert bereits 02 ab)
const SDeviceProfile& prof = cfg.profiles[cfg.active_profile];
// 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, prof.enc_actions[enc][ENC_ACTION_SW], RGB());
}
// MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen,
// Aktion + Base-Farbe + Animation aus aktivem Profil.
// mx_actions[0] ↔ key_id 5 (COL_1/ROW_0), mx_actions[19] ↔ key_id 24 (COL_4/ROW_4)
for (uint8_t key = 5; key < MATRIX_KEYS; key++) {
uint8_t col = key / MATRIX_ROWS;
uint8_t row = key % MATRIX_ROWS;
int8_t led = static_cast<int8_t>(LED_INDEX(col, row));
uint8_t mx_idx = key - 5;
// Effektive Farbe = base × led_brightness × global_brightness / 255²
auto scale = [&](uint8_t val) -> uint8_t {
return (uint8_t)((uint32_t)val
* prof.led_brightness[mx_idx] / 255
* cfg.global_brightness / 255);
};
RGB base(scale(prof.led_r[mx_idx]),
scale(prof.led_g[mx_idx]),
scale(prof.led_b[mx_idx]));
m_buttons[key].init(key, led, prof.mx_actions[mx_idx], base);
LEDAnim anim = static_cast<LEDAnim>(prof.led_anim[mx_idx]);
uint16_t period = prof.led_period_ms[mx_idx] > 0 ? prof.led_period_ms[mx_idx] : 4000;
if (anim == LEDAnim::COLOR_CYCLE) {
// Phase gleichmäßig verteilen → stehender Regenbogen dreht sich
uint16_t phase = (uint16_t)((uint32_t)mx_idx * period / 20);
m_buttons[key].set_anim(LEDAnim::COLOR_CYCLE, period, phase);
} else if (anim == LEDAnim::COLOR_FADE) {
// Config enthält nur eine Ziel-/Base-Farbe. COLOR_FADE wird beim
// Laden daher eindeutig als einmaliges Schwarz→Base interpretiert.
m_buttons[key].set_base(RGB(0, 0, 0));
m_buttons[key].set_color_fade(base, period);
} else {
m_buttons[key].set_anim(anim, period);
}
}
// Encoder CW/CCW-Aktionen separat merken Encoder haben kein CButton-Objekt
// da sie keine LED haben und kein Matrix-Key sind.
for (uint8_t enc = 0; enc < 4; enc++) {
m_enc_cw [enc] = prof.enc_actions[enc][ENC_ACTION_CW];
m_enc_ccw[enc] = prof.enc_actions[enc][ENC_ACTION_CCW];
}
}
// ─── Haupt-Loop ───────────────────────────────────────────────────────────────
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
check_factory_reset();// 4. Long-Press-Kombination für Werksreset prüfen
updateLEDs(); // 5. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
}
// ─── Vendor-Kommunikation (PC → Board) ───────────────────────────────────────
//
// Die Windows-App sendet 8-Byte-Pakete über den CDC Serial-Port.
// poll_vendor() holt alle verfügbaren vollständigen Pakete ab und
// wendet die Kommandos direkt auf die CButton-Instanzen an.
// LED-Änderungen werden beim nächsten updateLEDs()-Aufruf sichtbar.
void CMainController::poll_vendor()
{
SerialPacket pkt;
while (usb_serial_poll(pkt)) {
switch (pkt.command()) {
// Override-LED setzen: Button leuchtet in der angegebenen Farbe,
// bis clear_override() aufgerufen wird (z.B. Benachrichtigung)
case USB_CMD_SET_LED_OVERRIDE:
if (pkt.key_id() < MATRIX_KEYS)
m_buttons[pkt.key_id()].set_override(RGB(pkt.r(), pkt.g(), pkt.b()));
break;
// Override-LED löschen: Button kehrt zur konfigurierten Base-Farbe zurück
case USB_CMD_CLEAR_LED_OVERRIDE:
if (pkt.key_id() < MATRIX_KEYS)
m_buttons[pkt.key_id()].clear_override();
break;
// Base-LED setzen: dauerhaft neue Idle-Farbe (wird nicht in NVM geschrieben)
case USB_CMD_SET_LED_BASE:
if (pkt.key_id() < MATRIX_KEYS)
m_buttons[pkt.key_id()].set_base(RGB(pkt.r(), pkt.g(), pkt.b()));
break;
// Ping sofortige Antwort zum Testen der Verbindung
case USB_CMD_PING:
usb_serial_send(USB_EVT_PONG, 0);
break;
// Leichtgewichtiger Status (aktuell nur aktives Profil) ein NVM-Read,
// ein 8-Byte-Paket zurück. Für Polling (Live-Sync) statt CONFIG_READ,
// das für den Dump ~124 blockierende Pakete braucht und dabei
// updateLEDs() so lange verzögert, dass Pulse-Animationen sichtbar stottern.
case USB_CMD_READ_STATUS:
{
SDeviceConfig cfg;
nvm_config_load(cfg);
usb_serial_send(USB_EVT_STATUS, cfg.active_profile);
break;
}
// Config-Übertragung: BEGIN → n×DATA → COMMIT
case USB_CMD_CONFIG_BEGIN:
// Neuen Empfang starten bisherige Daten verwerfen
m_cfg_chunks_expected = pkt.key_id();
m_cfg_receiving = true;
m_cfg_transfer_valid = (m_cfg_chunks_expected == CONFIG_CHUNKS);
memset(m_cfg_buf, 0, sizeof(m_cfg_buf));
memset(m_cfg_received, 0, sizeof(m_cfg_received));
break;
case USB_CMD_CONFIG_DATA:
if (m_cfg_receiving) {
// 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen
uint8_t chunk = pkt.key_id();
uint16_t offset = (uint16_t)chunk * SERIAL_PAYLOAD_BYTES;
if (m_cfg_transfer_valid &&
chunk < CONFIG_CHUNKS &&
m_cfg_received[chunk] == 0 &&
offset < sizeof(m_cfg_buf))
{
uint16_t remaining = (uint16_t)(sizeof(m_cfg_buf) - offset);
uint8_t count = (uint8_t)(
remaining > SERIAL_PAYLOAD_BYTES
? SERIAL_PAYLOAD_BYTES
: remaining);
memcpy(m_cfg_buf + offset, &pkt.data[2], count);
m_cfg_received[chunk] = 1;
} else {
m_cfg_transfer_valid = false;
}
}
break;
// Config-Dump anfordern: Board sendet NVM-Config in 6-Byte-Chunks
// zurück an die App (gleiche Chunk-Struktur wie beim Schreiben).
case USB_CMD_CONFIG_READ:
{
SDeviceConfig cfg;
nvm_config_load(cfg); // ungültige NVM → Defaults
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg);
const uint16_t sz = sizeof(SDeviceConfig); // 740
const uint8_t payload = SERIAL_PAYLOAD_BYTES;
const uint8_t chunks = CONFIG_CHUNKS;
usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks);
for (uint8_t i = 0; i < chunks; i++) {
uint8_t p[SERIAL_PKT_SIZE] = {};
p[0] = USB_EVT_CONFIG_DATA;
p[1] = i;
uint16_t offset = (uint16_t)i * payload;
for (uint8_t b = 0; b < payload; b++) {
if (offset + b < sz) p[2 + b] = raw[offset + b];
}
if (SerialUSB) SerialUSB.write(p, SERIAL_PKT_SIZE);
}
usb_serial_send(USB_EVT_CONFIG_END, chunks);
break;
}
case USB_CMD_CONFIG_COMMIT:
{
bool complete =
m_cfg_receiving &&
m_cfg_transfer_valid &&
all_chunks_received(m_cfg_received, CONFIG_CHUNKS);
m_cfg_receiving = false;
if (complete) {
SDeviceConfig cfg;
memcpy(&cfg, m_cfg_buf, sizeof(cfg));
if (nvm_config_validate(cfg)) {
if (nvm_config_save(cfg)) {
init_buttons();
usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden
} else {
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // NVM-Timeout
}
}
else
{
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // CRC/Magic-Fehler
}
} else {
usb_serial_send(USB_EVT_CONFIG_NACK, 0);
}
break;
}
// ── Makro-Übertragung: BEGIN → n×DATA → COMMIT ──────────────────
case USB_CMD_MACRO_BEGIN:
m_macro_chunks_expected = pkt.key_id();
m_macro_receiving = true;
m_macro_transfer_valid = (m_macro_chunks_expected == MACRO_CHUNKS);
memset(m_macro_buf, 0, sizeof(m_macro_buf));
memset(m_macro_received, 0, sizeof(m_macro_received));
break;
case USB_CMD_MACRO_DATA:
if (m_macro_receiving) {
uint8_t chunk = pkt.key_id();
uint16_t offset =
(uint16_t)chunk * SERIAL_PAYLOAD_BYTES;
if (m_macro_transfer_valid &&
chunk < MACRO_CHUNKS &&
m_macro_received[chunk] == 0 &&
offset < sizeof(m_macro_buf))
{
uint16_t remaining = (uint16_t)(sizeof(m_macro_buf) - offset);
uint8_t count = (uint8_t)(
remaining > SERIAL_PAYLOAD_BYTES
? SERIAL_PAYLOAD_BYTES
: remaining);
memcpy(m_macro_buf + offset, &pkt.data[2], count);
m_macro_received[chunk] = 1;
} else {
m_macro_transfer_valid = false;
}
}
break;
case USB_CMD_MACRO_COMMIT:
{
bool complete =
m_macro_receiving &&
m_macro_transfer_valid &&
all_chunks_received(m_macro_received, MACRO_CHUNKS);
m_macro_receiving = false;
SMacroTable incoming;
if (complete) {
memcpy(&incoming, m_macro_buf, sizeof(incoming));
}
if (complete &&
macro_config_validate(incoming) &&
macro_config_save(incoming))
{
m_macros = incoming;
usb_serial_send(USB_EVT_MACRO_ACK, 0);
} else {
usb_serial_send(USB_EVT_MACRO_NACK, 0);
}
break;
}
// ── Makro-Dump anfordern ─────────────────────────────────────────
case USB_CMD_MACRO_READ:
{
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&m_macros);
const uint16_t sz = sizeof(SMacroTable); // 512
const uint8_t payload = SERIAL_PAYLOAD_BYTES;
const uint8_t chunks = MACRO_CHUNKS;
usb_serial_send(USB_EVT_MACRO_BEGIN, chunks);
for (uint8_t i = 0; i < chunks; i++) {
uint8_t p[SERIAL_PKT_SIZE] = {};
p[0] = USB_EVT_MACRO_DATA;
p[1] = i;
uint16_t offset = (uint16_t)i * payload;
for (uint8_t b = 0; b < payload; b++) {
if (offset + b < sz) p[2 + b] = raw[offset + b];
}
if (SerialUSB) SerialUSB.write(p, SERIAL_PKT_SIZE);
}
usb_serial_send(USB_EVT_MACRO_END, chunks);
break;
}
default:
break;
}
}
}
// ─── Event-Verarbeitung ───────────────────────────────────────────────────────
//
// Verarbeitet alle Events in der Queue bis sie leer ist.
// Reihenfolge: ältestes Event zuerst (FIFO).
//
// 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: Host-Event mit Richtung oder HID-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) {
update_factory_reset_hold(ev.key_id, true);
if (!(is_factory_reset_combo_active() && is_factory_reset_key(ev.key_id))) {
execute_action_down(m_buttons[ev.key_id].action(), ev.key_id);
}
}
break;
case EventType::KEY_UP:
if (ev.key_id < MATRIX_KEYS) {
bool suppress = is_factory_reset_combo_active() && is_factory_reset_key(ev.key_id);
update_factory_reset_hold(ev.key_id, false);
if (!suppress) {
execute_action_up(m_buttons[ev.key_id].action(), ev.key_id);
}
}
break;
case EventType::ENC_CW:
if (ev.key_id < 4) {
execute_encoder_action(
m_enc_cw[ev.key_id], ev.key_id, USB_EVT_ENC_CW);
}
break;
case EventType::ENC_CCW:
if (ev.key_id < 4) {
execute_encoder_action(
m_enc_ccw[ev.key_id], ev.key_id, USB_EVT_ENC_CCW);
}
break;
default:
break;
}
}
}
bool CMainController::is_factory_reset_key(uint8_t key_id) const
{
return key_id == FACTORY_RESET_LEFT_KEY || key_id == FACTORY_RESET_RIGHT_KEY;
}
bool CMainController::is_factory_reset_combo_active() const
{
return m_factory_left_held && m_factory_right_held;
}
void CMainController::update_factory_reset_led_feedback()
{
// Einzelne Reset-Taste gehalten: diese Taste rot hervorheben.
// Beide gehalten: beide Tasten rot hervorheben.
if (m_factory_left_held) {
m_buttons[FACTORY_RESET_LEFT_KEY].set_override(RGB(96, 0, 0));
} else {
m_buttons[FACTORY_RESET_LEFT_KEY].clear_override();
}
if (m_factory_right_held) {
m_buttons[FACTORY_RESET_RIGHT_KEY].set_override(RGB(96, 0, 0));
} else {
m_buttons[FACTORY_RESET_RIGHT_KEY].clear_override();
}
}
void CMainController::update_factory_reset_hold(uint8_t key_id, bool pressed)
{
if (key_id == FACTORY_RESET_LEFT_KEY) {
m_factory_left_held = pressed;
} else if (key_id == FACTORY_RESET_RIGHT_KEY) {
m_factory_right_held = pressed;
} else {
return;
}
update_factory_reset_led_feedback();
if (m_factory_left_held && m_factory_right_held) {
if (!m_factory_reset_armed) {
// Sobald beide Reset-Tasten gleichzeitig gehalten werden, sollen sie
// keine normale Aktion mehr auf dem Host auslösen. Falls die zuerst
// gedrückte Taste bereits ein HID-/Consumer-Hold gestartet hat,
// geben wir sie hier sofort wieder frei.
execute_action_up(m_buttons[FACTORY_RESET_LEFT_KEY].action(), FACTORY_RESET_LEFT_KEY);
execute_action_up(m_buttons[FACTORY_RESET_RIGHT_KEY].action(), FACTORY_RESET_RIGHT_KEY);
m_factory_reset_armed = true;
m_factory_reset_done = false;
m_factory_hold_started_ms = millis();
}
} else {
m_factory_reset_armed = false;
m_factory_reset_done = false;
m_factory_hold_started_ms = 0;
}
}
void CMainController::check_factory_reset()
{
if (!m_factory_reset_armed || m_factory_reset_done) return;
if (!(m_factory_left_held && m_factory_right_held)) return;
if ((millis() - m_factory_hold_started_ms) >= FACTORY_RESET_HOLD_MS) {
m_factory_reset_done = true;
perform_factory_reset();
}
}
void CMainController::perform_factory_reset()
{
SDeviceConfig cfg;
SMacroTable macros;
bool cfg_ok;
bool macro_ok;
nvm_config_defaults(cfg);
memset(&macros, 0, sizeof(macros));
cfg_ok = nvm_config_save(cfg);
macro_ok = macro_config_save(macros);
// Laufzeit-Zustand immer an die Defaults angleichen selbst wenn NVM gerade
// nicht geschrieben werden konnte, sieht das Gerät sofort wieder "frisch" aus.
m_macros = macros;
usb_hid_release_all_keys();
usb_hid_release_all_consumers();
init_buttons();
show_factory_reset_feedback();
// Während die beiden Tasten weiter gehalten werden, keine erneuten Resets.
// Neue Arming-Phase erst nach vollständigem Loslassen beider Tasten.
if (!(cfg_ok && macro_ok)) {
// Keine Host-Meldung vorgesehen das Gerät bleibt aber betriebsfähig
// und kann über die GUI erneut konfiguriert werden.
}
}
void CMainController::show_factory_reset_feedback()
{
// Kurze rote Bestätigung ähnlich der Startsequenz, aber kompakter.
ws2812_fill(100, 0, 0);
ws2812_show();
delay(180);
ws2812_clear();
delay(90);
ws2812_fill(100, 0, 0);
ws2812_show();
delay(180);
ws2812_clear();
delay(60);
// Danach sofort die frisch geladenen Default-Animationen wieder anzeigen.
updateLEDs();
}
// ─── 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: sendet KEY_UP mit Command-ID.
// 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
uint8_t keycode = static_cast<uint8_t>(action.data & 0xFF);
uint8_t modifier = static_cast<uint8_t>(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);
// Consumer-Control bleibt aktiv bis execute_action_up() aufgerufen wird
break;
}
case ActionType::HOST_COMMAND:
// Command-ID little-endian in Byte 2/3 übertragen.
usb_serial_send(
USB_EVT_KEY_DOWN,
key_id,
static_cast<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(action.data >> 8));
break;
case ActionType::MACRO:
{
// Makros sind Sequenzen Steps mit keycode=0 werden übersprungen;
// erstes leeres Step stoppt.
uint8_t slot = static_cast<uint8_t>(action.data);
if (slot >= MACRO_SLOTS) break;
for (uint8_t i = 0; i < MACRO_MAX_STEPS; i++) {
const SMacroStep& s = m_macros.steps[slot][i];
if (s.keycode == 0) break;
usb_hid_send_key(s.keycode, s.modifier);
delay(10);
usb_hid_release_key(s.keycode, s.modifier);
delay(20); // Kurze Pause zwischen Steps damit der Host mitkommt
}
break;
}
case ActionType::PROFILE_SWITCH:
{
SDeviceConfig cfg;
nvm_config_load(cfg);
uint8_t target = static_cast<uint8_t>(action.data);
if (target == 0xFF)
target = (cfg.active_profile + 1) % 3; // Zyklus: 0→1→2→0
if (target > 2) break;
cfg.active_profile = target;
cfg.crc = nvm_config_crc(cfg); // CRC nach Änderung aktualisieren
if (nvm_config_save(cfg))
init_buttons();
// Bei NVM-Timeout: kein Profil-Wechsel (Config unverändert in NVM)
break;
}
case ActionType::NONE:
default:
break;
}
}
void CMainController::execute_action_up(SAction action, uint8_t key_id)
{
switch (action.type) {
case ActionType::HID_KEY:
{
uint8_t keycode = static_cast<uint8_t>(action.data & 0xFF);
uint8_t modifier = static_cast<uint8_t>(action.data >> 8);
usb_hid_release_key(keycode, modifier);
break;
}
case ActionType::HID_CONSUMER:
usb_hid_release_consumer(action.data);
break;
case ActionType::HOST_COMMAND:
usb_serial_send(
USB_EVT_KEY_UP,
key_id,
static_cast<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(action.data >> 8));
break;
case ActionType::MACRO:
case ActionType::NONE:
default:
// MACRO: Sequenz ist in execute_action_down() komplett abgelaufen, nop hier
// NONE: keine Aktion
break;
}
}
void CMainController::execute_encoder_action(
SAction action, uint8_t enc_id, uint8_t host_event)
{
if (action.type == ActionType::HOST_COMMAND) {
usb_serial_send(
host_event,
enc_id,
static_cast<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(action.data >> 8));
return;
}
execute_action_down(action, enc_id);
delay(10);
execute_action_up(action, enc_id);
}
// ─── LED-Rendering ────────────────────────────────────────────────────────────
//
// Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt
// ihre aktuelle Farbe (override wenn aktiv, sonst base) in den WS2812-Buffer.
// ws2812_show() wird nur aufgerufen wenn mindestens ein Button dirty war
// das vermeidet unnötige noInterrupts()-Aufrufe (~600µs Blockzeit).
void CMainController::updateLEDs()
{
bool dirty = false;
for (uint8_t i = 0; i < MATRIX_KEYS; i++) {
if (m_buttons[i].render_led())
dirty = true;
}
if (dirty)
ws2812_show();
}