Close the serial link after each MCP board operation
VersaPadLink never closes itself; get_board_status()/load_from_board()/
write_to_board() were leaving the exclusive COM port open for the rest of
the MCP server process's lifetime after a single call. That locked out
Live-Sync, the official VersaGUI, and even the MCP server's own next call
with "busy", live-observed today after a single write_to_board() call. Each
of the three now closes the link in a finally block regardless of outcome.
Also documented in AGENTS.md: this environment can run several independent
versapad_mcp_server.py processes at once, each with its own in-memory
state, which caused a write_to_board() call to silently write blank data
from a fresh process instead of the config that had just been built up on
another one (ACK still said {"ok": true}). Recommended workaround noted
there: load_local() right before write_to_board(), and read back with
load_from_board() + get_profile() afterwards instead of trusting the ACK.
This commit is contained in:
parent
09fbd6ad96
commit
7d40fdaa60
2 changed files with 62 additions and 24 deletions
|
|
@ -116,9 +116,15 @@ def get_macro(slot: int) -> dict:
|
|||
def get_board_status() -> dict:
|
||||
"""Prueft per Serial, ob das Board erreichbar ist und welches Profil dort
|
||||
gerade aktiv ist. Schlaegt fehl/liefert busy, wenn VersaGUI oder der
|
||||
Tkinter-Viewer den COM-Port gerade halten."""
|
||||
profile = _link.read_active_profile()
|
||||
return {"connected": profile is not None, "active_profile": profile, "error": _link.last_error}
|
||||
Tkinter-Viewer den COM-Port gerade halten. Schliesst die Verbindung
|
||||
danach wieder (siehe write_to_board() fuer den Grund) -- der Port
|
||||
ist exklusiv, ein einzelner Status-Check darf ihn nicht dauerhaft
|
||||
fuer Live-Sync/VersaGUI blockieren."""
|
||||
try:
|
||||
profile = _link.read_active_profile()
|
||||
return {"connected": profile is not None, "active_profile": profile, "error": _link.last_error}
|
||||
finally:
|
||||
_link.close()
|
||||
|
||||
|
||||
# ── Buttons (20 pro Profil, MX-Matrix) ───────────────────────────────────────
|
||||
|
|
@ -299,22 +305,26 @@ 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."""
|
||||
raw_cfg = _link.read_full_config()
|
||||
if raw_cfg is None:
|
||||
raise RuntimeError(f"Config laden fehlgeschlagen: {_link.last_error}")
|
||||
raw_macros = _link.read_macros()
|
||||
if raw_macros is None:
|
||||
raise RuntimeError(f"Makros laden fehlgeschlagen: {_link.last_error}")
|
||||
von VersaGUI/dem Tkinter-Viewer gehalten wird. Schliesst die Verbindung
|
||||
danach wieder (siehe write_to_board() fuer den Grund)."""
|
||||
try:
|
||||
raw_cfg = _link.read_full_config()
|
||||
if raw_cfg is None:
|
||||
raise RuntimeError(f"Config laden fehlgeschlagen: {_link.last_error}")
|
||||
raw_macros = _link.read_macros()
|
||||
if raw_macros is None:
|
||||
raise RuntimeError(f"Makros laden fehlgeschlagen: {_link.last_error}")
|
||||
|
||||
cfg_dict = vproto.unpack_config(raw_cfg)
|
||||
if not (cfg_dict["magic_ok"] and cfg_dict["crc_ok"]):
|
||||
raise RuntimeError("Board-Antwort ungueltig (Magic/CRC)")
|
||||
macro_slots = vproto.unpack_macros(raw_macros)
|
||||
cfg_dict = vproto.unpack_config(raw_cfg)
|
||||
if not (cfg_dict["magic_ok"] and cfg_dict["crc_ok"]):
|
||||
raise RuntimeError("Board-Antwort ungueltig (Magic/CRC)")
|
||||
macro_slots = vproto.unpack_macros(raw_macros)
|
||||
|
||||
names = _cfg()["profile_names"]
|
||||
_state["combined"] = vcomb.from_binary(cfg_dict, macro_slots, profile_names=names)
|
||||
return list_profiles()
|
||||
names = _cfg()["profile_names"]
|
||||
_state["combined"] = vcomb.from_binary(cfg_dict, macro_slots, profile_names=names)
|
||||
return list_profiles()
|
||||
finally:
|
||||
_link.close()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
|
@ -322,13 +332,21 @@ def write_to_board() -> dict:
|
|||
"""Schreibt den kompletten In-Memory-State (alle 3 Profile + Makros) aufs
|
||||
Board -- ueberschreibt, was dort aktuell im NVM steht. Firmware prueft
|
||||
Magic/CRC/Keycode-Bereich vor jedem Schreiben und antwortet sonst nur mit
|
||||
NACK (kein Risiko fuer Datenmuell). Schlaegt fehl bei belegtem COM-Port."""
|
||||
cfg_bytes, macro_bytes = vcomb.to_binary(_cfg())
|
||||
if not _link.write_full_config(cfg_bytes):
|
||||
raise RuntimeError(f"Config-Schreiben fehlgeschlagen: {_link.last_error}")
|
||||
if not _link.write_macros(macro_bytes):
|
||||
raise RuntimeError(f"Makros-Schreiben fehlgeschlagen: {_link.last_error}")
|
||||
return {"ok": True}
|
||||
NACK (kein Risiko fuer Datenmuell). Schlaegt fehl bei belegtem COM-Port.
|
||||
Schliesst die Verbindung danach wieder -- VersaPadLink haelt den Port
|
||||
sonst dauerhaft offen (kein automatisches Schliessen nach einem Befehl),
|
||||
was Live-Sync/VersaGUI/den naechsten MCP-Aufruf sonst dauerhaft mit
|
||||
"busy" blockieren wuerde, obwohl der eigentliche Vorgang laengst fertig
|
||||
ist -- der Port ist exklusiv (siehe versapad_serial.py)."""
|
||||
try:
|
||||
cfg_bytes, macro_bytes = vcomb.to_binary(_cfg())
|
||||
if not _link.write_full_config(cfg_bytes):
|
||||
raise RuntimeError(f"Config-Schreiben fehlgeschlagen: {_link.last_error}")
|
||||
if not _link.write_macros(macro_bytes):
|
||||
raise RuntimeError(f"Makros-Schreiben fehlgeschlagen: {_link.last_error}")
|
||||
return {"ok": True}
|
||||
finally:
|
||||
_link.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue