Initial commit

This commit is contained in:
Julian Appel 2026-03-29 14:47:13 +02:00
commit b49984b9c0
32 changed files with 2394 additions and 0 deletions

353
src/CMainController.cpp Normal file
View file

@ -0,0 +1,353 @@
// 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();
}