Add free-text notes per button/encoder action
Lets you record what a binding actually does (e.g. "Save in Fusion
360") alongside the auto-generated label ("Strg+S"). Notes live in a
new "note" field on every action dict, purely local like profile
names -- the firmware struct has no room for strings, and
pack_config/unpack_config already only touch type/data so the extra
key round-trips harmlessly.
Two things had to be handled carefully: changing a button's key/type
must not wipe its note (all set_button_*/set_encoder_* setters and the
edit dialog now carry the previous note forward), and re-reading from
the board must not erase notes either, since the firmware doesn't know
about them -- versapad_combined.merge_notes() restores them onto the
freshly-fetched state by button/encoder index.
Editable via the Programmiermodus dialog (new text field), visible on
both the desktop card (grown from 84 to 114px to fit it) and the
browser view. MCP server gets set_button_note()/set_encoder_note() so
notes can be set programmatically too.
This commit is contained in:
parent
9f0ae12794
commit
4b5da69174
8 changed files with 173 additions and 41 deletions
|
|
@ -61,7 +61,17 @@ def _profile_switch_data(target):
|
|||
|
||||
|
||||
def _describe_action(action):
|
||||
return {"type": action["type"], "data": action["data"], "label": vp.action_label(action)}
|
||||
return {"type": action["type"], "data": action["data"], "label": vp.action_label(action),
|
||||
"note": action.get("note", "")}
|
||||
|
||||
|
||||
def _replace_action(old_action, new_type, new_data):
|
||||
"""Baut eine neue Action mit neuem Typ/Daten, behaelt aber die Notiz vom
|
||||
vorherigen Stand bei (rein lokal, unabhaengig davon was die Aktion tut --
|
||||
ein Tastenwechsel soll die Beschreibung 'was der Button macht' nicht
|
||||
loeschen). Zum Loeschen explizit set_button_note()/set_encoder_note()
|
||||
mit leerem String."""
|
||||
return {"type": new_type, "data": new_data, "note": old_action.get("note", "")}
|
||||
|
||||
|
||||
# ── Lesen ────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -138,7 +148,7 @@ def set_button_key(profile: int, index: int, key: str, modifiers: list[str] = []
|
|||
btn = _find(_profile(profile)["buttons"], index)
|
||||
keycode = vp.hid_key_code_for_name(key)
|
||||
mod_bits = vp.modifier_bits_for_names(modifiers)
|
||||
btn["action"] = {"type": "HidKey", "data": (mod_bits << 8) | keycode}
|
||||
btn["action"] = _replace_action(btn["action"], "HidKey", (mod_bits << 8) | keycode)
|
||||
return _describe_action(btn["action"])
|
||||
|
||||
|
||||
|
|
@ -148,7 +158,7 @@ def set_button_consumer(profile: int, index: int, consumer: str) -> dict:
|
|||
'Lauter', 'Leiser', 'Nächster Titel', 'Vorheriger Titel'."""
|
||||
btn = _find(_profile(profile)["buttons"], index)
|
||||
cid = vp.consumer_id_for_name(consumer)
|
||||
btn["action"] = {"type": "HidConsumer", "data": cid}
|
||||
btn["action"] = _replace_action(btn["action"], "HidConsumer", cid)
|
||||
return _describe_action(btn["action"])
|
||||
|
||||
|
||||
|
|
@ -157,7 +167,7 @@ def set_button_macro(profile: int, index: int, slot: int) -> dict:
|
|||
"""Belegt einen MX-Button mit einem Makro-Slot (0-31). Die Schritte selbst
|
||||
mit set_macro() befuellen."""
|
||||
btn = _find(_profile(profile)["buttons"], index)
|
||||
btn["action"] = {"type": "Macro", "data": slot}
|
||||
btn["action"] = _replace_action(btn["action"], "Macro", slot)
|
||||
return _describe_action(btn["action"])
|
||||
|
||||
|
||||
|
|
@ -165,15 +175,27 @@ def set_button_macro(profile: int, index: int, slot: int) -> dict:
|
|||
def set_button_profile_switch(profile: int, index: int, target) -> dict:
|
||||
"""Belegt einen MX-Button mit Profilwechsel. target: 'next' (Zyklus) oder 0/1/2."""
|
||||
btn = _find(_profile(profile)["buttons"], index)
|
||||
btn["action"] = {"type": "ProfileSwitch", "data": _profile_switch_data(target)}
|
||||
btn["action"] = _replace_action(btn["action"], "ProfileSwitch", _profile_switch_data(target))
|
||||
return _describe_action(btn["action"])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def set_button_none(profile: int, index: int) -> dict:
|
||||
"""Entfernt die Belegung eines MX-Buttons (Action = None)."""
|
||||
"""Entfernt die Belegung eines MX-Buttons (Action = None). Notiz bleibt
|
||||
erhalten -- zum Loeschen set_button_note(profile, index, "")."""
|
||||
btn = _find(_profile(profile)["buttons"], index)
|
||||
btn["action"] = {"type": "None", "data": 0}
|
||||
btn["action"] = _replace_action(btn["action"], "None", 0)
|
||||
return _describe_action(btn["action"])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def set_button_note(profile: int, index: int, note: str) -> dict:
|
||||
"""Setzt/aendert die freie Notiz eines MX-Buttons -- was der Button tut,
|
||||
unabhaengig von der technischen Aktion (z.B. 'Speichern in Fusion 360').
|
||||
Rein lokal, landet nie aufs Board (wie Profilnamen). Leerer String
|
||||
loescht die Notiz."""
|
||||
btn = _find(_profile(profile)["buttons"], index)
|
||||
btn["action"]["note"] = note
|
||||
return _describe_action(btn["action"])
|
||||
|
||||
|
||||
|
|
@ -205,7 +227,7 @@ def set_encoder_key(profile: int, index: int, field: str, key: str, modifiers: l
|
|||
enc, f = _encoder_field(profile, index, field)
|
||||
keycode = vp.hid_key_code_for_name(key)
|
||||
mod_bits = vp.modifier_bits_for_names(modifiers)
|
||||
enc[f] = {"type": "HidKey", "data": (mod_bits << 8) | keycode}
|
||||
enc[f] = _replace_action(enc[f], "HidKey", (mod_bits << 8) | keycode)
|
||||
return _describe_action(enc[f])
|
||||
|
||||
|
||||
|
|
@ -213,7 +235,7 @@ def set_encoder_key(profile: int, index: int, field: str, key: str, modifiers: l
|
|||
def set_encoder_consumer(profile: int, index: int, field: str, consumer: str) -> dict:
|
||||
"""Belegt eine Encoder-Aktion mit einer Medientaste."""
|
||||
enc, f = _encoder_field(profile, index, field)
|
||||
enc[f] = {"type": "HidConsumer", "data": vp.consumer_id_for_name(consumer)}
|
||||
enc[f] = _replace_action(enc[f], "HidConsumer", vp.consumer_id_for_name(consumer))
|
||||
return _describe_action(enc[f])
|
||||
|
||||
|
||||
|
|
@ -221,7 +243,7 @@ def set_encoder_consumer(profile: int, index: int, field: str, consumer: str) ->
|
|||
def set_encoder_macro(profile: int, index: int, field: str, slot: int) -> dict:
|
||||
"""Belegt eine Encoder-Aktion mit einem Makro-Slot (0-31)."""
|
||||
enc, f = _encoder_field(profile, index, field)
|
||||
enc[f] = {"type": "Macro", "data": slot}
|
||||
enc[f] = _replace_action(enc[f], "Macro", slot)
|
||||
return _describe_action(enc[f])
|
||||
|
||||
|
||||
|
|
@ -231,15 +253,26 @@ def set_encoder_profile_switch(profile: int, index: int, field: str, target) ->
|
|||
Achtung: Encoder 0 'sw' ist normalerweise auf allen 3 Profilen der
|
||||
Profilwechsel -- nicht ohne Ruecksprache mit dem User aendern."""
|
||||
enc, f = _encoder_field(profile, index, field)
|
||||
enc[f] = {"type": "ProfileSwitch", "data": _profile_switch_data(target)}
|
||||
enc[f] = _replace_action(enc[f], "ProfileSwitch", _profile_switch_data(target))
|
||||
return _describe_action(enc[f])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def set_encoder_none(profile: int, index: int, field: str) -> dict:
|
||||
"""Entfernt eine Encoder-Belegung (Action = None)."""
|
||||
"""Entfernt eine Encoder-Belegung (Action = None). Notiz bleibt erhalten
|
||||
-- zum Loeschen set_encoder_note(profile, index, field, "")."""
|
||||
enc, f = _encoder_field(profile, index, field)
|
||||
enc[f] = {"type": "None", "data": 0}
|
||||
enc[f] = _replace_action(enc[f], "None", 0)
|
||||
return _describe_action(enc[f])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def set_encoder_note(profile: int, index: int, field: str, note: str) -> dict:
|
||||
"""Setzt/aendert die freie Notiz einer Encoder-Aktion (sw/cw/ccw) -- was
|
||||
sie tut, unabhaengig von der technischen Aktion. Rein lokal, landet nie
|
||||
aufs Board. Leerer String loescht die Notiz."""
|
||||
enc, f = _encoder_field(profile, index, field)
|
||||
enc[f]["note"] = note
|
||||
return _describe_action(enc[f])
|
||||
|
||||
|
||||
|
|
@ -303,10 +336,10 @@ def load_local(path: str = None) -> dict:
|
|||
@mcp.tool()
|
||||
def load_from_board() -> dict:
|
||||
"""Liest die komplette Config + Makros vom Board (per Serial, ~1-2s) und
|
||||
ersetzt damit den In-Memory-State. Profilnamen bleiben erhalten (die
|
||||
kennt nur wir, nicht das Board). Schlaegt fehl, wenn der COM-Port gerade
|
||||
von VersaGUI/dem Tkinter-Viewer gehalten wird. Gibt den COM-Port danach
|
||||
sofort wieder frei (siehe get_board_status())."""
|
||||
ersetzt damit den In-Memory-State. Profilnamen UND Notizen bleiben
|
||||
erhalten (kennt nur wir, nicht das Board). Schlaegt fehl, wenn der
|
||||
COM-Port gerade von VersaGUI/dem Tkinter-Viewer gehalten wird. Gibt den
|
||||
COM-Port danach sofort wieder frei (siehe get_board_status())."""
|
||||
try:
|
||||
raw_cfg = _link.read_full_config()
|
||||
if raw_cfg is None:
|
||||
|
|
@ -321,7 +354,9 @@ def load_from_board() -> dict:
|
|||
macro_slots = vproto.unpack_macros(raw_macros)
|
||||
|
||||
names = _cfg()["profile_names"]
|
||||
_state["combined"] = vcomb.from_binary(cfg_dict, macro_slots, profile_names=names)
|
||||
previous = _state["combined"]
|
||||
_state["combined"] = vcomb.merge_notes(
|
||||
vcomb.from_binary(cfg_dict, macro_slots, profile_names=names), previous)
|
||||
return list_profiles()
|
||||
finally:
|
||||
_link.close()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue