VersaGUI-py/versapad_protocol.py

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