VersaGUI-py/versapad_data.py
cjjohn e456af7c19 Add MCP server so Claude (or any MCP client) can reprogram VersaPad directly
Wraps the existing protocol/serial/combined-config layers as MCP tools
(set_button_key, set_encoder_consumer, set_macro, load_from_board,
write_to_board, ...) instead of requiring hand-written JSON + manual
GUI import. Registered at user scope via `claude mcp add`.
2026-08-05 08:16:22 +02:00

218 lines
7 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())
_KEY_CODE_BY_NAME = {name: code for code, name in _SPECIAL_KEYS.items()}
_CONSUMER_ID_BY_NAME = {name: cid for cid, name in _CONSUMER_NAMES.items()}
def hid_key_code_for_name(name):
"""z.B. 'S' -> 0x16. Wirft ValueError mit Vorschlaegen bei unbekanntem Namen."""
if name not in _KEY_CODE_BY_NAME:
raise ValueError(f"Unbekannte Taste {name!r}. Gueltige Namen: {sorted(_KEY_CODE_BY_NAME)}")
return _KEY_CODE_BY_NAME[name]
def consumer_id_for_name(name):
"""z.B. 'Play / Pause' -> 0xCD. Wirft ValueError mit Vorschlaegen bei unbekanntem Namen."""
if name not in _CONSUMER_ID_BY_NAME:
raise ValueError(f"Unbekannte Medienaktion {name!r}. Gueltige Namen: {sorted(_CONSUMER_ID_BY_NAME)}")
return _CONSUMER_ID_BY_NAME[name]
def modifier_bits_for_names(names):
"""['Strg','Shift'] -> 0x03. Wirft ValueError bei unbekanntem Namen."""
valid = {name: bit for bit, name in MODIFIER_BITS}
bits = 0
for name in names:
if name not in valid:
raise ValueError(f"Unbekannter Modifier {name!r}. Gueltig: {sorted(valid)}")
bits |= valid[name]
return bits
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)}