Fix Manage Kits for apps read from a device

The schema built from a Sox session spelled checksums as signed decimals,
so no kit matched a manifest: Installed showed raw checksums and Action
read "Keep at checksum ...". They are now 8 hex digits like a .sax file,
and checksums compare regardless of zero-padding.

A device session only loads folders as they are opened, so only sys
counted as used. Used kits now come from the whole component tree, read
one request at a time so a small controller is not flooded into dropping
the session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:10:53 +03:00
co-authored by Claude Opus 5.5
parent 088fe69964
commit f22d4852c9
4 changed files with 60 additions and 6 deletions
+1 -1
View File
@@ -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
+38 -1
View File
@@ -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:
+9 -4
View File
@@ -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:
+12
View File
@@ -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: