Files
sedona_editor/live_hooks.py
T
ArdaandClaude Opus 4.6 dafebcebda Live Editor: add DASP/Sox device connection with full CRUD and COV
Port the DASP transport and Sox protocol stack from sedonaOntrol Java
to Python, enabling live device editing alongside the existing offline
SAX editor. Lazy-loads components on navigation, caches them, and
subscribes to COV events for the active wiresheet.

New files: dasp.py, sox.py, device_session.py, connect_dialog.py,
live_hooks.py. Modified: app_state.py (live-mode state), main.py
(Device menu, COV polling, lazy tree expand), properties.py (Sox
write-through), sax_file.py (structural mutations route through
live_hooks), schema.py (find_manifest helper).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-11 23:31:36 +03:00

252 lines
7.8 KiB
Python

"""
Write-through interception layer for live device mode.
Checks app_state.live_mode and routes mutations through DeviceSession/SoxClient.
In offline mode, falls through to the original DOM-only code paths.
"""
import app_state
from sox import type_id_for_slot
def is_live():
return (app_state.live_mode
and app_state.device_session is not None
and app_state.device_session.is_connected)
# ── Property Write ──────────────────────────────────────────────
def live_set_prop_value(element, slot_name, value_str):
"""In live mode, sends a Sox write to the device. Returns True on success."""
if not is_live():
return False
ds = app_state.device_session
comp_id = _comp_id(element)
if comp_id is None:
return False
slot_info = _find_slot(ds, comp_id, slot_name)
if slot_info is None:
return False
type_id = type_id_for_slot(slot_info["type_name"])
coerced = _coerce_value(type_id, value_str)
try:
ds.sox_client.write_prop(comp_id, slot_info["id"], type_id, coerced)
return True
except Exception as e:
app_state.set_status(f"Write failed: {e}", warn=True)
return False
# ── Add Component ───────────────────────────────────────────────
def live_add_component(parent_element, kit_name, type_name, comp_name, config_values=None):
"""In live mode, sends a Sox add to the device. Returns new comp_id or None."""
if not is_live():
return None
ds = app_state.device_session
parent_id = _comp_id(parent_element)
if parent_id is None:
return None
kit_id = _kit_id(ds, kit_name)
type_id_val = _type_id(ds, kit_id, type_name)
if kit_id is None or type_id_val is None:
return None
sox_config = []
if config_values:
for tid, val in config_values:
sox_config.append((tid, val))
try:
new_comp = ds.sox_client.add(parent_id, kit_id, type_id_val, comp_name, sox_config)
return new_comp.id
except Exception as e:
app_state.set_status(f"Add failed: {e}", warn=True)
return None
# ── Delete Component ────────────────────────────────────────────
def live_delete_component(element):
"""In live mode, sends a Sox delete to the device. Returns True on success."""
if not is_live():
return False
ds = app_state.device_session
comp_id = _comp_id(element)
if comp_id is None:
return False
try:
ds.sox_client.delete(comp_id)
return True
except Exception as e:
app_state.set_status(f"Delete failed: {e}", warn=True)
return False
# ── Rename Component ────────────────────────────────────────────
def live_rename_component(element, new_name):
"""In live mode, sends a Sox rename to the device. Returns True on success."""
if not is_live():
return False
ds = app_state.device_session
comp_id = _comp_id(element)
if comp_id is None:
return False
try:
ds.sox_client.rename(comp_id, new_name)
return True
except Exception as e:
app_state.set_status(f"Rename failed: {e}", warn=True)
return False
# ── Link / Unlink ───────────────────────────────────────────────
def live_add_link(from_element, from_slot_name, to_element, to_slot_name):
"""In live mode, sends a Sox link command. Returns True on success."""
if not is_live():
return False
ds = app_state.device_session
from_id = _comp_id(from_element)
to_id = _comp_id(to_element)
if from_id is None or to_id is None:
return False
from_slot = _find_slot(ds, from_id, from_slot_name)
to_slot = _find_slot(ds, to_id, to_slot_name)
if from_slot is None or to_slot is None:
return False
try:
ds.sox_client.link(from_id, from_slot["id"], to_id, to_slot["id"])
return True
except Exception as e:
app_state.set_status(f"Link failed: {e}", warn=True)
return False
def live_delete_link(from_element, from_slot_name, to_element, to_slot_name):
"""In live mode, sends a Sox unlink command. Returns True on success."""
if not is_live():
return False
ds = app_state.device_session
from_id = _comp_id(from_element)
to_id = _comp_id(to_element)
if from_id is None or to_id is None:
return False
from_slot = _find_slot(ds, from_id, from_slot_name)
to_slot = _find_slot(ds, to_id, to_slot_name)
if from_slot is None or to_slot is None:
return False
try:
ds.sox_client.unlink(from_id, from_slot["id"], to_id, to_slot["id"])
return True
except Exception as e:
app_state.set_status(f"Unlink failed: {e}", warn=True)
return False
# ── Invoke Action ───────────────────────────────────────────────
def live_invoke(element, slot_name, arg=None):
"""In live mode, sends a Sox invoke. Returns True on success."""
if not is_live():
return False
ds = app_state.device_session
comp_id = _comp_id(element)
if comp_id is None:
return False
slot_info = _find_slot(ds, comp_id, slot_name)
if slot_info is None:
return False
try:
ds.sox_client.invoke(comp_id, slot_info["id"])
return True
except Exception as e:
app_state.set_status(f"Invoke failed: {e}", warn=True)
return False
# ── Helpers ─────────────────────────────────────────────────────
def _comp_id(element):
cid = element.get("id")
if cid is None:
return None
try:
return int(cid)
except ValueError:
return None
def _find_slot(ds, comp_id, slot_name):
comp = ds.sox_client.cache_get(comp_id)
if comp is None:
return None
slots = ds.sox_client.slot_registry.get((comp.kit_id, comp.type_id))
if slots is None:
return None
for s in slots:
if s["name"] == slot_name:
return s
return None
def _kit_id(ds, kit_name):
if ds.sox_client.schema_parts:
for i, part in enumerate(ds.sox_client.schema_parts):
if part.name == kit_name:
return i
return None
def _type_id(ds, kit_id, type_name):
if kit_id is None:
return None
for (kid, tid), slots in ds.sox_client.slot_registry.items():
if kid == kit_id:
comp = ds.sox_client.cache_get(0)
if comp and comp.type_name.endswith(f"::{type_name}"):
return tid
# fallback: search all cached components for a matching type_name
for c in ds.sox_client.cache:
if c is not None and c.kit_id == kit_id and c.type_name.endswith(f"::{type_name}"):
return c.type_id
return None
def _coerce_value(type_id, value_str):
from sox import BOOL_ID, BYTE_ID, SHORT_ID, INT_ID, LONG_ID, FLOAT_ID, DOUBLE_ID, STR_ID
if type_id == BOOL_ID:
return value_str.lower() in ("true", "1", "yes")
elif type_id in (BYTE_ID, SHORT_ID, INT_ID):
return int(value_str)
elif type_id == LONG_ID:
return int(value_str)
elif type_id == FLOAT_ID:
return float(value_str)
elif type_id == DOUBLE_ID:
return float(value_str)
elif type_id == STR_ID:
return str(value_str)
return value_str