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:
@@ -5,7 +5,11 @@ import sys
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, font as tkfont, messagebox, ttk
|
||||
|
||||
import threading
|
||||
|
||||
import app_state
|
||||
import connect_dialog
|
||||
import device_session
|
||||
import link_view
|
||||
import manage_kits
|
||||
import palette
|
||||
@@ -393,6 +397,9 @@ def _clear_restoring_flag():
|
||||
|
||||
|
||||
def do_undo(event=None):
|
||||
if app_state.live_mode:
|
||||
app_state.set_status("Undo is not available in live mode", warn=True)
|
||||
return "break"
|
||||
label = undo.undo()
|
||||
if label is None:
|
||||
return "break"
|
||||
@@ -401,6 +408,9 @@ def do_undo(event=None):
|
||||
|
||||
|
||||
def do_redo(event=None):
|
||||
if app_state.live_mode:
|
||||
app_state.set_status("Redo is not available in live mode", warn=True)
|
||||
return "break"
|
||||
label = undo.redo()
|
||||
if label is None:
|
||||
return "break"
|
||||
@@ -409,10 +419,32 @@ def do_redo(event=None):
|
||||
|
||||
|
||||
def do_save(event=None):
|
||||
if app_state.live_mode:
|
||||
do_download_to_sax()
|
||||
return "break"
|
||||
sax_file.execute_direct_overwrite_save()
|
||||
return "break"
|
||||
|
||||
|
||||
def do_download_to_sax():
|
||||
"""In live mode, saves the current device DOM snapshot as a .sax file."""
|
||||
if app_state.xml_root_element is None:
|
||||
return
|
||||
path = filedialog.asksaveasfilename(
|
||||
title="Download to SAX",
|
||||
defaultextension=".sax",
|
||||
filetypes=[("Sedona SAX", "*.sax"), ("All files", "*.*")],
|
||||
initialfile=f"{app_state.device_host}.sax"
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
sax_file.write_sax_to_path(path)
|
||||
app_state.set_status(f"Downloaded to {os.path.basename(path)}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Download Failed", str(e))
|
||||
|
||||
|
||||
NL = chr(10) # newline, for the multi-line message boxes
|
||||
|
||||
|
||||
@@ -578,6 +610,23 @@ def on_tree_select(event):
|
||||
app_state.current_sheet_parent = element
|
||||
record_visit(VIEW_SHEET, element)
|
||||
|
||||
if app_state.live_mode and app_state.device_session and element is not None:
|
||||
comp_id_str = element.get("id")
|
||||
if comp_id_str is not None:
|
||||
ds = app_state.device_session
|
||||
ds.close_wiresheet()
|
||||
try:
|
||||
ds.open_wiresheet(int(comp_id_str))
|
||||
sax_root = ds.build_dom_tree(ds.sox_client.cache_get(0))
|
||||
ds.build_links_elements(sax_root)
|
||||
app_state.xml_root_element = sax_root
|
||||
sax_file.refresh_treeview_from_dom(sax_root)
|
||||
# re-find the element after DOM rebuild
|
||||
element = app_state.tree_element_map.get(selected_id)
|
||||
app_state.current_sheet_parent = element
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if element is not None:
|
||||
components_to_render = [child for child in element if child.tag == "comp"]
|
||||
else:
|
||||
@@ -1290,7 +1339,18 @@ def attach_tree_scrollbars(container, tree):
|
||||
|
||||
# Resizing, and opening or closing a branch, both change what the widest row is.
|
||||
tree.bind("<Configure>", lambda event: fit_tree_column(tree), add="+")
|
||||
tree.bind("<<TreeviewOpen>>", lambda event: tree.after_idle(fit_tree_column, tree), add="+")
|
||||
def _on_tree_open(event):
|
||||
tree.after_idle(fit_tree_column, tree)
|
||||
if app_state.live_mode:
|
||||
sel = tree.selection()
|
||||
if sel:
|
||||
elem = app_state.tree_element_map.get(sel[0])
|
||||
if elem is not None:
|
||||
cid = elem.get("id")
|
||||
if cid is not None:
|
||||
_live_tree_expand(int(cid))
|
||||
|
||||
tree.bind("<<TreeviewOpen>>", _on_tree_open, add="+")
|
||||
tree.bind("<<TreeviewClose>>", lambda event: tree.after_idle(fit_tree_column, tree), add="+")
|
||||
|
||||
|
||||
@@ -1838,6 +1898,143 @@ def verify_startup_configurations():
|
||||
wiresheet.render_components(app_state.active_components_list)
|
||||
|
||||
|
||||
# ── Live device connection ──────────────────────────────────────
|
||||
|
||||
_cov_poll_id = None
|
||||
|
||||
|
||||
def do_device_connect():
|
||||
"""Opens the connect dialog and initiates a device connection."""
|
||||
connect_dialog.ConnectDialog(app_state.window, _connect_to_device)
|
||||
|
||||
|
||||
def _connect_to_device(host, port, user, password):
|
||||
"""Called from the connect dialog's background thread."""
|
||||
ds = device_session.DeviceSession()
|
||||
root_comp = ds.connect(host, port, user, password)
|
||||
|
||||
app_state.device_session = ds
|
||||
app_state.device_host = host
|
||||
app_state.device_port = port
|
||||
app_state.live_mode = True
|
||||
app_state.connection_status = "connected"
|
||||
|
||||
app_state.window.after(0, lambda: _on_connected(ds, root_comp))
|
||||
return root_comp
|
||||
|
||||
|
||||
def _on_connected(ds, root_comp):
|
||||
"""Main-thread callback after successful connect."""
|
||||
app_state.window.title(app_state.base_window_title_live())
|
||||
menu_device.entryconfig("Connect...", state="disabled")
|
||||
menu_device.entryconfig("Disconnect", state="normal")
|
||||
menu_device.entryconfig("Download to SAX...", state="normal")
|
||||
app_state.set_status(f"Connected to {app_state.device_host}:{app_state.device_port}")
|
||||
|
||||
sax_root = ds.build_dom_tree(root_comp)
|
||||
app_state.xml_root_element = sax_root
|
||||
app_state.current_file_path = f"device://{app_state.device_host}"
|
||||
|
||||
_load_live_schema(ds)
|
||||
sax_file.refresh_treeview_from_dom(sax_root)
|
||||
|
||||
undo.reset()
|
||||
app_state.mark_clean()
|
||||
_start_cov_polling()
|
||||
|
||||
|
||||
def _load_live_schema(ds):
|
||||
"""Loads schema kits from the device's schema parts into the editor registry."""
|
||||
app_state.active_schema_kits.clear()
|
||||
app_state.final_components_registry.clear()
|
||||
if ds.sox_client and ds.sox_client.schema_parts:
|
||||
for part in ds.sox_client.schema_parts:
|
||||
app_state.active_schema_kits[part.name] = f"{part.checksum:08x}"
|
||||
try:
|
||||
schema.load_schema_kits_and_manifests()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def do_device_disconnect():
|
||||
"""Disconnects from the live device."""
|
||||
global _cov_poll_id
|
||||
|
||||
if _cov_poll_id is not None:
|
||||
app_state.window.after_cancel(_cov_poll_id)
|
||||
_cov_poll_id = None
|
||||
|
||||
ds = app_state.device_session
|
||||
if ds is not None:
|
||||
ds.disconnect()
|
||||
|
||||
app_state.device_session = None
|
||||
app_state.live_mode = False
|
||||
app_state.connection_status = "offline"
|
||||
app_state.device_host = ""
|
||||
app_state.device_port = 1876
|
||||
|
||||
app_state.window.title(app_state.base_window_title())
|
||||
menu_device.entryconfig("Connect...", state="normal")
|
||||
menu_device.entryconfig("Disconnect", state="disabled")
|
||||
menu_device.entryconfig("Download to SAX...", state="disabled")
|
||||
app_state.set_status("Disconnected")
|
||||
|
||||
sax_file.reset_to_startup_state()
|
||||
sax_file.refresh_treeview_from_dom(None)
|
||||
wiresheet.render_components([])
|
||||
|
||||
|
||||
def _start_cov_polling():
|
||||
"""Starts the 100ms COV event polling loop on the main thread."""
|
||||
global _cov_poll_id
|
||||
|
||||
def poll():
|
||||
global _cov_poll_id
|
||||
ds = app_state.device_session
|
||||
if ds is None or not ds.is_connected:
|
||||
_cov_poll_id = None
|
||||
if app_state.live_mode:
|
||||
app_state.connection_status = "error"
|
||||
app_state.set_status("Connection lost", warn=True)
|
||||
return
|
||||
|
||||
events = ds.poll_events()
|
||||
if events and app_state.xml_root_element is not None:
|
||||
needs_repaint = False
|
||||
for comp_id, what_char in events:
|
||||
elem = ds.update_element_from_cache(comp_id, app_state.xml_root_element)
|
||||
if elem is not None:
|
||||
needs_repaint = True
|
||||
if needs_repaint:
|
||||
wiresheet.render_current_sheet()
|
||||
if properties_panel is not None:
|
||||
try:
|
||||
properties_panel.draw()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_cov_poll_id = app_state.window.after(100, poll)
|
||||
|
||||
_cov_poll_id = app_state.window.after(100, poll)
|
||||
|
||||
|
||||
def _live_tree_expand(comp_id):
|
||||
"""Called when user expands a tree node in live mode to lazy-load children."""
|
||||
ds = app_state.device_session
|
||||
if ds is None or not ds.is_connected:
|
||||
return
|
||||
|
||||
children = ds.load_children(comp_id)
|
||||
if not children:
|
||||
return
|
||||
|
||||
sax_root = ds.build_dom_tree(ds.sox_client.cache_get(0))
|
||||
ds.build_links_elements(sax_root)
|
||||
app_state.xml_root_element = sax_root
|
||||
sax_file.refresh_treeview_from_dom(sax_root)
|
||||
|
||||
|
||||
# --- UI Setup ---
|
||||
window = tk.Tk()
|
||||
app_state.window = window
|
||||
@@ -1976,6 +2173,13 @@ menu_app_info = tk.Menu(menu_bar, tearoff=0)
|
||||
menu_app_info.add_command(label="App Info", command=show_app_info)
|
||||
menu_bar.add_cascade(label="App", menu=menu_app_info)
|
||||
|
||||
menu_device = tk.Menu(menu_bar, tearoff=0)
|
||||
menu_device.add_command(label="Connect...", command=do_device_connect)
|
||||
menu_device.add_command(label="Disconnect", command=do_device_disconnect, state="disabled")
|
||||
menu_device.add_separator()
|
||||
menu_device.add_command(label="Download to SAX...", command=do_download_to_sax, state="disabled")
|
||||
menu_bar.add_cascade(label="Device", menu=menu_device)
|
||||
|
||||
menu_prefs = tk.Menu(menu_bar, tearoff=0)
|
||||
menu_prefs.add_command(label="Preferences", command=open_preferences_dialog)
|
||||
menu_bar.add_cascade(label="Preferences", menu=menu_prefs)
|
||||
|
||||
Reference in New Issue
Block a user