// 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 // ─── 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). // CEventQueue::push() ist interrupt-sicher (kein Heap, atomare Indizes auf M0+). 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) { memset(m_cfg_buf, 0, sizeof(m_cfg_buf)); } void CMainController::setup() { 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 // 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]); } // MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen, // Aktion + Base-Farbe aus NVM. // mx_actions[0] ↔ key_id 5 (COL_1/ROW_0), mx_actions[19] ↔ key_id 24 (COL_4/ROW_4) // // Idle-Animation: Regenbogen-Sweep über alle 20 LEDs. // Jede LED bekommt einen gleichmäßigen Hue-Versatz (phase = idx * period / 20), // sodass immer ein voller Regenbogen auf dem Pad liegt und sich langsam dreht. const uint16_t k_rainbow_period = 4000; // 4s pro volle Runde 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; 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); // Phase gleichmäßig verteilen: LED 0 = Hue 0, LED 19 = Hue ~242 (fast voll) uint16_t phase = (uint16_t)((uint32_t)mx_idx * k_rainbow_period / 20); m_buttons[key].set_anim(LEDAnim::COLOR_CYCLE, k_rainbow_period, phase); } // 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] = cfg.enc_actions[enc][ENC_ACTION_CW]; m_enc_ccw[enc] = cfg.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, Buttons benachrichtigen updateLEDs(); // 4. 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)) { uint8_t count = (uint8_t)(sizeof(m_cfg_buf) - offset); if (count > 6) count = 6; 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 uint8_t sz = sizeof(SDeviceConfig); // 163 const uint8_t payload = 6; uint8_t chunks = (sz + payload - 1) / payload; // 28 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; uint8_t offset = 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)) { 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); // Fehler melden } } break; default: break; } } } // ─── Event-Verarbeitung ─────────────────────────────────────────────────────── // // 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…). 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); } break; case EventType::KEY_UP: if (ev.key_id < MATRIX_KEYS) m_buttons[ev.key_id].on_release(); 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); } 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); } break; default: break; } } } // 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) { 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(); break; case ActionType::HID_CONSUMER: usb_hid_send_consumer(action.data); usb_hid_release_consumer(); break; case ActionType::HOST_COMMAND: // Wird in processEvents() über Serial gesendet break; case ActionType::NONE: default: 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(); }