Harden firmware state and transfer handling

This commit is contained in:
Julian Appel 2026-07-24 09:49:21 +02:00
parent 50dbf8fbee
commit ce5db617a1
27 changed files with 558 additions and 231 deletions

View file

@ -6,15 +6,15 @@ enum class ActionType : uint8_t
NONE, // Keine Aktion
HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet)
HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …)
HOST_COMMAND, // Host-Event; data wird vom aktuellen Controller nicht übertragen
HOST_COMMAND, // Host-Event; data = Command-ID für die Desktop-App
MACRO, // Makro-Slot (data = Slot-Index 031) → bis zu 8 HID-Keys sequenziell
PROFILE_SWITCH, // Profil 02 oder 0xFF = nächstes Profil; speichert in NVM
PROFILE_SWITCH, // Profil 02 oder 0x00FF/0xFFFF = nächstes Profil; speichert in NVM
};
struct __attribute__((packed)) SAction
{
ActionType type;
uint16_t data; // Typabhängige Nutzdaten; für HOST_COMMAND aktuell ungenutzt
uint16_t data; // Typabhängige Nutzdaten; HOST_COMMAND = Command-ID
// packed: 1B type + 2B data = 3B (kein Alignment-Padding)
// Muss packed sein, damit sizeof(SDeviceConfig)==740 und die
// hostseitige Serialisierung bytegenau übereinstimmen.

View file

@ -42,6 +42,18 @@ static bool nvm_write_page(uint32_t addr, const uint8_t* data)
return nvm_exec(NVMCTRL_CTRLA_CMD_WP);
}
bool macro_config_validate(const SMacroTable& tbl)
{
for (uint8_t slot = 0; slot < MACRO_SLOTS; slot++) {
for (uint8_t step = 0; step < MACRO_MAX_STEPS; step++) {
uint8_t keycode = tbl.steps[slot][step].keycode;
if (keycode > 0x65)
return false;
}
}
return true;
}
bool macro_config_load(SMacroTable& tbl)
{
memcpy(&tbl, reinterpret_cast<const void*>(k_macro_addr), sizeof(tbl));
@ -56,11 +68,17 @@ bool macro_config_load(SMacroTable& tbl)
memset(&tbl, 0, sizeof(tbl)); // Leere Tabelle als Default
return false;
}
if (!macro_config_validate(tbl)) {
memset(&tbl, 0, sizeof(tbl));
return false;
}
return true;
}
bool macro_config_save(const SMacroTable& tbl)
{
if (!macro_config_validate(tbl)) return false;
// 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.

View file

@ -28,6 +28,13 @@ struct __attribute__((packed)) SMacroTable
SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS];
};
static_assert(sizeof(SMacroStep) == 2, "SMacroStep binary layout changed");
static_assert(sizeof(SMacroTable) == 512, "SMacroTable binary layout changed");
// Prüft, dass alle belegten Steps in den vom HID-Descriptor unterstützten
// Keyboard-Usage-Bereich fallen.
bool macro_config_validate(const SMacroTable& tbl);
// Makro-Tabelle aus NVM lesen (Row 0+1: 0x1FB00).
// Gibt false zurück wenn der Flash-Bereich noch gelöscht (0xFF) war → leere Tabelle geladen.
bool macro_config_load(SMacroTable& tbl);

View file

@ -2,6 +2,7 @@
// NVM-Zugriff für SDeviceConfig (3 Rows ab 0x1FD00, 768B gesamt, 740B genutzt).
#include "nvm_config.h"
#include "macro_config.h"
#include <Arduino.h>
#include <string.h>
@ -65,6 +66,63 @@ uint16_t nvm_config_crc(const SDeviceConfig& cfg)
return crc;
}
static bool action_valid(const SAction& action)
{
switch (action.type) {
case ActionType::NONE:
return true;
case ActionType::HID_KEY:
return static_cast<uint8_t>(action.data & 0xFF) <= 0x65;
case ActionType::HID_CONSUMER:
return action.data <= 0x03FF;
case ActionType::HOST_COMMAND:
return true;
case ActionType::MACRO:
return action.data < MACRO_SLOTS;
case ActionType::PROFILE_SWITCH:
return action.data <= 2 ||
action.data == 0x00FF ||
action.data == 0xFFFF;
default:
return false;
}
}
bool nvm_config_validate(const SDeviceConfig& cfg)
{
if (cfg.magic != NVM_CONFIG_MAGIC) return false;
if (cfg.version != NVM_CONFIG_VERSION) return false;
if (cfg.crc != nvm_config_crc(cfg)) return false;
if (cfg.active_profile >= 3) return false;
for (uint8_t p = 0; p < 3; p++) {
const SDeviceProfile& prof = cfg.profiles[p];
for (uint8_t i = 0; i < 20; i++) {
if (!action_valid(prof.mx_actions[i])) return false;
uint8_t anim = prof.led_anim[i];
if (anim > 6) return false; // LEDAnim::COLOR_FADE
if (anim == 2 && prof.led_period_ms[i] < 2) return false;
}
for (uint8_t enc = 0; enc < 4; enc++) {
for (uint8_t action = 0; action < 3; action++) {
if (!action_valid(prof.enc_actions[enc][action]))
return false;
}
}
}
return true;
}
// ── Defaults ─────────────────────────────────────────────────────────────────
void nvm_config_defaults(SDeviceConfig& cfg)
@ -112,12 +170,10 @@ bool nvm_config_load(SDeviceConfig& cfg)
{
memcpy(&cfg, reinterpret_cast<const void*>(k_config_addr), sizeof(cfg));
if (cfg.magic != NVM_CONFIG_MAGIC) { nvm_config_defaults(cfg); return false; }
if (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; }
if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; }
// Profil-Index absichern
if (cfg.active_profile >= 3) cfg.active_profile = 0;
if (!nvm_config_validate(cfg)) {
nvm_config_defaults(cfg);
return false;
}
return true;
}
@ -126,11 +182,15 @@ bool nvm_config_load(SDeviceConfig& cfg)
bool nvm_config_save(const SDeviceConfig& cfg)
{
SDeviceConfig stored = cfg;
stored.crc = nvm_config_crc(stored);
if (!nvm_config_validate(stored)) return false;
// Config (740B) in 768B-Puffer kopieren (3 Rows), Rest mit 0xFF füllen.
// __attribute__((aligned(4))) ist zwingend: nvm_write_page castet zu uint32_t*.
uint8_t row[768] __attribute__((aligned(4)));
memset(row, 0xFF, sizeof(row));
memcpy(row, &cfg, sizeof(cfg));
memcpy(row, &stored, sizeof(stored));
NVMCTRL->CTRLB.bit.MANW = 1;

View file

@ -61,13 +61,22 @@ struct __attribute__((packed)) SDeviceConfig
// Gesamt: 32 + 708 = 740B
};
static_assert(sizeof(SAction) == 3, "SAction binary layout changed");
static_assert(sizeof(SDeviceProfile) == 236, "SDeviceProfile binary layout changed");
static_assert(sizeof(SDeviceConfig) == 740, "SDeviceConfig binary layout changed");
// Standardwerte wenn keine gültige Config im NVM
void nvm_config_defaults(SDeviceConfig& cfg);
// Vollständige Prüfung des persistenten/seriellen Binärvertrags inklusive CRC,
// Enum-Bereichen, Action-Nutzdaten und animationsspezifischen Mindestwerten.
bool nvm_config_validate(const SDeviceConfig& cfg);
// Config aus NVM lesen. Gibt false zurück wenn Magic/CRC/Version ungültig → Defaults geladen.
bool nvm_config_load(SDeviceConfig& cfg);
// Config in NVM schreiben (löscht 3 Rows, schreibt 12 Pages).
// Config in NVM schreiben (CRC wird intern neu berechnet; löscht 3 Rows,
// schreibt 12 Pages).
// Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird (Board hängt nicht).
bool nvm_config_save(const SDeviceConfig& cfg);