VersaGUI-py/versapad_data.py

189 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Liest die 3 VersaPad-Config-JSONs (Desktop) und decodiert sie zu
lesbaren Strukturen. Referenz: VersaGUI/src/ActionDialog.cs, ConfigJson.cs
(s_consumer-Tabelle, HidKeyName()-Sonderzeichen, Modifier-Bits).
Kein Schreibzugriff auf die JSONs -- reines Lesen/Anzeigen.
"""
import json
import os
CONFIG_PATHS = {
0: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config1.json"),
1: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config2.json"),
2: os.path.expanduser(r"~\OneDrive\Desktop\versapad_config3.json"),
}
PROFILE_NAMES = {
0: "Profil 0 Windows",
1: "Profil 1 Fusion 360",
2: "Profil 2 BricsCAD",
}
# index = spalte*5 + reihe, Reihe 0 = oben, Reihe 4 = unten (VersaMCU-Firmware-Reihenfolge)
GRID_COLS = 4
GRID_ROWS = 5
MODIFIER_BITS = [
(0x01, "Strg"),
(0x02, "Shift"),
(0x04, "Alt"),
(0x08, "Win"),
]
# Verbatim aus ActionDialog.cs HidKeyName() -- Sondertasten ohne Scan-Code-Eintrag
_SPECIAL_KEYS = {
0x28: "Enter", 0x29: "Escape", 0x2A: "Backspace", 0x2B: "Tab",
0x2C: "Leer", 0x39: "Caps", 0x46: "Druck", 0x47: "Rollen",
0x48: "Pause", 0x49: "Einfg", 0x4A: "Pos1", 0x4B: "Bild↑",
0x4C: "Entf", 0x4D: "Ende", 0x4E: "Bild↓",
0x4F: "", 0x50: "", 0x51: "", 0x52: "",
0x53: "NumLock", 0x54: "Num/", 0x55: "Num*", 0x56: "Num-",
0x57: "Num+", 0x58: "NumEnter",
0x59: "Num1", 0x5A: "Num2", 0x5B: "Num3", 0x5C: "Num4",
0x5D: "Num5", 0x5E: "Num6", 0x5F: "Num7", 0x60: "Num8",
0x61: "Num9", 0x62: "Num0", 0x63: "Num.",
0x3A: "F1", 0x3B: "F2", 0x3C: "F3", 0x3D: "F4", 0x3E: "F5",
0x3F: "F6", 0x40: "F7", 0x41: "F8", 0x42: "F9", 0x43: "F10",
0x44: "F11", 0x45: "F12",
}
# HID Usage Page 0x07 (Keyboard), Zeichentasten -- US-Layout-Beschriftung als
# lesbare Näherung (die reale GUI zeigt das aktive Windows-Layout via
# GetKeyNameText, das können wir hier ohne WinAPI-Call nicht nachbilden).
for _i in range(26):
_SPECIAL_KEYS[0x04 + _i] = chr(ord("A") + _i)
for _i in range(9):
_SPECIAL_KEYS[0x1E + _i] = str(_i + 1)
_SPECIAL_KEYS[0x27] = "0"
_SPECIAL_KEYS.update({
0x2D: "-", 0x2E: "=", 0x2F: "[", 0x30: "]", 0x31: "\\",
0x32: "#", 0x33: ";", 0x34: "'", 0x35: "`", 0x36: ",",
0x37: ".", 0x38: "/", 0x64: "\\ (Non-US)", 0x65: "Menu",
})
# Verbatim aus ActionDialog.cs s_consumer -- HID Consumer Usage IDs (Usage Page 0x0C)
_CONSUMER_NAMES = {
0x00CD: "Play / Pause",
0x00B5: "Nächster Titel",
0x00B6: "Vorheriger Titel",
0x00B7: "Stop",
0x00E9: "Lauter",
0x00EA: "Leiser",
0x00E2: "Stummschalten",
0x0192: "Taschenrechner",
0x0223: "Browser Startseite",
0x0224: "Browser Zurück",
0x0225: "Browser Vor",
0x00B0: "Aufnahme",
}
ANIM_LABELS = {
"Static": "● statisch",
"Blink": "◎ blinkend",
"Pulse": "≈ pulsierend",
"FadeIn": "↗ fade-in",
"FadeOut": "↘ fade-out",
"ColorCycle": "⟳ regenbogen",
"ColorFade": "⟳ farbwechsel",
}
def hid_key_choices():
"""Sortierte [(keycode, name), ...] fuer Dropdown-Auswahl beim Editieren."""
return sorted(_SPECIAL_KEYS.items())
def consumer_choices():
"""Sortierte [(usage_id, name), ...] fuer Dropdown-Auswahl beim Editieren."""
return sorted(_CONSUMER_NAMES.items())
def macro_step_label(step):
"""step: {'keycode','modifier'} -> z.B. 'Strg+S'. Reine Keycode/Modifier-Variante
von hid_key_label() (dort steckt beides in einem 16-Bit data-Feld, hier getrennt)."""
mods = [name for bit, name in MODIFIER_BITS if step["modifier"] & bit]
key = _SPECIAL_KEYS.get(step["keycode"], f"0x{step['keycode']:02X}")
return "+".join(mods + [key]) if mods else key
def macro_slot_label(steps):
if not steps:
return "(leer)"
return "".join(macro_step_label(s) for s in steps)
def hid_key_label(data):
"""data = keycode | (modifier << 8) -> z.B. 'Strg+S'"""
keycode = data & 0xFF
modifier = (data >> 8) & 0xFF
mods = [name for bit, name in MODIFIER_BITS if modifier & bit]
key = _SPECIAL_KEYS.get(keycode, f"0x{keycode:02X}")
return "+".join(mods + [key]) if mods else key
def consumer_label(data):
return _CONSUMER_NAMES.get(data, f"Consumer 0x{data:04X}")
def action_label(action):
"""Menschenlesbarer Text für eine DeviceAction {type, data}."""
t = action.get("type")
d = action.get("data", 0)
if t == "None":
return ""
if t == "HidKey":
return hid_key_label(d)
if t == "HidConsumer":
return consumer_label(d)
if t == "Macro":
return f"Makro (Slot {d})"
if t == "ProfileSwitch":
if d in (0xFFFF, 0x00FF):
return "Profilwechsel → nächstes"
return f"Profilwechsel → Profil {d}"
return f"{t} ({d})"
def led_css(led):
return f"rgb({led['r']},{led['g']},{led['b']})"
def led_css_hex(led):
"""Tk-Farben brauchen #RRGGBB statt rgb(...)."""
return f"#{led['r']:02x}{led['g']:02x}{led['b']:02x}"
def button_grid_position(index):
"""index -> (col, row), Reihe 0 = oben"""
col, row = divmod(index, GRID_ROWS)
return col, row
def annotate_profile(cfg):
"""Fuegt label/col/row-Felder hinzu (fuer die Anzeige) -- egal ob cfg aus
einer Einzel-JSON oder aus dem kombinierten Programmiermodus-State kommt."""
for b in cfg["buttons"]:
b["label"] = action_label(b["action"])
b["col"], b["row"] = button_grid_position(b["index"])
for e in cfg["encoders"]:
e["sw_label"] = action_label(e["sw"])
e["cw_label"] = action_label(e["cw"])
e["ccw_label"] = action_label(e["ccw"])
return cfg
def load_profile(profile):
path = CONFIG_PATHS[profile]
with open(path, "r", encoding="utf-8") as f:
cfg = json.load(f)
return annotate_profile(cfg)
def load_all_profiles():
return {p: load_profile(p) for p in CONFIG_PATHS}
def config_mtimes():
"""Für Auto-Reload: liefert {profile: mtime} der 3 JSON-Dateien."""
return {p: os.path.getmtime(path) for p, path in CONFIG_PATHS.items() if os.path.exists(path)}