196 lines
7.7 KiB
Python
196 lines
7.7 KiB
Python
"""
|
|
Binaeres NVM-Layout des VersaPad-Boards -- pack/unpack fuer SDeviceConfig
|
|
(740B) und SMacroTable (512B), plus die CRC16, die die Firmware fuer
|
|
CONFIG_COMMIT/-READ prueft.
|
|
|
|
1:1 aus den Firmware-Quellen uebernommen, nichts geraten:
|
|
- VersaMCU/src/config/nvm_config.h (SDeviceConfig/SDeviceProfile-Layout)
|
|
- VersaMCU/src/config/action.h (SAction, ActionType-Enum-Werte)
|
|
- VersaMCU/src/config/macro_config.h (SMacroTable/SMacroStep-Layout)
|
|
- VersaMCU/src/CButton.h (LEDAnim-Enum-Werte)
|
|
- VersaMCU/src/config/nvm_config.cpp (nvm_config_crc(), Validierung)
|
|
|
|
Wer hier etwas aendert: IMMER gegen diese Dateien abgleichen, nicht raten --
|
|
ein falsches Byte-Layout schreibt (nach CRC-Fehlschlag zum Glueck nur NACK,
|
|
kein Datenmuell aufs Board, siehe nvm_config_validate()).
|
|
"""
|
|
import struct
|
|
|
|
NVM_CONFIG_MAGIC = 0x56503203
|
|
NVM_CONFIG_VERSION = 3
|
|
CONFIG_SIZE = 740
|
|
MACRO_SIZE = 512
|
|
MACRO_SLOTS = 32
|
|
MACRO_MAX_STEPS = 8
|
|
|
|
ACTION_TYPES = ["None", "HidKey", "HidConsumer", "HostCommand", "Macro", "ProfileSwitch"]
|
|
ACTION_TYPE_TO_INT = {name: i for i, name in enumerate(ACTION_TYPES)}
|
|
|
|
ANIM_TYPES = ["Static", "Blink", "Pulse", "FadeIn", "FadeOut", "ColorCycle", "ColorFade"]
|
|
ANIM_TYPE_TO_INT = {name: i for i, name in enumerate(ANIM_TYPES)}
|
|
|
|
_ACTION_FMT = "<BH" # type(1B) + data(2B, little-endian) = 3B
|
|
|
|
|
|
def _pack_action(action):
|
|
return struct.pack(_ACTION_FMT, ACTION_TYPE_TO_INT[action["type"]], action["data"] & 0xFFFF)
|
|
|
|
|
|
def _unpack_action(buf, offset):
|
|
t, data = struct.unpack_from(_ACTION_FMT, buf, offset)
|
|
return {"type": ACTION_TYPES[t] if t < len(ACTION_TYPES) else f"Unknown{t}", "data": data}, offset + 3
|
|
|
|
|
|
def crc16(buf):
|
|
"""CRC16-CCITT (Poly 0x1021, Init 0xFFFF, MSB-first, kein XOR-Out) --
|
|
exakt nvm_config_crc() aus nvm_config.cpp."""
|
|
crc = 0xFFFF
|
|
for byte in buf:
|
|
crc ^= byte << 8
|
|
for _ in range(8):
|
|
crc = ((crc << 1) ^ 0x1021) if (crc & 0x8000) else (crc << 1)
|
|
crc &= 0xFFFF
|
|
return crc
|
|
|
|
|
|
# ── Profil (236B) ────────────────────────────────────────────────────────────────
|
|
|
|
def pack_profile(profile):
|
|
"""profile: {"buttons": [20x {index,action,led}], "encoders": [4x {index,sw,cw,ccw}]}"""
|
|
buttons_by_index = {b["index"]: b for b in profile["buttons"]}
|
|
encoders_by_index = {e["index"]: e for e in profile["encoders"]}
|
|
|
|
mx_actions = b"".join(_pack_action(buttons_by_index[i]["action"]) for i in range(20))
|
|
|
|
enc_actions = b""
|
|
for i in range(4):
|
|
enc = encoders_by_index[i]
|
|
enc_actions += _pack_action(enc["sw"]) + _pack_action(enc["cw"]) + _pack_action(enc["ccw"])
|
|
|
|
leds = [buttons_by_index[i]["led"] for i in range(20)]
|
|
led_r = bytes(led["r"] for led in leds)
|
|
led_g = bytes(led["g"] for led in leds)
|
|
led_b = bytes(led["b"] for led in leds)
|
|
led_bri = bytes(led["brightness"] for led in leds)
|
|
led_anim = bytes(ANIM_TYPE_TO_INT[led["anim"]] for led in leds)
|
|
led_period = b"".join(struct.pack("<H", led["period_ms"]) for led in leds)
|
|
|
|
blob = mx_actions + enc_actions + led_r + led_g + led_b + led_bri + led_anim + led_period
|
|
assert len(blob) == 236, f"Profil-Blob {len(blob)}B != 236B"
|
|
return blob
|
|
|
|
|
|
def unpack_profile(buf, offset):
|
|
start = offset
|
|
buttons = []
|
|
for i in range(20):
|
|
action, offset = _unpack_action(buf, offset)
|
|
buttons.append({"index": i, "action": action})
|
|
|
|
encoders = []
|
|
for i in range(4):
|
|
sw, offset = _unpack_action(buf, offset)
|
|
cw, offset = _unpack_action(buf, offset)
|
|
ccw, offset = _unpack_action(buf, offset)
|
|
encoders.append({"index": i, "sw": sw, "cw": cw, "ccw": ccw})
|
|
|
|
led_r = buf[offset:offset + 20]; offset += 20
|
|
led_g = buf[offset:offset + 20]; offset += 20
|
|
led_b = buf[offset:offset + 20]; offset += 20
|
|
led_bri = buf[offset:offset + 20]; offset += 20
|
|
led_anim = buf[offset:offset + 20]; offset += 20
|
|
led_period = struct.unpack_from("<20H", buf, offset); offset += 40
|
|
|
|
for i in range(20):
|
|
buttons[i]["led"] = {
|
|
"r": led_r[i], "g": led_g[i], "b": led_b[i], "brightness": led_bri[i],
|
|
"anim": ANIM_TYPES[led_anim[i]] if led_anim[i] < len(ANIM_TYPES) else "Static",
|
|
"period_ms": led_period[i],
|
|
}
|
|
|
|
assert offset - start == 236, f"Profil-Unpack {offset - start}B != 236B"
|
|
return {"buttons": buttons, "encoders": encoders}, offset
|
|
|
|
|
|
# ── Gesamt-Config (740B) ───────────────────────────────────────────────
|
|
|
|
def pack_config(cfg):
|
|
"""cfg: {"active_profile", "global_brightness", "enc_sensitivity"[4], "profiles"[3]}
|
|
Berechnet die CRC selbst -- ignoriert einen evtl. vorhandenen cfg["crc"]."""
|
|
header_tail = struct.pack(
|
|
"<BB4B19x",
|
|
cfg["active_profile"] & 0xFF,
|
|
cfg["global_brightness"] & 0xFF,
|
|
*[s & 0xFF for s in cfg["enc_sensitivity"]],
|
|
)
|
|
profiles_blob = b"".join(pack_profile(p) for p in cfg["profiles"])
|
|
tail = header_tail + profiles_blob
|
|
assert len(tail) == CONFIG_SIZE - 7, f"Config-Tail {len(tail)}B != {CONFIG_SIZE - 7}B"
|
|
|
|
crc = crc16(tail)
|
|
head = struct.pack("<IBH", NVM_CONFIG_MAGIC, NVM_CONFIG_VERSION, crc)
|
|
blob = head + tail
|
|
assert len(blob) == CONFIG_SIZE, f"Config-Blob {len(blob)}B != {CONFIG_SIZE}B"
|
|
return blob
|
|
|
|
|
|
def unpack_config(buf):
|
|
if len(buf) != CONFIG_SIZE:
|
|
raise ValueError(f"Config-Blob hat {len(buf)}B, erwartet {CONFIG_SIZE}B")
|
|
|
|
magic, version, crc, active_profile, global_brightness = struct.unpack_from("<IBHBB", buf, 0)
|
|
enc_sensitivity = list(struct.unpack_from("<4B", buf, 9))
|
|
offset = 32
|
|
|
|
profiles = []
|
|
for _ in range(3):
|
|
profile, offset = unpack_profile(buf, offset)
|
|
profiles.append(profile)
|
|
|
|
computed_crc = crc16(buf[7:])
|
|
return {
|
|
"magic": magic,
|
|
"magic_ok": magic == NVM_CONFIG_MAGIC,
|
|
"version": version,
|
|
"crc": crc,
|
|
"crc_ok": crc == computed_crc,
|
|
"active_profile": active_profile,
|
|
"global_brightness": global_brightness,
|
|
"enc_sensitivity": enc_sensitivity,
|
|
"profiles": profiles,
|
|
}
|
|
|
|
|
|
# ── Makro-Tabelle (512B) ─────────────────────────────────────────────────────
|
|
|
|
def pack_macros(slots):
|
|
"""slots: Liste von 32 Listen mit bis zu 8 {"keycode","modifier"}-Dicts
|
|
(fehlende/kuerzere werden mit keycode=0/modifier=0 aufgefuellt)."""
|
|
assert len(slots) == MACRO_SLOTS, f"{len(slots)} Makro-Slots != {MACRO_SLOTS}"
|
|
out = bytearray(MACRO_SIZE)
|
|
for slot_idx, steps in enumerate(slots):
|
|
for step_idx in range(MACRO_MAX_STEPS):
|
|
keycode, modifier = 0, 0
|
|
if step_idx < len(steps) and steps[step_idx]:
|
|
keycode = steps[step_idx].get("keycode", 0) & 0xFF
|
|
modifier = steps[step_idx].get("modifier", 0) & 0xFF
|
|
offset = (slot_idx * MACRO_MAX_STEPS + step_idx) * 2
|
|
out[offset] = keycode
|
|
out[offset + 1] = modifier
|
|
return bytes(out)
|
|
|
|
|
|
def unpack_macros(buf):
|
|
if len(buf) != MACRO_SIZE:
|
|
raise ValueError(f"Makro-Blob hat {len(buf)}B, erwartet {MACRO_SIZE}B")
|
|
slots = []
|
|
for slot_idx in range(MACRO_SLOTS):
|
|
steps = []
|
|
for step_idx in range(MACRO_MAX_STEPS):
|
|
offset = (slot_idx * MACRO_MAX_STEPS + step_idx) * 2
|
|
keycode, modifier = buf[offset], buf[offset + 1]
|
|
if keycode == 0:
|
|
break # keycode=0 beendet die Sequenz (Firmware-Konvention)
|
|
steps.append({"keycode": keycode, "modifier": modifier})
|
|
slots.append(steps)
|
|
return slots
|