Compare commits
3
Commits
0d5e5115d9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95be6613f3 | ||
|
|
6dd01b13b9 | ||
|
|
0dddea53f8 |
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.180"
|
VERSION = "0.0.0.183"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
|
|||||||
@@ -92,17 +92,55 @@ def _trace(direction, msg, datagram=None):
|
|||||||
import datetime
|
import datetime
|
||||||
ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
name = _MSG_TYPE_NAMES.get(msg.msg_type, f"?{msg.msg_type}")
|
name = _MSG_TYPE_NAMES.get(msg.msg_type, f"?{msg.msg_type}")
|
||||||
seq = f" seq={msg.seq_num}" if msg.seq_num >= 0 else ""
|
arrow = {"S": ">> SEND", "R": "<< RECV"}.get(direction, direction)
|
||||||
ack = f" ack={msg.ack}" if msg.ack >= 0 else ""
|
payload = msg.payload or b""
|
||||||
plen = len(msg.payload) if msg.payload else 0
|
size = f" ({len(datagram)} bytes)" if datagram else ""
|
||||||
hexdump = ""
|
lines = [f"[DASP] {ts} {arrow} {name:<12} {_trace_fields(msg)}{size}"]
|
||||||
if datagram:
|
if datagram:
|
||||||
hexdump = " [" + datagram.hex(" ") + "]"
|
hdr_len = len(datagram) - len(payload)
|
||||||
line = f"[DASP] {ts} {direction} {name}{seq}{ack} payload={plen}{hexdump}"
|
lines.extend(_hexdump(datagram[:hdr_len], f"header {hdr_len}"))
|
||||||
|
if payload:
|
||||||
|
lines.extend(_hexdump(payload, f"payload {len(payload)}"))
|
||||||
color = _TRACE_COLORS.get(direction)
|
color = _TRACE_COLORS.get(direction)
|
||||||
if color and _console_colors():
|
if color and _console_colors():
|
||||||
line = f"{color}{line}\033[0m"
|
lines = [f"{color}{ln}\033[0m" for ln in lines]
|
||||||
_trace_out(line)
|
_trace_out("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
def _trace_fields(msg):
|
||||||
|
"""The message's header fields as name=value pairs, only those it carries."""
|
||||||
|
f = []
|
||||||
|
if msg.session_id >= 0: f.append(f"session=0x{msg.session_id:04x}")
|
||||||
|
if msg.seq_num >= 0: f.append(f"seq={msg.seq_num}")
|
||||||
|
if msg.ack >= 0: f.append(f"ack={msg.ack}")
|
||||||
|
if msg.ack_more: f.append(f"ackMore={msg.ack_more.hex()}")
|
||||||
|
if msg.version >= 0: f.append(f"version=0x{msg.version:04x}")
|
||||||
|
if msg.remote_id >= 0: f.append(f"remoteId=0x{msg.remote_id:04x}")
|
||||||
|
if msg.username: f.append(f"user={msg.username}")
|
||||||
|
if msg.digest_algorithm: f.append(f"digestAlg={msg.digest_algorithm}")
|
||||||
|
if msg.nonce: f.append(f"nonce=<{len(msg.nonce)} bytes>")
|
||||||
|
if msg.digest: f.append(f"digest=<{len(msg.digest)} bytes>")
|
||||||
|
if msg.ideal_max >= 0: f.append(f"idealMax={msg.ideal_max}")
|
||||||
|
if msg.abs_max >= 0: f.append(f"absMax={msg.abs_max}")
|
||||||
|
if msg.receive_max >= 0: f.append(f"receiveMax={msg.receive_max}")
|
||||||
|
if msg.receive_timeout >= 0: f.append(f"receiveTimeout={msg.receive_timeout // 1000}s")
|
||||||
|
if msg.platform_id: f.append(f"platform={msg.platform_id}")
|
||||||
|
if msg.error_code >= 0:
|
||||||
|
hint = _ERROR_HINTS.get(msg.error_code, "")
|
||||||
|
f.append(f"error=0x{msg.error_code:02x}{hint}")
|
||||||
|
return " ".join(f)
|
||||||
|
|
||||||
|
|
||||||
|
def _hexdump(data, label):
|
||||||
|
"""Hex and ASCII, 16 bytes a row, the first row headed by label."""
|
||||||
|
out = []
|
||||||
|
for off in range(0, len(data), 16):
|
||||||
|
chunk = data[off:off + 16]
|
||||||
|
hexs = chunk.hex(" ").ljust(47)
|
||||||
|
text = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||||
|
head = label if off == 0 else ""
|
||||||
|
out.append(f" {head:<11} {off:04x} {hexs} |{text}|")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# Sent lines yellow, received lines green, so the two directions read apart at a glance.
|
# Sent lines yellow, received lines green, so the two directions read apart at a glance.
|
||||||
@@ -560,7 +598,9 @@ class SendWindow:
|
|||||||
while self._is_alive and (self._full() or self._exceeds_ack_more_limit(p)):
|
while self._is_alive and (self._full() or self._exceeds_ack_more_limit(p)):
|
||||||
self._cond.wait()
|
self._cond.wait()
|
||||||
if not self._is_alive:
|
if not self._is_alive:
|
||||||
return
|
# Waiting for room when the session closed: it must not go out after
|
||||||
|
# the CLOSE, and the caller must not take it as sent.
|
||||||
|
raise DaspException(f"Session closed: {self._session.close_cause}")
|
||||||
if self._tail is None:
|
if self._tail is None:
|
||||||
self._head = self._tail = p
|
self._head = self._tail = p
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ class DeviceSession:
|
|||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
return self.sox_client is not None and not self.sox_client.is_closed
|
return self.sox_client is not None and not self.sox_client.is_closed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def close_cause(self):
|
||||||
|
return self.sox_client.close_cause if self.sox_client else "disconnected"
|
||||||
|
|
||||||
# ── Schema → Slot Registry ──────────────────────────────────
|
# ── Schema → Slot Registry ──────────────────────────────────
|
||||||
|
|
||||||
def _load_slot_registry(self, parts):
|
def _load_slot_registry(self, parts):
|
||||||
|
|||||||
+3
-2
@@ -5,10 +5,11 @@ add_timestamp = True
|
|||||||
box_width = 250
|
box_width = 250
|
||||||
left_pane_width = 230
|
left_pane_width = 230
|
||||||
trace_dasp = True
|
trace_dasp = True
|
||||||
lazy_loading = False
|
lazy_loading = True
|
||||||
|
show_slot_numbers = False
|
||||||
|
|
||||||
[Device]
|
[Device]
|
||||||
host = localhost
|
host = 192.168.0.26
|
||||||
port = 1876
|
port = 1876
|
||||||
user = admin
|
user = admin
|
||||||
|
|
||||||
|
|||||||
@@ -1109,7 +1109,6 @@ def on_tree_select(event):
|
|||||||
|
|
||||||
selected_id = selected_items[0]
|
selected_id = selected_items[0]
|
||||||
element = app_state.tree_element_map.get(selected_id)
|
element = app_state.tree_element_map.get(selected_id)
|
||||||
components_to_render = []
|
|
||||||
|
|
||||||
# Remembered so undo and tidy can repaint this same sheet from the DOM.
|
# Remembered so undo and tidy can repaint this same sheet from the DOM.
|
||||||
app_state.current_sheet_parent = element
|
app_state.current_sheet_parent = element
|
||||||
@@ -1118,42 +1117,16 @@ def on_tree_select(event):
|
|||||||
if app_state.live_mode and app_state.device_session and element is not None:
|
if app_state.live_mode and app_state.device_session and element is not None:
|
||||||
comp_id_str = element.get("id")
|
comp_id_str = element.get("id")
|
||||||
if comp_id_str is not None:
|
if comp_id_str is not None:
|
||||||
ds = app_state.device_session
|
_load_live_sheet(app_state.device_session, comp_id_str, pending_link)
|
||||||
ds.close_wiresheet()
|
return
|
||||||
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 element by comp id after DOM rebuild (tree item IDs changed)
|
|
||||||
element = None
|
|
||||||
for tid, elem in app_state.tree_element_map.items():
|
|
||||||
if elem.get("id") == comp_id_str:
|
|
||||||
element = elem
|
|
||||||
selected_id = tid
|
|
||||||
break
|
|
||||||
if element is not None:
|
|
||||||
_restoring_selection = True
|
|
||||||
tree.selection_set(selected_id)
|
|
||||||
tree.see(selected_id)
|
|
||||||
tree.after_idle(_clear_restoring_flag)
|
|
||||||
app_state.current_sheet_parent = element
|
|
||||||
except Exception:
|
|
||||||
import traceback; traceback.print_exc()
|
|
||||||
|
|
||||||
# The DOM rebuild replaced every element object, so a pending link from the
|
_paint_sheet(element, selected_id, pending_link)
|
||||||
# old tree can no longer be found by identity. Re-resolve it by from/to.
|
|
||||||
if pending_link is not None:
|
|
||||||
old_from = pending_link.get("from", "")
|
|
||||||
old_to = pending_link.get("to", "")
|
|
||||||
links_container = app_state.xml_root_element.find("links") if app_state.xml_root_element is not None else None
|
|
||||||
if links_container is not None:
|
|
||||||
for lnk in links_container:
|
|
||||||
if lnk.get("from") == old_from and lnk.get("to") == old_to:
|
|
||||||
pending_link = lnk
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
|
def _paint_sheet(element, selected_id, pending_link):
|
||||||
|
"""Paints the sheet of `element` (or of the tree row `selected_id` when it has none)."""
|
||||||
|
tree = app_state.tree
|
||||||
|
components_to_render = []
|
||||||
if element is not None:
|
if element is not None:
|
||||||
components_to_render = [child for child in element if child.tag == "comp"]
|
components_to_render = [child for child in element if child.tag == "comp"]
|
||||||
elif tree.exists(selected_id):
|
elif tree.exists(selected_id):
|
||||||
@@ -1170,6 +1143,98 @@ def on_tree_select(event):
|
|||||||
select_link_on_open_sheet(pending_link)
|
select_link_on_open_sheet(pending_link)
|
||||||
|
|
||||||
|
|
||||||
|
# Bumped by every live sheet load, so a load the user has clicked away from is dropped
|
||||||
|
# when it finishes rather than painted over the sheet they moved on to.
|
||||||
|
_sheet_load_gen = 0
|
||||||
|
# One load talks to the device at a time: each starts by unsubscribing the sheet the
|
||||||
|
# previous one subscribed.
|
||||||
|
_sheet_load_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_live_sheet(ds, comp_id_str, pending_link):
|
||||||
|
"""Reads a folder's sheet from the device on a background thread, then paints it.
|
||||||
|
|
||||||
|
Sox requests wait for the device. A device that has gone away holds them until DASP
|
||||||
|
gives up (30 s), which on the UI thread froze the whole window.
|
||||||
|
"""
|
||||||
|
global _sheet_load_gen
|
||||||
|
_sheet_load_gen += 1
|
||||||
|
gen = _sheet_load_gen
|
||||||
|
|
||||||
|
# Blank until the device answers: the rows on screen belong to the DOM the load
|
||||||
|
# is about to replace.
|
||||||
|
wiresheet.render_components([])
|
||||||
|
app_state.set_status("Loading sheet...")
|
||||||
|
|
||||||
|
def _bg():
|
||||||
|
with _sheet_load_lock:
|
||||||
|
if gen != _sheet_load_gen:
|
||||||
|
return # clicked away while waiting for the previous load
|
||||||
|
try:
|
||||||
|
ds.close_wiresheet()
|
||||||
|
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)
|
||||||
|
except Exception as exc:
|
||||||
|
app_state.window.after(
|
||||||
|
0, lambda err=exc: _live_sheet_failed(ds, gen, err))
|
||||||
|
return
|
||||||
|
app_state.window.after(
|
||||||
|
0, lambda: _live_sheet_loaded(ds, gen, comp_id_str, pending_link, sax_root))
|
||||||
|
|
||||||
|
threading.Thread(target=_bg, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def _live_sheet_failed(ds, gen, exc):
|
||||||
|
if gen != _sheet_load_gen or app_state.device_session is not ds:
|
||||||
|
return
|
||||||
|
if not ds.is_connected:
|
||||||
|
_on_connection_lost(ds, ds.close_cause)
|
||||||
|
return
|
||||||
|
app_state.set_status(f"Could not open sheet: {exc}", warn=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _live_sheet_loaded(ds, gen, comp_id_str, pending_link, sax_root):
|
||||||
|
"""UI thread: swaps in the freshly read DOM and paints the sheet."""
|
||||||
|
global _restoring_selection
|
||||||
|
if gen != _sheet_load_gen or app_state.device_session is not ds:
|
||||||
|
return
|
||||||
|
|
||||||
|
tree = app_state.tree
|
||||||
|
app_state.xml_root_element = sax_root
|
||||||
|
sax_file.refresh_treeview_from_dom(sax_root)
|
||||||
|
# re-find element by comp id after DOM rebuild (tree item IDs changed)
|
||||||
|
element = None
|
||||||
|
selected_id = None
|
||||||
|
for tid, elem in app_state.tree_element_map.items():
|
||||||
|
if elem.get("id") == comp_id_str:
|
||||||
|
element = elem
|
||||||
|
selected_id = tid
|
||||||
|
break
|
||||||
|
if element is not None:
|
||||||
|
_restoring_selection = True
|
||||||
|
tree.selection_set(selected_id)
|
||||||
|
tree.see(selected_id)
|
||||||
|
tree.after_idle(_clear_restoring_flag)
|
||||||
|
app_state.current_sheet_parent = element
|
||||||
|
|
||||||
|
# The DOM rebuild replaced every element object, so a pending link from the
|
||||||
|
# old tree can no longer be found by identity. Re-resolve it by from/to.
|
||||||
|
if pending_link is not None:
|
||||||
|
old_from = pending_link.get("from", "")
|
||||||
|
old_to = pending_link.get("to", "")
|
||||||
|
links_container = sax_root.find("links")
|
||||||
|
if links_container is not None:
|
||||||
|
for lnk in links_container:
|
||||||
|
if lnk.get("from") == old_from and lnk.get("to") == old_to:
|
||||||
|
pending_link = lnk
|
||||||
|
break
|
||||||
|
|
||||||
|
app_state.clear_status()
|
||||||
|
if element is not None:
|
||||||
|
_paint_sheet(element, selected_id, pending_link)
|
||||||
|
|
||||||
|
|
||||||
def delete_selected_link():
|
def delete_selected_link():
|
||||||
"""Removes every selected link from the DOM, as one undoable step, and repaints."""
|
"""Removes every selected link from the DOM, as one undoable step, and repaints."""
|
||||||
link_elems = []
|
link_elems = []
|
||||||
@@ -2135,14 +2200,17 @@ def on_canvas_drag(event):
|
|||||||
dy = cy - app_state.drag_data["y"]
|
dy = cy - app_state.drag_data["y"]
|
||||||
xs = [entry["origin"][0] for entry in group]
|
xs = [entry["origin"][0] for entry in group]
|
||||||
ys = [entry["origin"][1] for entry in group]
|
ys = [entry["origin"][1] for entry in group]
|
||||||
dx = min(limit - max(xs), max(margin - min(xs), dx))
|
# A box with an off-sheet input stops a step further in, keeping its knob on the sheet.
|
||||||
|
left = wiresheet.box_left_limits([entry["tag"] for entry in group], margin, step)
|
||||||
|
dx = min(limit - max(xs),
|
||||||
|
max(max(left[e["tag"]] - e["origin"][0] for e in group), dx))
|
||||||
dy = min(limit - max(ys), max(margin - min(ys), dy))
|
dy = min(limit - max(ys), max(margin - min(ys), dy))
|
||||||
|
|
||||||
shifted = False
|
shifted = False
|
||||||
for entry in group:
|
for entry in group:
|
||||||
# Land each box on a step every step of the way.
|
# Land each box on a step every step of the way.
|
||||||
start_x, start_y = entry["origin"]
|
start_x, start_y = entry["origin"]
|
||||||
target_x = min(limit, max(margin, round((start_x + dx) / step) * step))
|
target_x = min(limit, max(left[entry["tag"]], round((start_x + dx) / step) * step))
|
||||||
target_y = min(limit, max(margin, round((start_y + dy) / step) * step))
|
target_y = min(limit, max(margin, round((start_y + dy) / step) * step))
|
||||||
origin = wiresheet.get_box_origin(entry["tag"])
|
origin = wiresheet.get_box_origin(entry["tag"])
|
||||||
if origin is None:
|
if origin is None:
|
||||||
@@ -2178,6 +2246,7 @@ def on_canvas_release(event):
|
|||||||
|
|
||||||
changes = []
|
changes = []
|
||||||
failed = []
|
failed = []
|
||||||
|
left = wiresheet.box_left_limits([entry["tag"] for entry in group], margin, step)
|
||||||
for entry in group:
|
for entry in group:
|
||||||
group_tag = entry["tag"]
|
group_tag = entry["tag"]
|
||||||
origin = wiresheet.get_box_origin(group_tag)
|
origin = wiresheet.get_box_origin(group_tag)
|
||||||
@@ -2185,7 +2254,7 @@ def on_canvas_release(event):
|
|||||||
continue
|
continue
|
||||||
current_x, current_y = origin
|
current_x, current_y = origin
|
||||||
|
|
||||||
snapped_x = min(limit, max(margin, round(current_x / step) * step))
|
snapped_x = min(limit, max(left[group_tag], round(current_x / step) * step))
|
||||||
snapped_y = min(limit, max(margin, round(current_y / step) * step))
|
snapped_y = min(limit, max(margin, round(current_y / step) * step))
|
||||||
|
|
||||||
shift_x = snapped_x - current_x
|
shift_x = snapped_x - current_x
|
||||||
@@ -2255,9 +2324,11 @@ def nudge_selected_boxes(dx, dy):
|
|||||||
origins = {t: wiresheet.get_box_origin(t) for t in tags}
|
origins = {t: wiresheet.get_box_origin(t) for t in tags}
|
||||||
if any(o is None for o in origins.values()):
|
if any(o is None for o in origins.values()):
|
||||||
return "break"
|
return "break"
|
||||||
# The group stops as one at the margin strip and the sheet's far edge.
|
# The group stops as one at the margin strip and the sheet's far edge; a box with an
|
||||||
|
# off-sheet input stops a step further in, keeping its knob on the sheet.
|
||||||
|
left = wiresheet.box_left_limits(tags, margin, step)
|
||||||
shift_x = min(limit - max(o[0] for o in origins.values()),
|
shift_x = min(limit - max(o[0] for o in origins.values()),
|
||||||
max(margin - min(o[0] for o in origins.values()), dx * step))
|
max(max(left[t] - o[0] for t, o in origins.items()), dx * step))
|
||||||
shift_y = min(limit - max(o[1] for o in origins.values()),
|
shift_y = min(limit - max(o[1] for o in origins.values()),
|
||||||
max(margin - min(o[1] for o in origins.values()), dy * step))
|
max(margin - min(o[1] for o in origins.values()), dy * step))
|
||||||
if not shift_x and not shift_y:
|
if not shift_x and not shift_y:
|
||||||
@@ -3151,6 +3222,15 @@ def do_device_disconnect(device_down=False):
|
|||||||
wiresheet.render_components([])
|
wiresheet.render_components([])
|
||||||
|
|
||||||
|
|
||||||
|
def _on_connection_lost(ds, cause):
|
||||||
|
"""Main thread: the device stopped answering (closed simulator, unplugged cable) or
|
||||||
|
ended the session. Goes offline as Disconnect does, without asking anything of it."""
|
||||||
|
if app_state.device_session is not ds:
|
||||||
|
return # already disconnected, or a newer connection has taken its place
|
||||||
|
do_device_disconnect(device_down=True)
|
||||||
|
app_state.set_status(f"Connection lost: {cause}", warn=True)
|
||||||
|
|
||||||
|
|
||||||
def quit_app():
|
def quit_app():
|
||||||
"""File > Exit and the window's close box: let go of the device before going.
|
"""File > Exit and the window's close box: let go of the device before going.
|
||||||
|
|
||||||
@@ -3184,11 +3264,13 @@ def _start_cov_polling():
|
|||||||
ds = app_state.device_session
|
ds = app_state.device_session
|
||||||
if ds is None or not ds.is_connected:
|
if ds is None or not ds.is_connected:
|
||||||
_cov_poll_id = None
|
_cov_poll_id = None
|
||||||
if app_state.live_mode:
|
if ds is not None and app_state.live_mode:
|
||||||
do_device_disconnect()
|
_on_connection_lost(ds, ds.close_cause)
|
||||||
app_state.set_status("Connection lost", warn=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Scheduled first, so one bad event cannot stop the polling for good.
|
||||||
|
_cov_poll_id = app_state.window.after(100, poll)
|
||||||
|
|
||||||
events = ds.poll_events()
|
events = ds.poll_events()
|
||||||
if events and app_state.xml_root_element is not None:
|
if events and app_state.xml_root_element is not None:
|
||||||
needs_repaint = False
|
needs_repaint = False
|
||||||
@@ -3206,8 +3288,6 @@ def _start_cov_polling():
|
|||||||
|
|
||||||
_cov_poll_id = app_state.window.after(100, poll)
|
_cov_poll_id = app_state.window.after(100, poll)
|
||||||
|
|
||||||
_cov_poll_id = app_state.window.after(100, poll)
|
|
||||||
|
|
||||||
|
|
||||||
def _live_tree_expand(comp_id, tree_item_id, select=True, on_loaded=None):
|
def _live_tree_expand(comp_id, tree_item_id, select=True, on_loaded=None):
|
||||||
"""Called when user expands a tree node in live mode to lazy-load children.
|
"""Called when user expands a tree node in live mode to lazy-load children.
|
||||||
@@ -3266,8 +3346,11 @@ def _live_tree_expand(comp_id, tree_item_id, select=True, on_loaded=None):
|
|||||||
|
|
||||||
app_state.window.after(0, _ui)
|
app_state.window.after(0, _ui)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
if not ds.is_connected:
|
||||||
|
# The COV poll sees the closed session, goes offline and says so.
|
||||||
|
return
|
||||||
app_state.window.after(
|
app_state.window.after(
|
||||||
0, lambda: app_state.set_status(f"Load error: {exc}", warn=True)
|
0, lambda msg=f"Load error: {exc}": app_state.set_status(msg, warn=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
threading.Thread(target=_bg, daemon=True).start()
|
threading.Thread(target=_bg, daemon=True).start()
|
||||||
|
|||||||
@@ -421,6 +421,11 @@ class SoxClient:
|
|||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
return self._session is None or self._session.is_closed
|
return self._session is None or self._session.is_closed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def close_cause(self):
|
||||||
|
"""Why the session ended, e.g. "receive timeout"."""
|
||||||
|
return self._session.close_cause if self._session else "closed"
|
||||||
|
|
||||||
# ── Read Schema ─────────────────────────────────────────────
|
# ── Read Schema ─────────────────────────────────────────────
|
||||||
|
|
||||||
def read_schema(self):
|
def read_schema(self):
|
||||||
|
|||||||
@@ -154,6 +154,36 @@ def suggest_box_width_for_grid(width, grid_size):
|
|||||||
return proposed
|
return proposed
|
||||||
|
|
||||||
|
|
||||||
|
def offsheet_input_comps(components):
|
||||||
|
"""The components with a link arriving from off this sheet, drawn as a knob on
|
||||||
|
their left edge."""
|
||||||
|
root = app_state.xml_root_element
|
||||||
|
links_elem = root.find("links") if root is not None else None
|
||||||
|
if links_elem is None:
|
||||||
|
return set()
|
||||||
|
comp_by_path = {get_component_path(comp, root): comp for comp in components}
|
||||||
|
found = set()
|
||||||
|
for link in links_elem.findall("link"):
|
||||||
|
from_ref, to_ref = link.get("from", ""), link.get("to", "")
|
||||||
|
if "." not in from_ref or "." not in to_ref:
|
||||||
|
continue
|
||||||
|
dst = comp_by_path.get(to_ref.rsplit(".", 1)[0])
|
||||||
|
if dst is not None and from_ref.rsplit(".", 1)[0] not in comp_by_path:
|
||||||
|
found.add(dst)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def box_left_limits(tags, margin, step):
|
||||||
|
"""Left-most x each box on the open sheet may be moved to, keyed by canvas tag.
|
||||||
|
|
||||||
|
The margin, or one step further in for a box with an off-sheet input, so its knob
|
||||||
|
stays on the sheet rather than out in the grey margin.
|
||||||
|
"""
|
||||||
|
knobbed = offsheet_input_comps(list(app_state.canvas_comp_map.values()))
|
||||||
|
return {tag: margin + step if app_state.canvas_comp_map.get(tag) in knobbed
|
||||||
|
else margin for tag in tags}
|
||||||
|
|
||||||
|
|
||||||
def layout_margin(grid_size=None):
|
def layout_margin(grid_size=None):
|
||||||
"""The top/left edge offset a box may not encroach on, snapped onto the grid."""
|
"""The top/left edge offset a box may not encroach on, snapped onto the grid."""
|
||||||
if grid_size is None:
|
if grid_size is None:
|
||||||
@@ -1575,6 +1605,10 @@ def plan_columns(columns, column_keys, demand, scale, wires=()):
|
|||||||
# Column x positions depend only on widths and wire lanes, so they come first.
|
# Column x positions depend only on widths and wire lanes, so they come first.
|
||||||
column_x = []
|
column_x = []
|
||||||
cursor_x = margin # the left-most column clears the canvas edge by the margin
|
cursor_x = margin # the left-most column clears the canvas edge by the margin
|
||||||
|
if count and any(item.get("knob_in") for item in columns[column_keys[0]]):
|
||||||
|
# One step further in, so a knob on a first-column box's left edge stays on
|
||||||
|
# the sheet instead of out in the margin.
|
||||||
|
cursor_x = snap_up(margin + app_state.GRID_SIZE, scale)
|
||||||
for index, grid_x in enumerate(column_keys):
|
for index, grid_x in enumerate(column_keys):
|
||||||
column_x.append(cursor_x)
|
column_x.append(cursor_x)
|
||||||
gap = column_gap_for(demand.get(index, 0))
|
gap = column_gap_for(demand.get(index, 0))
|
||||||
@@ -1868,6 +1902,7 @@ def build_layout_items(components):
|
|||||||
linked_slot_index = build_linked_slot_index()
|
linked_slot_index = build_linked_slot_index()
|
||||||
pairs = sheet_link_pairs(components)
|
pairs = sheet_link_pairs(components)
|
||||||
on_sheet = {comp for src, _, dst, _ in pairs for comp in (src, dst)}
|
on_sheet = {comp for src, _, dst, _ in pairs for comp in (src, dst)}
|
||||||
|
knobbed = offsheet_input_comps(components)
|
||||||
|
|
||||||
linked = []
|
linked = []
|
||||||
loose = []
|
loose = []
|
||||||
@@ -1880,6 +1915,7 @@ def build_layout_items(components):
|
|||||||
"comp": comp, "meta": meta_val,
|
"comp": comp, "meta": meta_val,
|
||||||
"grid_x": grid_x, "grid_y": grid_y,
|
"grid_x": grid_x, "grid_y": grid_y,
|
||||||
"width": width, "height": height,
|
"width": width, "height": height,
|
||||||
|
"knob_in": comp in knobbed,
|
||||||
# Where each slot's wire meets the box, as painted by draw_component_box.
|
# Where each slot's wire meets the box, as painted by draw_component_box.
|
||||||
"slot_offsets": {
|
"slot_offsets": {
|
||||||
raw_name: HEADER_HEIGHT + row * SLOT_ROW_HEIGHT + SLOT_ROW_HEIGHT // 2
|
raw_name: HEADER_HEIGHT + row * SLOT_ROW_HEIGHT + SLOT_ROW_HEIGHT // 2
|
||||||
|
|||||||
Reference in New Issue
Block a user