Compare commits
2
Commits
fd35e5c9bb
...
73d168fd87
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73d168fd87 | ||
|
|
a9593de065 |
+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.133"
|
VERSION = "0.0.0.135"
|
||||||
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
|
||||||
|
|||||||
+27
-16
@@ -45,6 +45,7 @@ class DeviceSession:
|
|||||||
"""DASP connect → read schema → load root. Called from background thread."""
|
"""DASP connect → read schema → load root. Called from background thread."""
|
||||||
self.sox_client = SoxClient(host, int(port), user, password)
|
self.sox_client = SoxClient(host, int(port), user, password)
|
||||||
self.sox_client.on_event = self._on_sox_event
|
self.sox_client.on_event = self._on_sox_event
|
||||||
|
self.sox_client.checksum_resolver = schema.resolve_truncated_checksum
|
||||||
self.sox_client.connect()
|
self.sox_client.connect()
|
||||||
|
|
||||||
parts = self.sox_client.read_schema()
|
parts = self.sox_client.read_schema()
|
||||||
@@ -299,6 +300,10 @@ class DeviceSession:
|
|||||||
app_elem = ET.SubElement(sax_root, "app")
|
app_elem = ET.SubElement(sax_root, "app")
|
||||||
if root_comp is not None:
|
if root_comp is not None:
|
||||||
app_elem.set("id", str(root_comp.id))
|
app_elem.set("id", str(root_comp.id))
|
||||||
|
# The app is a component like any other, so the Properties view needs its
|
||||||
|
# type and slot values too. A .sax file leaves both implied; here they are not.
|
||||||
|
app_elem.set("type", root_comp.type_name)
|
||||||
|
self._add_prop_elements(app_elem, root_comp)
|
||||||
for child_id in root_comp.children:
|
for child_id in root_comp.children:
|
||||||
child_comp = self.sox_client.cache_get(child_id)
|
child_comp = self.sox_client.cache_get(child_id)
|
||||||
if child_comp is not None:
|
if child_comp is not None:
|
||||||
@@ -317,22 +322,7 @@ class DeviceSession:
|
|||||||
"type": type_qname,
|
"type": type_qname,
|
||||||
}
|
}
|
||||||
comp_elem = ET.SubElement(parent_elem, "comp", **attribs)
|
comp_elem = ET.SubElement(parent_elem, "comp", **attribs)
|
||||||
|
self._add_prop_elements(comp_elem, sox_comp)
|
||||||
slots = self.sox_client.slot_registry.get(
|
|
||||||
(sox_comp.kit_id, sox_comp.type_id))
|
|
||||||
if slots:
|
|
||||||
for slot in slots:
|
|
||||||
if slot.get("is_action"):
|
|
||||||
continue
|
|
||||||
name = slot["name"]
|
|
||||||
if name in sox_comp.values:
|
|
||||||
val = sox_comp.values[name]
|
|
||||||
if isinstance(val, float):
|
|
||||||
val_str = _fmt_float(val, _slot_precision(type_qname, name))
|
|
||||||
else:
|
|
||||||
val_str = str(val)
|
|
||||||
ET.SubElement(comp_elem, "prop",
|
|
||||||
name=name, val=val_str)
|
|
||||||
|
|
||||||
for child_id in sox_comp.children:
|
for child_id in sox_comp.children:
|
||||||
child_comp = self.sox_client.cache_get(child_id)
|
child_comp = self.sox_client.cache_get(child_id)
|
||||||
@@ -345,6 +335,24 @@ class DeviceSession:
|
|||||||
|
|
||||||
return comp_elem
|
return comp_elem
|
||||||
|
|
||||||
|
def _add_prop_elements(self, comp_elem, sox_comp):
|
||||||
|
"""Writes a <prop> for every cached non-action slot value of `sox_comp`."""
|
||||||
|
slots = self.sox_client.slot_registry.get(
|
||||||
|
(sox_comp.kit_id, sox_comp.type_id))
|
||||||
|
if not slots:
|
||||||
|
return
|
||||||
|
for slot in slots:
|
||||||
|
if slot.get("is_action"):
|
||||||
|
continue
|
||||||
|
name = slot["name"]
|
||||||
|
if name in sox_comp.values:
|
||||||
|
val = sox_comp.values[name]
|
||||||
|
if isinstance(val, float):
|
||||||
|
val_str = _fmt_float(val, _slot_precision(sox_comp.type_name, name))
|
||||||
|
else:
|
||||||
|
val_str = str(val)
|
||||||
|
ET.SubElement(comp_elem, "prop", name=name, val=val_str)
|
||||||
|
|
||||||
def build_links_elements(self, sax_root):
|
def build_links_elements(self, sax_root):
|
||||||
"""Adds <link> elements under a <links> container, matching .sax structure."""
|
"""Adds <link> elements under a <links> container, matching .sax structure."""
|
||||||
links_elem = sax_root.find("links")
|
links_elem = sax_root.find("links")
|
||||||
@@ -448,6 +456,9 @@ class DeviceSession:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_element_by_id(root, comp_id):
|
def _find_element_by_id(root, comp_id):
|
||||||
target = str(comp_id)
|
target = str(comp_id)
|
||||||
|
app_elem = root.find("app")
|
||||||
|
if app_elem is not None and app_elem.get("id") == target:
|
||||||
|
return app_elem
|
||||||
for elem in root.iter("comp"):
|
for elem in root.iter("comp"):
|
||||||
if elem.get("id") == target:
|
if elem.get("id") == target:
|
||||||
return elem
|
return elem
|
||||||
|
|||||||
@@ -293,7 +293,8 @@ def show_properties(comp):
|
|||||||
)
|
)
|
||||||
properties_panel.frame.pack(fill=tk.BOTH, expand=True)
|
properties_panel.frame.pack(fill=tk.BOTH, expand=True)
|
||||||
record_visit(VIEW_PROPERTIES, comp)
|
record_visit(VIEW_PROPERTIES, comp)
|
||||||
app_state.set_status(f"Properties: {comp.get('name', '?')}")
|
default_name = "App" if comp.tag == "app" else "?"
|
||||||
|
app_state.set_status(f"Properties: {comp.get('name', default_name)}")
|
||||||
|
|
||||||
|
|
||||||
def dismiss_properties(restore_canvas=True):
|
def dismiss_properties(restore_canvas=True):
|
||||||
@@ -1064,6 +1065,12 @@ def on_tree_right_click(event):
|
|||||||
|
|
||||||
if parent_id == "":
|
if parent_id == "":
|
||||||
tree_root_context_menu.delete(0, tk.END)
|
tree_root_context_menu.delete(0, tk.END)
|
||||||
|
# The root row names the <app> itself, which has slots of its own to show.
|
||||||
|
app_element = app_state.tree_element_map.get(item_id)
|
||||||
|
if app_element is not None:
|
||||||
|
tree_root_context_menu.add_command(
|
||||||
|
label="Properties", command=lambda: show_properties(app_element))
|
||||||
|
tree_root_context_menu.add_separator()
|
||||||
if app_state.live_mode and app_state.device_session and app_state.device_session.is_connected:
|
if app_state.live_mode and app_state.device_session and app_state.device_session.is_connected:
|
||||||
for action_name in _APP_ACTIONS:
|
for action_name in _APP_ACTIONS:
|
||||||
tree_root_context_menu.add_command(
|
tree_root_context_menu.add_command(
|
||||||
@@ -1100,10 +1107,14 @@ def on_tree_double_click(event):
|
|||||||
"""Double-click on a tree node opens its special view (or Properties)."""
|
"""Double-click on a tree node opens its special view (or Properties)."""
|
||||||
tree = app_state.tree
|
tree = app_state.tree
|
||||||
item_id = tree.identify_row(event.y)
|
item_id = tree.identify_row(event.y)
|
||||||
if not item_id or tree.parent(item_id) == "":
|
if not item_id:
|
||||||
return
|
return
|
||||||
comp = app_state.tree_element_map.get(item_id)
|
comp = app_state.tree_element_map.get(item_id)
|
||||||
if comp is not None:
|
if comp is None:
|
||||||
|
return
|
||||||
|
if tree.parent(item_id) == "":
|
||||||
|
show_properties(comp)
|
||||||
|
else:
|
||||||
_open_special_view(comp)
|
_open_special_view(comp)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -508,7 +508,9 @@ class PropertiesPanel:
|
|||||||
# the view would open already claiming unsaved edits.
|
# the view would open already claiming unsaved edits.
|
||||||
self._loading = False
|
self._loading = False
|
||||||
|
|
||||||
comp_type = comp.get("type", "sys::Component")
|
# A .sax file's <app> carries no type attribute: it is always a sys::App.
|
||||||
|
default_type = "sys::App" if comp.tag == "app" else "sys::Component"
|
||||||
|
comp_type = comp.get("type") or default_type
|
||||||
self.slots = schema.get_full_slots_for_type(comp_type)
|
self.slots = schema.get_full_slots_for_type(comp_type)
|
||||||
|
|
||||||
self.frame = tk.Frame(parent)
|
self.frame = tk.Frame(parent)
|
||||||
@@ -624,7 +626,8 @@ class PropertiesPanel:
|
|||||||
name_font = tkfont.Font(family="Segoe UI", size=9, weight="bold")
|
name_font = tkfont.Font(family="Segoe UI", size=9, weight="bold")
|
||||||
type_font = tkfont.Font(family="Segoe UI", size=9)
|
type_font = tkfont.Font(family="Segoe UI", size=9)
|
||||||
|
|
||||||
self._tinted(tk.Label(header, text=self.comp.get("name", "?"), font=name_font,
|
default_name = "App" if self.comp.tag == "app" else "?"
|
||||||
|
self._tinted(tk.Label(header, text=self.comp.get("name", default_name), font=name_font,
|
||||||
bg=HEADER_BG), HEADER_BG).pack(side=tk.LEFT)
|
bg=HEADER_BG), HEADER_BG).pack(side=tk.LEFT)
|
||||||
self._tinted(tk.Label(header, text=f"({comp_type})", font=type_font,
|
self._tinted(tk.Label(header, text=f"({comp_type})", font=type_font,
|
||||||
fg="#555555", bg=HEADER_BG),
|
fg="#555555", bg=HEADER_BG),
|
||||||
|
|||||||
+4
-3
@@ -64,12 +64,13 @@ def refresh_treeview_from_dom(xml_root):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if app_state.live_mode:
|
if app_state.live_mode:
|
||||||
file_name = f"{app_state.device_host}:{app_state.device_port}"
|
source = f"{app_state.device_host}:{app_state.device_port}"
|
||||||
else:
|
else:
|
||||||
file_name = os.path.basename(app_state.current_file_path)
|
source = os.path.basename(app_state.current_file_path)
|
||||||
|
# The row is the <app> component itself; where it came from goes in brackets.
|
||||||
# Inserted plain. refresh_dirty_display puts the unsaved tag on or off, and runs
|
# Inserted plain. refresh_dirty_display puts the unsaved tag on or off, and runs
|
||||||
# after every rebuild, so a fresh row picks up the right state there.
|
# after every rebuild, so a fresh row picks up the right state there.
|
||||||
root_node = tree.insert("", "end", text=file_name, open=True)
|
root_node = tree.insert("", "end", text=f"app ({source})", open=True)
|
||||||
|
|
||||||
app_element = xml_root.find("app")
|
app_element = xml_root.find("app")
|
||||||
if app_element is not None:
|
if app_element is not None:
|
||||||
|
|||||||
@@ -224,9 +224,32 @@ def find_manifest_path(kit_name, checksum):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_truncated_checksum(kit_name, prefix):
|
||||||
|
"""The checksum of the one local manifest whose leading bytes match ``prefix``.
|
||||||
|
|
||||||
|
Used when a device's schema reply cut a kit's checksum short. Returns the
|
||||||
|
checksum as a signed 32-bit int (as read off the wire), or None when no
|
||||||
|
manifest, or more than one, matches.
|
||||||
|
"""
|
||||||
|
manifest_dir = os.path.join(app_state.SEDONA_HOME, "manifests", kit_name)
|
||||||
|
if not os.path.isdir(manifest_dir):
|
||||||
|
return None
|
||||||
|
want = prefix.hex()
|
||||||
|
matches = set()
|
||||||
|
for f in os.listdir(manifest_dir):
|
||||||
|
if f.startswith(f"{kit_name}-") and f.endswith(".xml"):
|
||||||
|
hex_part = f[len(kit_name) + 1:-4].zfill(8).lower()
|
||||||
|
if hex_part.startswith(want):
|
||||||
|
matches.add(hex_part)
|
||||||
|
if len(matches) != 1:
|
||||||
|
return None
|
||||||
|
value = int(matches.pop(), 16)
|
||||||
|
return value - (1 << 32) if value & 0x80000000 else value
|
||||||
|
|
||||||
|
|
||||||
def find_manifest(kit_name, checksum):
|
def find_manifest(kit_name, checksum):
|
||||||
"""Returns the parsed ET root of a kit manifest, or None."""
|
"""Returns the parsed ET root of a kit manifest, or None."""
|
||||||
checksum_hex = f"{checksum:08x}" if isinstance(checksum, int) else checksum
|
checksum_hex = f"{checksum & 0xFFFFFFFF:08x}" if isinstance(checksum, int) else checksum
|
||||||
path = find_manifest_path(kit_name, checksum_hex)
|
path = find_manifest_path(kit_name, checksum_hex)
|
||||||
if path is None:
|
if path is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ class SoxMsg:
|
|||||||
self._pos += 1 # null terminator
|
self._pos += 1 # null terminator
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
def try_str(self):
|
||||||
|
"""Like str(), but returns None instead of overrunning a truncated buffer."""
|
||||||
|
end = self._buf.find(0, self._pos, self._size)
|
||||||
|
if end < 0:
|
||||||
|
return None
|
||||||
|
return self.str()
|
||||||
|
|
||||||
def read_bytes(self, n):
|
def read_bytes(self, n):
|
||||||
data = bytes(self._buf[self._pos:self._pos + n])
|
data = bytes(self._buf[self._pos:self._pos + n])
|
||||||
self._pos += n
|
self._pos += n
|
||||||
@@ -363,6 +370,10 @@ class SoxClient:
|
|||||||
# each slot dict: {name, id, type_name, is_config, is_runtime, is_action, is_operator, is_as_str}
|
# each slot dict: {name, id, type_name, is_config, is_runtime, is_action, is_operator, is_as_str}
|
||||||
self.slot_registry = {}
|
self.slot_registry = {}
|
||||||
|
|
||||||
|
# fn(kit_name, prefix_bytes) -> signed i4 checksum or None; recovers a
|
||||||
|
# checksum the device's reply truncated. Set externally by device_session.
|
||||||
|
self.checksum_resolver = None
|
||||||
|
|
||||||
# type name map — set externally by device_session
|
# type name map — set externally by device_session
|
||||||
# maps (kit_id, type_id) -> "kit::TypeName" for proper type resolution
|
# maps (kit_id, type_id) -> "kit::TypeName" for proper type resolution
|
||||||
self.type_name_map = {}
|
self.type_name_map = {}
|
||||||
@@ -413,10 +424,28 @@ class SoxClient:
|
|||||||
res.check_response('V')
|
res.check_response('V')
|
||||||
num = res.u1()
|
num = res.u1()
|
||||||
parts = []
|
parts = []
|
||||||
|
# The device builds this reply in a fixed 512-byte packet and silently
|
||||||
|
# drops whatever does not fit, so a device with many kits can cut the
|
||||||
|
# list short — sometimes mid-checksum of the last kit.
|
||||||
for _ in range(num):
|
for _ in range(num):
|
||||||
name = res.str()
|
name = res.try_str()
|
||||||
checksum = res.i4()
|
if name is None:
|
||||||
|
break
|
||||||
|
if res.remaining >= 4:
|
||||||
|
checksum = res.i4()
|
||||||
|
else:
|
||||||
|
prefix = res.read_bytes(res.remaining)
|
||||||
|
checksum = (self.checksum_resolver(name, prefix)
|
||||||
|
if self.checksum_resolver else None)
|
||||||
|
if checksum is None:
|
||||||
|
break
|
||||||
parts.append(KitPart(name, checksum))
|
parts.append(KitPart(name, checksum))
|
||||||
|
if len(parts) < num:
|
||||||
|
raise SoxException(
|
||||||
|
f"The device lists {num} kits but its reply was cut off after "
|
||||||
|
f"{len(parts)} — the list does not fit in the device's 512-byte "
|
||||||
|
f"Sox packet. Remove unused kits from the device's scode, or "
|
||||||
|
f"shorten kit names.")
|
||||||
self.schema_parts = parts
|
self.schema_parts = parts
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
@@ -427,17 +456,21 @@ class SoxClient:
|
|||||||
req = _prepare_request(ord('y'))
|
req = _prepare_request(ord('y'))
|
||||||
res = self.request(req)
|
res = self.request(req)
|
||||||
res.check_response('Y')
|
res.check_response('Y')
|
||||||
platform_id = res.str()
|
# Same 512-byte limit as read_schema: whatever is cut off is left
|
||||||
scode_flags = res.u1()
|
# blank rather than failing the connect.
|
||||||
|
platform_id = res.try_str() or ""
|
||||||
|
scode_flags = res.u1() if res.remaining > 0 else 0
|
||||||
kit_versions = []
|
kit_versions = []
|
||||||
for p in parts:
|
for p in parts:
|
||||||
ver = res.str()
|
ver = res.try_str()
|
||||||
kit_versions.append((p.name, p.checksum, ver))
|
kit_versions.append((p.name, p.checksum, ver or ""))
|
||||||
vi = VersionInfo(platform_id, scode_flags, kit_versions)
|
vi = VersionInfo(platform_id, scode_flags, kit_versions)
|
||||||
prop_num = res.u1()
|
prop_num = res.u1() if res.remaining > 0 else 0
|
||||||
for _ in range(prop_num):
|
for _ in range(prop_num):
|
||||||
k = res.str()
|
k = res.try_str()
|
||||||
v = res.str()
|
v = res.try_str()
|
||||||
|
if k is None or v is None:
|
||||||
|
break
|
||||||
vi.props[k] = v
|
vi.props[k] = v
|
||||||
self.version_info = vi
|
self.version_info = vi
|
||||||
return vi
|
return vi
|
||||||
|
|||||||
Reference in New Issue
Block a user