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>
This commit is contained in:
+66
-19
@@ -6,11 +6,13 @@ from tkinter import filedialog, messagebox
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import app_state
|
||||
import live_hooks
|
||||
import palette
|
||||
import schema
|
||||
import undo
|
||||
import wiresheet
|
||||
from sax_dom import (
|
||||
find_component_by_path,
|
||||
find_parent_element,
|
||||
get_component_path,
|
||||
restore_children,
|
||||
@@ -253,6 +255,18 @@ def execute_direct_overwrite_save():
|
||||
messagebox.showerror("Save Error", f"Failed to write direct update stream:\n{str(e)}")
|
||||
|
||||
|
||||
def write_sax_to_path(path):
|
||||
"""Writes the current DOM to a .sax file at the given path."""
|
||||
if app_state.xml_root_element is None:
|
||||
raise RuntimeError("No DOM to write")
|
||||
sync_live_metadata_to_dom()
|
||||
with open(path, "wb") as f:
|
||||
f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||
ET.ElementTree(app_state.xml_root_element).write(
|
||||
f, encoding="utf-8", xml_declaration=False
|
||||
)
|
||||
|
||||
|
||||
def save_file_as():
|
||||
"""Saves the tracking tree document hierarchy targeting an explicit location choice dialog."""
|
||||
if not app_state.current_file_path or app_state.xml_root_element is None:
|
||||
@@ -754,9 +768,16 @@ def add_component(kit_name, type_name, grid_x, grid_y):
|
||||
reset_to_startup_state()
|
||||
return None
|
||||
|
||||
comp_id = allocate_component_id()
|
||||
comp_name = unique_child_name(parent, type_name)
|
||||
|
||||
if live_hooks.is_live():
|
||||
new_id = live_hooks.live_add_component(parent, kit_name, type_name, comp_name)
|
||||
if new_id is None:
|
||||
return None
|
||||
comp_id = new_id
|
||||
else:
|
||||
comp_id = allocate_component_id()
|
||||
|
||||
element = ET.Element("comp")
|
||||
element.set("name", comp_name)
|
||||
element.set("id", str(comp_id))
|
||||
@@ -769,18 +790,19 @@ def add_component(kit_name, type_name, grid_x, grid_y):
|
||||
index = len(list(parent))
|
||||
parent.append(element)
|
||||
|
||||
undo.push(undo.AddComponentCommand(
|
||||
parent, index, element,
|
||||
label=f"add {comp_name}"
|
||||
))
|
||||
if not app_state.live_mode:
|
||||
undo.push(undo.AddComponentCommand(
|
||||
parent, index, element,
|
||||
label=f"add {comp_name}"
|
||||
))
|
||||
|
||||
app_state.mark_dirty()
|
||||
refresh_treeview_from_dom(app_state.xml_root_element)
|
||||
wiresheet.render_current_sheet()
|
||||
|
||||
# The schema is only reconciled on save, so the placement says so here rather than
|
||||
# letting the save dialog be the first the user hears of it.
|
||||
if kit_name in app_state.pending_schema_kits:
|
||||
if app_state.live_mode:
|
||||
app_state.set_status(f"{comp_name} ({kit_name}::{type_name}) added to device.")
|
||||
elif kit_name in app_state.pending_schema_kits:
|
||||
app_state.set_status(
|
||||
f"{comp_name} ({kit_name}::{type_name}) added — '{kit_name}' is not in this "
|
||||
"app's schema and will be declared when you save.",
|
||||
@@ -801,20 +823,31 @@ def remove_link(link_element):
|
||||
if links_elem is None:
|
||||
return
|
||||
|
||||
from_ref = link_element.get("from", "")
|
||||
to_ref = link_element.get("to", "")
|
||||
|
||||
if live_hooks.is_live():
|
||||
from_comp_path, from_slot_name = from_ref.rsplit(".", 1) if "." in from_ref else ("", from_ref)
|
||||
to_comp_path, to_slot_name = to_ref.rsplit(".", 1) if "." in to_ref else ("", to_ref)
|
||||
from_elem = find_component_by_path(app_state.xml_root_element, from_comp_path)
|
||||
to_elem = find_component_by_path(app_state.xml_root_element, to_comp_path)
|
||||
if from_elem is not None and to_elem is not None:
|
||||
if not live_hooks.live_delete_link(from_elem, from_slot_name, to_elem, to_slot_name):
|
||||
return
|
||||
|
||||
try:
|
||||
index = list(links_elem).index(link_element)
|
||||
links_elem.remove(link_element)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
from_ref = link_element.get("from", "")
|
||||
to_ref = link_element.get("to", "")
|
||||
from_slot = from_ref.rsplit(".", 1)[-1] if "." in from_ref else from_ref
|
||||
to_slot = to_ref.rsplit(".", 1)[-1] if "." in to_ref else to_ref
|
||||
undo.push(undo.DeleteLinkCommand(
|
||||
links_elem, index, link_element,
|
||||
label=f"delete link {from_slot}→{to_slot}"
|
||||
))
|
||||
if not app_state.live_mode:
|
||||
undo.push(undo.DeleteLinkCommand(
|
||||
links_elem, index, link_element,
|
||||
label=f"delete link {from_slot}→{to_slot}"
|
||||
))
|
||||
|
||||
app_state.selected_link_tag = None
|
||||
app_state.mark_dirty()
|
||||
@@ -830,16 +863,26 @@ def add_link(from_path_slot, to_path_slot):
|
||||
if links_elem is None:
|
||||
links_elem = ET.SubElement(app_state.xml_root_element, "links")
|
||||
|
||||
if live_hooks.is_live():
|
||||
from_comp_path, from_slot_name = from_path_slot.rsplit(".", 1) if "." in from_path_slot else ("", from_path_slot)
|
||||
to_comp_path, to_slot_name = to_path_slot.rsplit(".", 1) if "." in to_path_slot else ("", to_path_slot)
|
||||
from_elem = find_component_by_path(app_state.xml_root_element, from_comp_path)
|
||||
to_elem = find_component_by_path(app_state.xml_root_element, to_comp_path)
|
||||
if from_elem is not None and to_elem is not None:
|
||||
if not live_hooks.live_add_link(from_elem, from_slot_name, to_elem, to_slot_name):
|
||||
return
|
||||
|
||||
link_elem = ET.SubElement(links_elem, "link")
|
||||
link_elem.set("from", from_path_slot)
|
||||
link_elem.set("to", to_path_slot)
|
||||
|
||||
from_slot = from_path_slot.rsplit(".", 1)[-1] if "." in from_path_slot else from_path_slot
|
||||
to_slot = to_path_slot.rsplit(".", 1)[-1] if "." in to_path_slot else to_path_slot
|
||||
undo.push(undo.AddLinkCommand(
|
||||
links_elem, link_elem,
|
||||
label=f"add link {from_slot}→{to_slot}"
|
||||
))
|
||||
if not app_state.live_mode:
|
||||
undo.push(undo.AddLinkCommand(
|
||||
links_elem, link_elem,
|
||||
label=f"add link {from_slot}→{to_slot}"
|
||||
))
|
||||
|
||||
app_state.mark_dirty()
|
||||
wiresheet.render_current_sheet()
|
||||
@@ -861,6 +904,10 @@ def execute_cascading_deletion(comp_element):
|
||||
if not confirm:
|
||||
return False
|
||||
|
||||
if live_hooks.is_live():
|
||||
if not live_hooks.live_delete_component(comp_element):
|
||||
return False
|
||||
|
||||
comp_path = get_component_path(comp_element, app_state.xml_root_element)
|
||||
|
||||
all_elements_to_prune = [comp_element] + descendant_comps
|
||||
@@ -896,7 +943,7 @@ def execute_cascading_deletion(comp_element):
|
||||
comp_index = list(xml_parent).index(comp_element)
|
||||
xml_parent.remove(comp_element)
|
||||
|
||||
if xml_parent is not None:
|
||||
if xml_parent is not None and not app_state.live_mode:
|
||||
undo.push(undo.DeleteComponentCommand(
|
||||
xml_parent, comp_index, comp_element, link_records,
|
||||
label=f"delete {comp_name}"
|
||||
|
||||
Reference in New Issue
Block a user