App > Put with Files: restore a backup zip onto the connected device

Follows Ontrol's Workbench tool (BPutWithFilesTool / PutApp): the backup's
app.sax is encoded for the device's kits and put as app.sab.writing, renamed
to app.sab.stage; the platform service's deleteFile clears fonts/* and
images/*, then images, fonts (from Sedona home's bank, in the device's cut)
and the boot logo go. The zip is checked against the device first: same
platform (Orion, Rion or P-ION, from the platform id), and kits the device
can load. Falls back to the backup's app.sab, with its kits.scode if needed.

- sab_app.encode_sax: .sax -> .sab, ported from Py_sab2sax with Java's value
  parsing (fromLiteral, nullbool/nullfloat, Integer.decode); byte-identical
  to sedonac on the saved backups
- app_backup.native_image: port of OntOxImageUtil's png -> native .bin
- sox.put_file / rename_file
- Get with Files: Orion's boot logo is system/logo.jpg
- A restart that goes unanswered is taken as the device going down

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 23:37:45 +03:00
co-authored by Claude Opus 5.5
parent 1deb31f3b3
commit 232eea99f4
5 changed files with 932 additions and 41 deletions
+83 -3
View File
@@ -384,7 +384,7 @@ class SoxClient:
# file transfer: one at a time; its chunks arrive unasked, so the receiver
# hands them to whichever transfer is open
self._xfer_lock = threading.Lock()
self._xfer = None # _FileGet while a get is open
self._xfer = None # _FileGet or _FilePut while a transfer is open
# ── Connect / Disconnect ────────────────────────────────────
@@ -989,6 +989,77 @@ class SoxClient:
progress(file_size, file_size)
return bytes(data)
def put_file(self, uri, data, progress=None):
"""Writes `data` to the device as `uri`.
Ported from FileTransfer.putFile, as Ontrol's BSoxFilePut does it: an 'f'
request opens the transfer, the chunks go out as 'k' messages -- the DASP send
window paces them -- and the device answers the last with an unsolicited 'z'
once the file is closed. progress(done_bytes, total_bytes) is called from this
thread.
"""
if self.is_closed:
raise SoxException("SoxClient closed")
with self._xfer_lock:
# 21, not FileTransfer's 18: room for Orion's message-length header too.
chunk_size = max(16, self._session.ideal_max - 21)
req = _prepare_request(ord('f'))
req.write_str("p")
req.write_str(uri)
req.write_i4(len(data))
req.write_u2(chunk_size)
req.write_u1(0) # end of headers
xfer = _FilePut()
self._xfer = xfer
opened = False
try:
res = self.request(req)
res.check_response('F')
opened = True
file_size = res.i4()
chunk_size = res.u2()
if file_size != len(data) or chunk_size <= 0:
raise SoxException(
f"The device opened {uri} for {file_size} bytes in chunks of "
f"{chunk_size}; {len(data)} bytes were offered")
num_chunks = max(1, -(-file_size // chunk_size))
for num in range(num_chunks):
if self.is_closed:
raise SoxException(f"Session closed while writing {uri}")
chunk = data[num * chunk_size:(num + 1) * chunk_size]
msg = _prepare_request(ord('k'), 0)
msg.write_u2(num)
msg.write_u2(len(chunk))
msg.write_bytes(chunk)
self._session.send(msg.to_bytes())
if progress:
progress(min(file_size, (num + 1) * chunk_size), file_size)
timeout = getattr(self._session, "receive_timeout", 30.0) or 30.0
deadline = time.monotonic() + timeout
while not xfer.closed.wait(0.5):
if self.is_closed:
raise SoxException(f"Session closed while writing {uri}")
if time.monotonic() > deadline:
raise SoxException(f"Timed out waiting for the device to close {uri}")
opened = False
finally:
self._xfer = None
# Left open, the transfer keeps the device answering "busy" to the next.
if opened and not self.is_closed:
try:
self.request(_prepare_request(ord('z')))
except Exception:
pass
def rename_file(self, from_uri, to_uri):
"""Renames a file on the device ('b'), as SoxClient.renameFile."""
req = _prepare_request(ord('b'))
req.write_str(from_uri)
req.write_str(to_uri)
self.request(req).check_response('B')
# ── Networking ──────────────────────────────────────────────
def request(self, req):
@@ -1111,6 +1182,13 @@ class _FileGet:
# SoxReceiver — daemon thread
# ────────────────────────────────────────────────────────────────
class _FilePut:
"""The put in progress; `closed` is set when the device's 'z' arrives."""
def __init__(self):
self.closed = threading.Event()
class _SoxReceiver(threading.Thread):
def __init__(self, client):
super().__init__(name="SoxReceiver", daemon=True)
@@ -1136,10 +1214,12 @@ class _SoxReceiver(threading.Thread):
client._receive_event(msg)
elif cmd == ord('k'):
xfer = client._xfer
if xfer is not None:
if isinstance(xfer, _FileGet):
xfer.receive_chunk(msg)
elif cmd == ord('z'):
pass # close of a put -- the editor only gets files
xfer = client._xfer # the device closing a put
if isinstance(xfer, _FilePut):
xfer.closed.set()
else:
client._receive_response(msg)
except DaspException: