diff --git a/app_state.py b/app_state.py index 22a179f..73a0140 100644 --- a/app_state.py +++ b/app_state.py @@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it. import configparser import os -VERSION = "0.0.0.137" +VERSION = "0.0.0.138" PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties") DEFAULT_GRID_SIZE = 10 DEFAULT_ADD_TIMESTAMP = True diff --git a/device_session.py b/device_session.py index adcd48b..6995481 100644 --- a/device_session.py +++ b/device_session.py @@ -176,6 +176,41 @@ class DeviceSession: for cid in parent.children if self.sox_client.cache_get(cid) is not None] + def used_kit_names(self): + """Every kit a component on the device belongs to, loading the whole tree to see. + + The DOM only holds what has been opened, so it cannot answer this on its own. Only + the tree is read — no config or runtime. + + Requests go one at a time. A small controller handles a single folder's burst fine, + but a back-to-back stream of them fills its receive buffer; it stops acking, and + DASP closes the whole session ("unacked send timeout"). Waiting for each reply + before sending the next keeps it at the pace the device sets. Anything already + cached costs nothing, so only the first call is slow. + """ + client = self.sox_client + if client is None: + return set() + + pending = [0] + loaded = 0 + while pending: + comp_id = pending.pop() + comp = client.cache_get(comp_id) + if comp is None: + comp = client.load_ids([comp_id], checked=False)[0] + loaded += 1 + if loaded % 20 == 0: + app_state.set_status(f"Reading components from the device… {loaded}") + if app_state.status_label is not None: + app_state.status_label.update_idletasks() + if comp is not None: + pending.extend(comp.children) + + parts = client.schema_parts or [] + return {parts[c.kit_id].name for c in client.cache + if c is not None and 0 <= c.kit_id < len(parts)} + def find_component_of_type(self, type_name, slot_name): """A component of `type_name` with `slot_name` read, loading where services live. @@ -294,8 +329,10 @@ class DeviceSession: schema_elem = ET.SubElement(sax_root, "schema") if self.sox_client and self.sox_client.schema_parts: for part in self.sox_client.schema_parts: + # Sox sends the checksum as a signed int; .sax files and manifest names + # spell it as 8 hex digits, and nothing matches unless this does too. ET.SubElement(schema_elem, "kit", - name=part.name, checksum=str(part.checksum)) + name=part.name, checksum=f"{part.checksum & 0xFFFFFFFF:08x}") app_elem = ET.SubElement(sax_root, "app") if root_comp is not None: diff --git a/manage_kits.py b/manage_kits.py index 6b0c642..e78a487 100644 --- a/manage_kits.py +++ b/manage_kits.py @@ -82,6 +82,11 @@ def column_label(version, checksum): return version if version else checksum +def same_checksum(a, b): + """Whether two checksums name the same build, whatever their zero-padding.""" + return bool(a) and bool(b) and a.lower().lstrip("0") == b.lower().lstrip("0") + + def action_options(kit_name, current_checksum, is_declared, builds): """The Action dropdown for one kit: (label, checksum to apply) pairs, no-op first. @@ -91,7 +96,7 @@ def action_options(kit_name, current_checksum, is_declared, builds): """ installed_version = None for version, checksum in builds: - if checksum == current_checksum: + if same_checksum(checksum, current_checksum): installed_version = version break @@ -109,7 +114,7 @@ def action_options(kit_name, current_checksum, is_declared, builds): installed_rank = palette.version_sort_key(installed_version) if installed_version else None for version, checksum in builds: - if is_declared and checksum == current_checksum: + if is_declared and same_checksum(checksum, current_checksum): continue label = build_label(version, checksum) @@ -176,7 +181,7 @@ def collect_rows(): installed_text = NO_CHECKSUM else: installed_text = column_label( - next((v for v, c in builds if c == checksum), ""), checksum + next((v for v, c in builds if same_checksum(c, checksum)), ""), checksum ) rows.append({ @@ -512,7 +517,7 @@ class KitsPanel: if "section" in row or not row["declared"] or not row["latest"]: continue latest_checksum = row["latest"][1] - if latest_checksum != row["checksum"]: + if not same_checksum(latest_checksum, row["checksum"]): selections.append((row["kit"], latest_checksum)) if selections: diff --git a/sax_file.py b/sax_file.py index d53cecd..86b22e8 100644 --- a/sax_file.py +++ b/sax_file.py @@ -501,6 +501,18 @@ def used_kit_names(): if app_state.xml_root_element is None: return names + # A device session's DOM only holds the folders opened so far. + session = app_state.device_session + if session is not None and session.is_connected: + try: + return session.used_kit_names() + except Exception as error: + messagebox.showwarning( + "Kits", + f"Could not read every component from the device:\n{error}\n\n" + "Kits shown as used come only from the folders opened so far.", + ) + for comp in app_state.xml_root_element.iter("comp"): type_str = comp.get("type") or "" if "::" in type_str: