// 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 #include #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; s_queue->push(ev); } // Wird von handle_encoder() aufgerufen – läuft im ISR-Kontext (EIC-Interrupt). // Die Queue vermeidet den Heap, schützt den gemischten Matrix-/ISR-Producerfall // aber aktuell nicht mit einer Critical Section. 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_macro_chunks_expected(0) , m_macro_receiving(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_macro_buf, 0, sizeof(m_macro_buf)); memset(&m_macros, 0, sizeof(m_macros)); } 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(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(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 { 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; // 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; memset(m_cfg_buf, 0, sizeof(m_cfg_buf)); break; case USB_CMD_CONFIG_DATA: if (m_cfg_receiving) { // 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen uint16_t offset = (uint16_t)pkt.key_id() * 6; if (offset < sizeof(m_cfg_buf)) { uint16_t remaining = (uint16_t)(sizeof(m_cfg_buf) - offset); uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining); memcpy(m_cfg_buf + offset, &pkt.data[2], count); } } 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(&cfg); const uint16_t sz = sizeof(SDeviceConfig); // 740 const uint8_t payload = 6; uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 124 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: if (m_cfg_receiving) { m_cfg_receiving = false; SDeviceConfig cfg; memcpy(&cfg, m_cfg_buf, sizeof(cfg)); if (cfg.magic == NVM_CONFIG_MAGIC && cfg.version == NVM_CONFIG_VERSION && cfg.crc == nvm_config_crc(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 } } break; // ── Makro-Übertragung: BEGIN → n×DATA → COMMIT ────────────────── case USB_CMD_MACRO_BEGIN: m_macro_chunks_expected = pkt.key_id(); m_macro_receiving = true; memset(m_macro_buf, 0, sizeof(m_macro_buf)); break; case USB_CMD_MACRO_DATA: if (m_macro_receiving) { uint16_t offset = (uint16_t)pkt.key_id() * 6; if (offset < sizeof(m_macro_buf)) { uint16_t remaining = (uint16_t)(sizeof(m_macro_buf) - offset); uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining); memcpy(m_macro_buf + offset, &pkt.data[2], count); } } break; case USB_CMD_MACRO_COMMIT: if (m_macro_receiving) { m_macro_receiving = false; memcpy(&m_macros, m_macro_buf, sizeof(m_macros)); if (macro_config_save(m_macros)) { usb_serial_send(USB_EVT_MACRO_ACK, 0); } else { usb_serial_send(USB_EVT_MACRO_NACK, 0); // NVM-Timeout } } break; // ── Makro-Dump anfordern ───────────────────────────────────────── case USB_CMD_MACRO_READ: { const uint8_t* raw = reinterpret_cast(&m_macros); const uint16_t sz = sizeof(SMacroTable); // 512 const uint8_t payload = 6; uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 86 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: 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) { 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_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_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; 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_key(); usb_hid_release_consumer(); 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: aktuell keine Ausgabe auf Release. // 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(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); // Consumer-Control bleibt aktiv bis execute_action_up() aufgerufen wird break; } case ActionType::HOST_COMMAND: // Windows-App übernimmt Ausführung; KEY_DOWN-Event senden usb_serial_send(USB_EVT_KEY_DOWN, key_id); break; case ActionType::MACRO: { // 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++) { 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(); 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(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: usb_hid_release_key(); break; case ActionType::HID_CONSUMER: usb_hid_release_consumer(); break; case ActionType::HOST_COMMAND: // USB_EVT_KEY_UP ist definiert, wird aktuell aber nicht gesendet. 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 // 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(); }