VersaMCU/src/CMainController.cpp
2026-03-29 14:47:13 +02:00

353 lines
14 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 <string.h>
// ─── 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:
// 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
// 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<int8_t>(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<const uint8_t*>(&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<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(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();
}