From 59b3cb4dd1aec67be7c7ada8987ca507f174c316 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 29 Mar 2026 22:19:27 +0200 Subject: [PATCH] Added Macro functionality, updated readme --- README.md | 137 +++++++++++++++++++++++++++++++++--- src/CMainController.cpp | 100 ++++++++++++++++++++++---- src/CMainController.h | 18 +++-- src/config/action.h | 1 + src/config/macro_config.cpp | 66 +++++++++++++++++ src/config/macro_config.h | 36 ++++++++++ src/config/nvm_config.cpp | 6 ++ src/config/nvm_config.h | 14 ++-- src/hal/usb_serial.h | 9 +++ 9 files changed, 354 insertions(+), 33 deletions(-) create mode 100644 src/config/macro_config.cpp create mode 100644 src/config/macro_config.h diff --git a/README.md b/README.md index 43f5206..ddde9cd 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,108 @@ pio run --target upload Der Upload läuft via OpenOCD über SWD. Kein Bootloader nötig – der Chip wird direkt programmiert. +--- + +## Funktionsumfang (Anforderungskatalog) + +### 1 Hardware-Plattform + +| # | Anforderung | Status | +|---|-------------|--------| +| 1.1 | Ziel-MCU: **ATSAMD21G17D** (Cortex-M0+, 48 MHz, 128 KB Flash, 16 KB RAM) | ✅ | +| 1.2 | Framework: **Arduino + PlatformIO**, kein Bootloader (Direktflash via SWD/Atmel-ICE) | ✅ | +| 1.3 | USB-Enumeration ohne externen Quarz (`-DCRYSTALLESS`, DFLL48M nutzt USB-SOF als Referenz) | ✅ | +| 1.4 | Benutzerdefiniertes Board-Profil (`versapad_nobl.json`) mit korrekten Flash/RAM-Limits | ✅ | + +### 2 Tasten-Matrix + +| # | Anforderung | Status | +|---|-------------|--------| +| 2.1 | **5×5-Matrix-Scan** (25 Keys, davon 20 MX-Buttons + 4 Encoder-SW + 1 NC) | ✅ | +| 2.2 | **10 ms Software-Debounce** pro Taste (Flanken-Erkennung) | ✅ | +| 2.3 | KEY_DOWN- und KEY_UP-Events werden in die Event-Queue geschrieben | ✅ | +| 2.4 | Matrix-Scan im **Loop-Kontext** (kein ISR, kein Heap) | ✅ | + +### 3 Encoder + +| # | Anforderung | Status | +|---|-------------|--------| +| 3.1 | **4 Quadratur-Encoder** (A/B-Phasen per EIC-Interrupt) | ✅ | +| 3.2 | **Richtungserkennung**: CW / CCW via 2-Bit-Greycode-Auswertung | ✅ | +| 3.3 | Encoder-Events im ISR-Kontext direkt in Event-Queue (interrupt-sicher, kein Heap) | ✅ | +| 3.4 | Encoder-SW-Tasten über Matrix-Scan (gleicher Pfad wie MX-Buttons) | ✅ | + +### 4 Aktions-Engine + +| # | Anforderung | Status | +|---|-------------|--------| +| 4.1 | **ActionType NONE**: kein HID-Event beim Drücken | ✅ | +| 4.2 | **ActionType HID_KEY**: USB-HID-Tastendruck (Keycode + Modifier-Byte) | ✅ | +| 4.3 | **ActionType HID_CONSUMER**: USB Consumer Control (Play/Pause, Lautstärke …) | ✅ | +| 4.4 | **ActionType HOST_COMMAND**: Event-ID an VersaGUI senden, App führt aus | ✅ | +| 4.5 | **ActionType MACRO**: Sequenz aus bis zu 4 HID-Key-Schritten aus NVM-Tabelle abspielen | ✅ | +| 4.6 | Jede Aktion ausführbar **ohne laufende VersaGUI** (lokal per HID/Makro) | ✅ | + +### 5 Makro-System + +| # | Anforderung | Status | +|---|-------------|--------| +| 5.1 | **32 Makro-Slots**, je **4 Steps** (Keycode + Modifier-Byte) = 256 Byte gesamt | ✅ | +| 5.2 | Steps mit `keycode = 0` werden übersprungen (variable Makrolänge 1–4) | ✅ | +| 5.3 | Timing: 10 ms Key-Down-Dauer + 20 ms Pause zwischen Steps | ✅ | +| 5.4 | Makro-Tabelle in **separater NVM-Row** (Row 1, 0x1FF00, 256 Byte) | ✅ | +| 5.5 | Makro-Tabelle wird beim Start aus NVM geladen; gelöschter Flash (0xFF) → leere Tabelle | ✅ | + +### 6 LED-System (WS2812) + +| # | Anforderung | Status | +|---|-------------|--------| +| 6.1 | **20 WS2812-LEDs**, serpentiner Verdrahtung; Adafruit-NeoPixel bit-bang Treiber | ✅ | +| 6.2 | **2-Schicht-Modell** pro Button: `base` (Idle) + `override` (temporär von GUI) | ✅ | +| 6.3 | **STATIC**: feste Farbe aus NVM | ✅ | +| 6.4 | **BLINK**: binäres An/Aus mit konfigurierbarer Halbperiode | ✅ | +| 6.5 | **PULSE**: lineares Helligkeitsdreieck (0→255→0), kein Float | ✅ | +| 6.6 | **COLOR_CYCLE** (Regenbogen): Hue-Sweep über 6 Segmente, ignoriert base/override | ✅ | +| 6.7 | **COLOR_FADE**: einmaliger RGB-Crossfade zu Zielfarbe | ✅ | +| 6.8 | Phasenversatz beim Start: Regenbogen-LEDs sind gleichmäßig über die Periode verteilt | ✅ | +| 6.9 | `ws2812_show()` nur bei dirty-Flag aufgerufen (~600 µs Blockzeit vermieden) | ✅ | +| 6.10 | Alle Animationen in **Integer-Arithmetik** (kein FPU auf M0+) | ✅ | + +### 7 Konfigurations-Speicherung (NVM) + +| # | Anforderung | Status | +|---|-------------|--------| +| 7.1 | Config-Layout **Version 2**, 223 Byte packed, mit Magic `0x56503202` + CRC16-CCITT | ✅ | +| 7.2 | Config gespeichert in **NVM Row 0** (0x1FE00, 256 Byte, via Linkerscript reserviert) | ✅ | +| 7.3 | Makro-Tabelle in **NVM Row 1** (0x1FF00, 256 Byte) | ✅ | +| 7.4 | NVM-Schreiben: Row löschen + 4 Pages à 64 Byte manuell schreiben (MANW=1) | ✅ | +| 7.5 | Bei ungültigem Magic / falscher Version / CRC-Fehler → **Defaults** laden (kein Crash) | ✅ | +| 7.6 | Defaults: alle Aktionen NONE, LEDs warm-weiß, Animation Regenbogen 4 s | ✅ | + +### 8 Serial-Kommunikation mit VersaGUI + +| # | Anforderung | Status | +|---|-------------|--------| +| 8.1 | **CDC Serial** (USB), kein Treiber nötig; 8-Byte-Festlängen-Pakete | ✅ | +| 8.2 | **Ring-Buffer** (256 Byte = 32 Pakete) für eingehende Bytes; kein Datenverlust bei Burst | ✅ | +| 8.3 | **Ping / Pong** (0x05 / 0x85) zur Verbindungsdiagnose | ✅ | +| 8.4 | **Config-Transfer PC→Board**: BEGIN(0x10) → 38×DATA(0x11) → COMMIT(0x12) → ACK/NACK | ✅ | +| 8.5 | **Config-Dump Board→PC**: auf READ(0x13) → BEGIN(0x92) → 38×DATA(0x93) → END(0x94) | ✅ | +| 8.6 | **Makro-Transfer PC→Board**: BEGIN(0x20) → 43×DATA(0x21) → COMMIT(0x22) → ACK(0x95) | ✅ | +| 8.7 | **Makro-Dump Board→PC**: auf READ(0x23) → BEGIN(0x96) → 43×DATA(0x97) → END(0x98) | ✅ | +| 8.8 | Config-COMMIT validiert Magic + Version + CRC; bei Fehler **NACK** ohne NVM-Schreiben | ✅ | +| 8.9 | Alle Sende-Pakete nur wenn `SerialUSB` aktiv (DTR-Check verhindert stilles Verwerfen) | ✅ | + +### 9 Nicht implementiert / Roadmap + +| # | Anforderung | Status | +|---|-------------|--------| +| 9.1 | **Fader/Potentiometer**: 3× ADC-Kanäle auf Board vorhanden, HAL nicht implementiert | 🔲 TODO | +| 9.2 | **HOST_COMMAND-Payload**: Board sendet Command-ID, App-Seite führt aus (halbfertig) | 🔲 TODO | +| 9.3 | **FADE_IN / FADE_OUT** per GUI konfigurierbar (Firmware vorhanden, kein GUI-Eintrag) | 🔲 TODO | + +--- + ## Projekt-Struktur ``` @@ -111,25 +213,38 @@ Byte 5–7: reserviert (0x00) | 0x85 | Board→PC | Pong | | 0x90 | Board→PC | Config-ACK | | 0x91 | Board→PC | Config-NACK | +| 0x20 | PC→Board | Makro-Begin (Chunk-Anzahl = 43) | +| 0x21 | PC→Board | Makro-Data (Chunk-Index + 6B Nutzdaten) | +| 0x22 | PC→Board | Makro-Commit (in NVM schreiben) | +| 0x23 | PC→Board | Makro-Read (Board sendet Tabelle zurück) | +| 0x90 | Board→PC | Config-ACK | +| 0x91 | Board→PC | Config-NACK (CRC/Magic/Version ungültig) | | 0x92 | Board→PC | Config-Begin (Dump-Start, Chunks-Anzahl) | | 0x93 | Board→PC | Config-Data (Chunk-Index + 6B) | | 0x94 | Board→PC | Config-End | +| 0x95 | Board→PC | Makro-ACK | +| 0x96 | Board→PC | Makro-Begin (Dump-Start) | +| 0x97 | Board→PC | Makro-Data (Chunk-Index + 6B) | +| 0x98 | Board→PC | Makro-End | -### NVM-Config-Layout (163 Bytes, packed) +### NVM-Config-Layout (Version 2, 223 Bytes, packed) ``` -Offset 0 4B Magic 0x56503202 -Offset 4 1B Version 1 -Offset 5 2B CRC16-CCITT (über Bytes 7–162) -Offset 7 60B mx_actions[20] – je 3B: type(1B) + data(2B) -Offset 67 36B enc_actions[4][3] – je 3B -Offset103 20B led_r[20] -Offset123 20B led_g[20] -Offset143 20B led_b[20] +Offset 0 4B Magic 0x56503202 +Offset 4 1B Version 2 +Offset 5 2B CRC16-CCITT (über Bytes 7–222) +Offset 7 60B mx_actions[20] – je 3B: type(1B) + data(2B) +Offset 67 36B enc_actions[4][3] – je 3B +Offset 103 20B led_r[20] +Offset 123 20B led_g[20] +Offset 143 20B led_b[20] +Offset 163 20B led_anim[20] – LEDAnim-Typ (uint8_t) +Offset 183 40B led_period_ms[20] – Animationsperiode in ms (uint16_t, LE) ``` -Gespeichert in den letzten 512 Bytes des Flashs (0x1FE00), via Linkerscript reserviert. -Bei ungültigem Magic/CRC werden Defaults geladen (alle Tasten: NONE, LEDs: warm-weiß). +**NVM Row 0** (0x1FE00, 256 Byte): Config (223B genutzt, 33B Padding) +**NVM Row 1** (0x1FF00, 256 Byte): Makro-Tabelle (32 Slots × 4 Steps × 2B) +Via Linkerscript reserviert. Bei ungültigem Magic/Version/CRC werden Defaults geladen. ## Bekannte Fallstricke diff --git a/src/CMainController.cpp b/src/CMainController.cpp index 7f5a499..ad3ebcf 100644 --- a/src/CMainController.cpp +++ b/src/CMainController.cpp @@ -31,7 +31,7 @@ #include "hal/usb_serial.h" #include "config/pins.h" #include "config/nvm_config.h" -#include +#include "config/macro_config.h" // ─── Static Bridge: HAL-Callbacks → EventQueue ─────────────────────────────── // @@ -70,13 +70,18 @@ static void encoder_cb(uint8_t enc, int8_t dir) CMainController::CMainController() : m_cfg_chunks_expected(0) , m_cfg_receiving(false) + , m_macro_chunks_expected(0) + , m_macro_receiving(false) { - memset(m_cfg_buf, 0, sizeof(m_cfg_buf)); + 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() { - init_buttons(); // Buttons aus NVM laden (oder Defaults) + 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 @@ -102,13 +107,8 @@ void CMainController::init_buttons() } // MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen, - // Aktion + Base-Farbe aus NVM. + // Aktion + Base-Farbe + Animation 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; @@ -118,9 +118,16 @@ void CMainController::init_buttons() 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); + LEDAnim anim = static_cast(cfg.led_anim[mx_idx]); + uint16_t period = cfg.led_period_ms[mx_idx] > 0 ? cfg.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 @@ -246,6 +253,58 @@ void CMainController::poll_vendor() } 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)) { + uint8_t count = (uint8_t)(sizeof(m_macro_buf) - offset); + if (count > 6) count = 6; + 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)); + macro_config_save(m_macros); + usb_serial_send(USB_EVT_MACRO_ACK, 0); + } + break; + + // ── Makro-Dump anfordern ───────────────────────────────────────── + case USB_CMD_MACRO_READ: + { + const uint8_t* raw = reinterpret_cast(&m_macros); + const uint16_t sz = sizeof(SMacroTable); // 256 + const uint8_t payload = 6; + uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 43 + + 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; } @@ -328,6 +387,23 @@ void CMainController::execute_action(SAction action) // Wird in processEvents() über Serial gesendet break; + case ActionType::MACRO: + { + // Makro-Slot aus dem RAM ausführen (bei setup() aus NVM geladen). + // 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::NONE: default: break; diff --git a/src/CMainController.h b/src/CMainController.h index c8291a3..b95887a 100644 --- a/src/CMainController.h +++ b/src/CMainController.h @@ -12,6 +12,7 @@ #include "hal/usb_hid.h" #include "hal/usb_serial.h" #include "config/action.h" +#include "config/macro_config.h" class CMainController { @@ -41,10 +42,15 @@ private: void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben // ── Config-Empfangspuffer ───────────────────────────────────────────────── - // Mehrteilige Übertragung: BEGIN setzt receiving=true, DATA füllt den Buffer, - // COMMIT validiert und schreibt in den NVM. - // Puffergröße = sizeof(SDeviceConfig) = 163 Bytes. - uint8_t m_cfg_buf[163]; // Empfangspuffer für eingehende Config-Daten - uint8_t m_cfg_chunks_expected; // Anzahl erwarteter Chunks (aus BEGIN-Paket) - bool m_cfg_receiving; // true wenn Übertragung läuft + uint8_t m_cfg_buf[223]; // sizeof(SDeviceConfig) = 223 Bytes + uint8_t m_cfg_chunks_expected; + bool m_cfg_receiving; + + // ── Makro-Empfangspuffer ────────────────────────────────────────────────── + uint8_t m_macro_buf[256]; // sizeof(SMacroTable) = 256 Bytes + uint8_t m_macro_chunks_expected; + bool m_macro_receiving; + + // Geladene Makro-Tabelle (im RAM – wird beim Start aus NVM geladen) + SMacroTable m_macros; }; diff --git a/src/config/action.h b/src/config/action.h index 88c7323..2ee7408 100644 --- a/src/config/action.h +++ b/src/config/action.h @@ -7,6 +7,7 @@ enum class ActionType : uint8_t HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet) HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …) HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …) + MACRO, // Makro-Slot (data = Slot-Index 0–31) → bis zu 4 HID-Keys sequenziell }; struct __attribute__((packed)) SAction diff --git a/src/config/macro_config.cpp b/src/config/macro_config.cpp new file mode 100644 index 0000000..de3d289 --- /dev/null +++ b/src/config/macro_config.cpp @@ -0,0 +1,66 @@ +// macro_config.cpp +// NVM-Zugriff für die Makro-Tabelle (Row 1, 0x1FF00). +// Nutzt dieselben NVMCTRL-Hilfsfunktionen wie nvm_config.cpp (dupliziert, +// da static – kein gemeinsamer Header für interne NVM-Helfer). + +#include "macro_config.h" +#include +#include + +static const uint32_t k_macro_addr = 0x1FF00UL; // Row 1 (256B nach Row 0) + +static void nvm_wait() { while (!NVMCTRL->INTFLAG.bit.READY) {} } +static void nvm_exec(uint16_t cmd) +{ + NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd; + nvm_wait(); +} + +static void nvm_erase_row(uint32_t addr) +{ + nvm_wait(); + NVMCTRL->ADDR.reg = addr / 2; + nvm_exec(NVMCTRL_CTRLA_CMD_ER); +} + +static void nvm_write_page(uint32_t addr, const uint8_t* data) +{ + nvm_exec(NVMCTRL_CTRLA_CMD_PBC); + volatile uint32_t* dst = reinterpret_cast(addr); + const uint32_t* src = reinterpret_cast(data); + for (uint8_t i = 0; i < 64 / 4; i++) dst[i] = src[i]; + NVMCTRL->ADDR.reg = addr / 2; + nvm_exec(NVMCTRL_CTRLA_CMD_WP); +} + +bool macro_config_load(SMacroTable& tbl) +{ + memcpy(&tbl, reinterpret_cast(k_macro_addr), sizeof(tbl)); + + // Prüfen ob Row 1 noch gelöscht ist (alle 0xFF = nie beschrieben) + const uint8_t* raw = reinterpret_cast(&tbl); + bool all_ff = true; + for (uint16_t i = 0; i < sizeof(tbl); i++) { + if (raw[i] != 0xFF) { all_ff = false; break; } + } + if (all_ff) { + memset(&tbl, 0, sizeof(tbl)); // Leere Tabelle als Default + return false; + } + return true; +} + +void macro_config_save(const SMacroTable& tbl) +{ + // Auf 4-Byte-ausgerichteten Puffer kopieren bevor nvm_write_page ihn als uint32_t* liest. + // SMacroTable ist __attribute__((packed)) und könnte unaligned liegen → + // direkter uint32_t*-Cast würde auf Cortex-M0+ einen HardFault auslösen. + uint8_t aligned_buf[256] __attribute__((aligned(4))); + memcpy(aligned_buf, &tbl, sizeof(tbl)); + + NVMCTRL->CTRLB.bit.MANW = 1; + nvm_erase_row(k_macro_addr); + for (uint8_t p = 0; p < 4; p++) { + nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64); + } +} diff --git a/src/config/macro_config.h b/src/config/macro_config.h new file mode 100644 index 0000000..e81a162 --- /dev/null +++ b/src/config/macro_config.h @@ -0,0 +1,36 @@ +#pragma once +// macro_config.h +// Makro-Tabelle: bis zu 32 Slots, je 4 HID-Key-Steps. +// Gespeichert in NVM Row 1 (0x1FF00, 256 Bytes). +// +// Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind): +// Slot 0–19 : MX-Buttons (mx_idx) +// Slot 20–31 : Encoder-Aktionen (enc*3 + act_idx, 0=SW/1=CW/2=CCW) +// +// Ein Step mit keycode=0 gilt als leer → Ausführung stoppt dort. +// Delay zwischen Steps: 20 ms (hardcoded). + +#include + +#define MACRO_SLOTS 32 +#define MACRO_MAX_STEPS 4 + +// Ein einzelner HID-Key-Step im Makro +struct __attribute__((packed)) SMacroStep +{ + uint8_t keycode; // HID Keyboard Usage (0x00 = leer → Step überspringen) + uint8_t modifier; // HID Modifier-Byte (Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08) +}; + +// Komplette Makro-Tabelle (32 × 4 × 2 = 256 Bytes = eine NVM-Row) +struct __attribute__((packed)) SMacroTable +{ + SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS]; +}; + +// Makro-Tabelle aus NVM lesen (Row 1: 0x1FF00). +// Gibt false zurück wenn der Flash-Bereich noch gelöscht (0xFF) war → leere Tabelle geladen. +bool macro_config_load(SMacroTable& tbl); + +// Makro-Tabelle in NVM schreiben (löscht Row 1, schreibt 4 Pages). +void macro_config_save(const SMacroTable& tbl); diff --git a/src/config/nvm_config.cpp b/src/config/nvm_config.cpp index 882a135..d8564a8 100644 --- a/src/config/nvm_config.cpp +++ b/src/config/nvm_config.cpp @@ -82,6 +82,12 @@ void nvm_config_defaults(SDeviceConfig& cfg) cfg.led_b[i] = 0; } + // LED-Animationen: Regenbogen (COLOR_CYCLE=5) mit 4s Periode als Standard + for (uint8_t i = 0; i < 20; i++) { + cfg.led_anim[i] = 5; // LEDAnim::COLOR_CYCLE + cfg.led_period_ms[i] = 4000; + } + cfg.crc = nvm_config_crc(cfg); } diff --git a/src/config/nvm_config.h b/src/config/nvm_config.h index 5952ece..d232be7 100644 --- a/src/config/nvm_config.h +++ b/src/config/nvm_config.h @@ -7,19 +7,21 @@ // Offset Size Inhalt // 0 4 Magic (0x56503202 = 'VP2\x02') // 4 1 Version -// 5 2 CRC16 über Bytes 7–162 +// 5 2 CRC16 über Bytes 7–222 // 7 60 mx_actions[20] – 20 × 3B (SAction packed) // 67 36 enc_actions[4][3] – 12 × 3B // 103 20 led_r[20] // 123 20 led_g[20] // 143 20 led_b[20] -// 163 93 Padding bis 256 Bytes (erste Row voll) +// 163 20 led_anim[20] – LEDAnim-Typ pro Button (uint8_t) +// 183 40 led_period_ms[20] – Animationsperiode in ms (uint16_t, little-endian) +// 223 33 Padding bis 256 Bytes (erste Row voll) // 256 256 Reserviert für zukünftige Erweiterungen (zweite Row) // -// Gesamt genutzt: 163 Bytes (sizeof SDeviceConfig mit packed SAction) +// Gesamt genutzt: 223 Bytes (sizeof SDeviceConfig mit packed SAction) #define NVM_CONFIG_MAGIC 0x56503202UL -#define NVM_CONFIG_VERSION 1 +#define NVM_CONFIG_VERSION 2 // Version 2: led_anim + led_period_ms hinzugefügt // Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[]) // Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW @@ -41,6 +43,10 @@ struct __attribute__((packed)) SDeviceConfig uint8_t led_r[20]; uint8_t led_g[20]; uint8_t led_b[20]; + + // LED-Animationen pro MX-Button + uint8_t led_anim[20]; // LEDAnim-Typ (0=STATIC, 1=BLINK, 2=PULSE, 5=COLOR_CYCLE) + uint16_t led_period_ms[20]; // Animationsperiode in ms (0 = Firmware-Default verwenden) }; // Standardwerte wenn keine gültige Config im NVM diff --git a/src/hal/usb_serial.h b/src/hal/usb_serial.h index a2ea1fd..bbeb2ab 100644 --- a/src/hal/usb_serial.h +++ b/src/hal/usb_serial.h @@ -36,6 +36,11 @@ #define USB_CMD_CONFIG_DATA 0x11 #define USB_CMD_CONFIG_COMMIT 0x12 #define USB_CMD_CONFIG_READ 0x13 // Board sendet aktuelle NVM-Config zurück +// Makro-Tabelle übertragen (gleiche Chunk-Struktur wie Config): +#define USB_CMD_MACRO_BEGIN 0x20 // Data[1] = Chunk-Anzahl +#define USB_CMD_MACRO_DATA 0x21 // Data[1] = Chunk-Index, Data[2..7] = 6B +#define USB_CMD_MACRO_COMMIT 0x22 // NVM schreiben + ACK zurück +#define USB_CMD_MACRO_READ 0x23 // Board sendet aktuelle Makro-Tabelle zurück // ── Events: Board → PC ──────────────────────────────────────────────────────── #define USB_EVT_KEY_DOWN 0x81 // key_id → HOST_COMMAND-Button gedrückt @@ -48,6 +53,10 @@ #define USB_EVT_CONFIG_BEGIN 0x92 // Beginn Config-Dump: Data[1] = Chunk-Anzahl #define USB_EVT_CONFIG_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B #define USB_EVT_CONFIG_END 0x94 // Config-Dump abgeschlossen +#define USB_EVT_MACRO_ACK 0x95 // Makro-Tabelle erfolgreich gespeichert +#define USB_EVT_MACRO_BEGIN 0x96 // Beginn Makro-Dump: Data[1] = Chunk-Anzahl +#define USB_EVT_MACRO_DATA 0x97 // Makro-Chunk: Data[1] = Index, Data[2..7] = 6B +#define USB_EVT_MACRO_END 0x98 // Makro-Dump abgeschlossen // Paket-Struct mit Accessor-Methoden für lesbareren Code struct SerialPacket