Added Macro functionality, updated readme

This commit is contained in:
Julian Appel 2026-03-29 22:19:27 +02:00
parent b49984b9c0
commit 59b3cb4dd1
9 changed files with 354 additions and 33 deletions

131
README.md
View File

@ -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. 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 14) | ✅ |
| 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 ## Projekt-Struktur
``` ```
@ -111,25 +213,38 @@ Byte 57: reserviert (0x00)
| 0x85 | Board→PC | Pong | | 0x85 | Board→PC | Pong |
| 0x90 | Board→PC | Config-ACK | | 0x90 | Board→PC | Config-ACK |
| 0x91 | Board→PC | Config-NACK | | 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) | | 0x92 | Board→PC | Config-Begin (Dump-Start, Chunks-Anzahl) |
| 0x93 | Board→PC | Config-Data (Chunk-Index + 6B) | | 0x93 | Board→PC | Config-Data (Chunk-Index + 6B) |
| 0x94 | Board→PC | Config-End | | 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 0 4B Magic 0x56503202
Offset 4 1B Version 1 Offset 4 1B Version 2
Offset 5 2B CRC16-CCITT (über Bytes 7162) Offset 5 2B CRC16-CCITT (über Bytes 7222)
Offset 7 60B mx_actions[20] je 3B: type(1B) + data(2B) Offset 7 60B mx_actions[20] je 3B: type(1B) + data(2B)
Offset 67 36B enc_actions[4][3] je 3B Offset 67 36B enc_actions[4][3] je 3B
Offset103 20B led_r[20] Offset 103 20B led_r[20]
Offset123 20B led_g[20] Offset 123 20B led_g[20]
Offset143 20B led_b[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. **NVM Row 0** (0x1FE00, 256 Byte): Config (223B genutzt, 33B Padding)
Bei ungültigem Magic/CRC werden Defaults geladen (alle Tasten: NONE, LEDs: warm-weiß). **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 ## Bekannte Fallstricke

View File

@ -31,7 +31,7 @@
#include "hal/usb_serial.h" #include "hal/usb_serial.h"
#include "config/pins.h" #include "config/pins.h"
#include "config/nvm_config.h" #include "config/nvm_config.h"
#include <string.h> #include "config/macro_config.h"
// ─── Static Bridge: HAL-Callbacks → EventQueue ─────────────────────────────── // ─── Static Bridge: HAL-Callbacks → EventQueue ───────────────────────────────
// //
@ -70,12 +70,17 @@ static void encoder_cb(uint8_t enc, int8_t dir)
CMainController::CMainController() CMainController::CMainController()
: m_cfg_chunks_expected(0) : m_cfg_chunks_expected(0)
, m_cfg_receiving(false) , 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() void CMainController::setup()
{ {
macro_config_load(m_macros); // Makro-Tabelle aus NVM laden (oder leere Tabelle)
init_buttons(); // Buttons aus NVM laden (oder Defaults) init_buttons(); // Buttons aus NVM laden (oder Defaults)
s_queue = &m_queue; // Queue-Pointer setzen bevor Callbacks registriert werden s_queue = &m_queue; // Queue-Pointer setzen bevor Callbacks registriert werden
usb_hid_init(); // HID-Descriptor registriert sich via globalem Konstruktor, usb_hid_init(); // HID-Descriptor registriert sich via globalem Konstruktor,
@ -102,13 +107,8 @@ void CMainController::init_buttons()
} }
// MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen, // 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) // 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++) { for (uint8_t key = 5; key < MATRIX_KEYS; key++) {
uint8_t col = key / MATRIX_ROWS; 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]); 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); 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) LEDAnim anim = static_cast<LEDAnim>(cfg.led_anim[mx_idx]);
uint16_t phase = (uint16_t)((uint32_t)mx_idx * k_rainbow_period / 20); uint16_t period = cfg.led_period_ms[mx_idx] > 0 ? cfg.led_period_ms[mx_idx] : 4000;
m_buttons[key].set_anim(LEDAnim::COLOR_CYCLE, k_rainbow_period, phase);
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 // Encoder CW/CCW-Aktionen separat merken Encoder haben kein CButton-Objekt
@ -246,6 +253,58 @@ void CMainController::poll_vendor()
} }
break; 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<const uint8_t*>(&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: default:
break; break;
} }
@ -328,6 +387,23 @@ void CMainController::execute_action(SAction action)
// Wird in processEvents() über Serial gesendet // Wird in processEvents() über Serial gesendet
break; 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<uint8_t>(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: case ActionType::NONE:
default: default:
break; break;

View File

@ -12,6 +12,7 @@
#include "hal/usb_hid.h" #include "hal/usb_hid.h"
#include "hal/usb_serial.h" #include "hal/usb_serial.h"
#include "config/action.h" #include "config/action.h"
#include "config/macro_config.h"
class CMainController class CMainController
{ {
@ -41,10 +42,15 @@ private:
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben
// ── Config-Empfangspuffer ───────────────────────────────────────────────── // ── Config-Empfangspuffer ─────────────────────────────────────────────────
// Mehrteilige Übertragung: BEGIN setzt receiving=true, DATA füllt den Buffer, uint8_t m_cfg_buf[223]; // sizeof(SDeviceConfig) = 223 Bytes
// COMMIT validiert und schreibt in den NVM. uint8_t m_cfg_chunks_expected;
// Puffergröße = sizeof(SDeviceConfig) = 163 Bytes. bool m_cfg_receiving;
uint8_t m_cfg_buf[163]; // Empfangspuffer für eingehende Config-Daten
uint8_t m_cfg_chunks_expected; // Anzahl erwarteter Chunks (aus BEGIN-Paket) // ── Makro-Empfangspuffer ──────────────────────────────────────────────────
bool m_cfg_receiving; // true wenn Übertragung läuft 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;
}; };

View File

@ -7,6 +7,7 @@ enum class ActionType : uint8_t
HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet) HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet)
HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …) HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …)
HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …) HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …)
MACRO, // Makro-Slot (data = Slot-Index 031) → bis zu 4 HID-Keys sequenziell
}; };
struct __attribute__((packed)) SAction struct __attribute__((packed)) SAction

View File

@ -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 <Arduino.h>
#include <string.h>
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<volatile uint32_t*>(addr);
const uint32_t* src = reinterpret_cast<const uint32_t*>(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<const void*>(k_macro_addr), sizeof(tbl));
// Prüfen ob Row 1 noch gelöscht ist (alle 0xFF = nie beschrieben)
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&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);
}
}

36
src/config/macro_config.h Normal file
View File

@ -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 019 : MX-Buttons (mx_idx)
// Slot 2031 : 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 <stdint.h>
#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);

View File

@ -82,6 +82,12 @@ void nvm_config_defaults(SDeviceConfig& cfg)
cfg.led_b[i] = 0; 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); cfg.crc = nvm_config_crc(cfg);
} }

View File

@ -7,19 +7,21 @@
// Offset Size Inhalt // Offset Size Inhalt
// 0 4 Magic (0x56503202 = 'VP2\x02') // 0 4 Magic (0x56503202 = 'VP2\x02')
// 4 1 Version // 4 1 Version
// 5 2 CRC16 über Bytes 7162 // 5 2 CRC16 über Bytes 7222
// 7 60 mx_actions[20] 20 × 3B (SAction packed) // 7 60 mx_actions[20] 20 × 3B (SAction packed)
// 67 36 enc_actions[4][3] 12 × 3B // 67 36 enc_actions[4][3] 12 × 3B
// 103 20 led_r[20] // 103 20 led_r[20]
// 123 20 led_g[20] // 123 20 led_g[20]
// 143 20 led_b[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) // 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_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[]) // Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[])
// Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW // 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_r[20];
uint8_t led_g[20]; uint8_t led_g[20];
uint8_t led_b[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 // Standardwerte wenn keine gültige Config im NVM

View File

@ -36,6 +36,11 @@
#define USB_CMD_CONFIG_DATA 0x11 #define USB_CMD_CONFIG_DATA 0x11
#define USB_CMD_CONFIG_COMMIT 0x12 #define USB_CMD_CONFIG_COMMIT 0x12
#define USB_CMD_CONFIG_READ 0x13 // Board sendet aktuelle NVM-Config zurück #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 ──────────────────────────────────────────────────────── // ── Events: Board → PC ────────────────────────────────────────────────────────
#define USB_EVT_KEY_DOWN 0x81 // key_id → HOST_COMMAND-Button gedrückt #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_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_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B
#define USB_EVT_CONFIG_END 0x94 // Config-Dump abgeschlossen #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 // Paket-Struct mit Accessor-Methoden für lesbareren Code
struct SerialPacket struct SerialPacket