forked from jappel/VersaMCU
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>
746 lines
28 KiB
C++
746 lines
28 KiB
C++
// 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:
|
||
// 0–3 : Encoder-SW-Buttons (COL_0 × ROW_0–3), kein LED
|
||
// 4 : nicht belegt (COL_0 × ROW_4)
|
||
// 5–24 : Cherry MX Buttons (COL_1–4 × ROW_0–4), 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 0–2 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(¯os, 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();
|
||
}
|