forked from jappel/VersaMCU
353 lines
14 KiB
C++
353 lines
14 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 <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:
|
||
// 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<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();
|
||
}
|