From 5acca522bf2deef2ef99057b47657120478ad66d Mon Sep 17 00:00:00 2001 From: Arda Aydin Date: Mon, 5 Oct 2026 14:52:18 +0300 Subject: [PATCH] DASP: drop the P-ION resend overrides, use standard 512-byte packets The save and file-open requests used their own fixed resend intervals (5 s / 15 s) and the hello asked for 256-byte packets. Ontrol's Sedona Java stack does neither and works with the P-ION: the standard backoff (1, 2, 4, 8 s) sends a busy device a few copies with the same seqNum, which its receive window drops. Both are removed. Also fixes a race in SendWindow.send(): the packet entered the window before its payload and sent_time were set, so housekeeping could resend it with an empty payload, and the device would then drop the real one as a duplicate. Tested on a P-ION at 192.168.1.252, App > Get with File: save 3.2 s (2 resends), app.sab + kits.scode in 11.3 s with 500-byte chunks (was 14.6 s at 256), device stayed up, files identical to a 256-byte backup. Co-Authored-By: Claude Opus 5.5 --- app_backup.py | 7 ++----- app_state.py | 2 +- dasp.py | 29 +++++++++++------------------ device_session.py | 4 +--- sox.py | 38 +++++++++++--------------------------- 5 files changed, 26 insertions(+), 54 deletions(-) diff --git a/app_backup.py b/app_backup.py index 4de06cd..c6356d5 100644 --- a/app_backup.py +++ b/app_backup.py @@ -23,7 +23,6 @@ import zipfile import app_state import sab_app -import sox from ox_widget_properties import IMAGE_EXTENSIONS, ORION_KIT, RION_KIT APP_SAB = "app.sab" @@ -163,14 +162,12 @@ def _source_entry(media, source_path): def save_app(client): """Invokes App.save on the device and waits for it to finish. - Ontrol's Niagara driver does this before every app.sab get, resending the request - only every 5 s: a P-ION saving a large app takes seconds, and 1 s resends of the - request while it is busy are what knocked it over. + Ontrol's Niagara driver does this before every app.sab get. """ comp = client.cache_get(0) or client.load(0) slots = client.slot_registry.get((comp.kit_id, comp.type_id)) or [] slot = next((s for s in slots if s["name"] == "save" and s.get("is_action")), None) - client.invoke(0, slot["id"] if slot else 0, send_retry=sox.SAVE_SEND_RETRY) + client.invoke(0, slot["id"] if slot else 0) class BackupResult: diff --git a/app_state.py b/app_state.py index de2a7fa..3dd5fdd 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.144" +VERSION = "0.0.0.145" 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/dasp.py b/dasp.py index 7774698..4f62ec9 100644 --- a/dasp.py +++ b/dasp.py @@ -456,7 +456,7 @@ def _to_ack_more(seq_more, max_bytes): class _Packet: __slots__ = ("seq_num", "payload", "acked", "enqueued_time", - "sent_time", "send_attempts", "send_retry", "next") + "sent_time", "send_attempts", "next") def __init__(self): self.seq_num = 0 @@ -465,7 +465,6 @@ class _Packet: self.enqueued_time = 0.0 self.sent_time = 0.0 self.send_attempts = 1 - self.send_retry = None # seconds; None = the window's own interval self.next = None @@ -507,17 +506,17 @@ class SendWindow: self._tail = None self._cond.notify_all() - def send(self, payload, send_retry=None): + def send(self, payload): p = _Packet() - p.send_retry = send_retry p.enqueued_time = _ticks() + p.payload = payload with self._cond: p.seq_num = self._seq_num & 0xFFFF self._seq_num = (self._seq_num + 1) & 0xFFFF + # sent_time is stamped inside the lock: send_retries() must not see the + # packet in the window as never sent and resend it before its first send. self._enqueue(p) - p.payload = payload - p.sent_time = _ticks() self._session._send_raw(self._to_msg(p)) def _enqueue(self, p): @@ -531,6 +530,7 @@ class SendWindow: else: self._tail.next = p self._tail = p + p.sent_time = _ticks() self._size += 1 self._cond.notify_all() @@ -620,15 +620,9 @@ class SendWindow: p = self._head while p is not None: if not p.acked: - if p.send_retry is not None: - # A request the device is slow to take -- a P-ION saving its - # app, opening a file -- is resent at its own steady interval: - # the 1 s default resends it while the device is still busy. - interval = p.send_retry - else: - backoff_shift = min(p.send_attempts - 1, 32) - interval = min(self.send_retry * (1 << backoff_shift), - self.max_retry_backoff) + backoff_shift = min(p.send_attempts - 1, 32) + interval = min(self.send_retry * (1 << backoff_shift), + self.max_retry_backoff) if now - p.sent_time >= interval: self._resend(p) oldest = max(oldest, now - p.enqueued_time) @@ -784,11 +778,10 @@ class DaspSession: # ── Send / Receive ────────────────────────────────────────── - def send(self, payload, send_retry=None): - """Queues a datagram. send_retry (seconds) overrides the resend interval.""" + def send(self, payload): if self.is_closed: raise DaspException(f"Session closed: {self.close_cause}") - self.send_window.send(payload, send_retry) + self.send_window.send(payload) def receive(self, timeout=1.0): if self.is_closed: diff --git a/device_session.py b/device_session.py index d6d00f9..6995481 100644 --- a/device_session.py +++ b/device_session.py @@ -10,7 +10,6 @@ import xml.etree.ElementTree as ET import app_state import schema -import sox from sox import SoxClient, SoxComponent, TREE, CONFIG, RUNTIME, LINKS, ALL @@ -44,8 +43,7 @@ class DeviceSession: def connect(self, host, port, user, password): """DASP connect → read schema → load root. Called from background thread.""" - self.sox_client = SoxClient(host, int(port), user, password, - ideal_max=sox.IDEAL_MAX, abs_max=sox.ABS_MAX) + self.sox_client = SoxClient(host, int(port), user, password) self.sox_client.on_event = self._on_sox_event self.sox_client.checksum_resolver = schema.resolve_truncated_checksum self.sox_client.connect() diff --git a/sox.py b/sox.py index e7970a4..67e9b88 100644 --- a/sox.py +++ b/sox.py @@ -35,18 +35,6 @@ BUF_ID = 7 STR_ID = 8 -# DASP resend intervals (seconds) for requests a device can be slow to take, as Ontrol's -# Niagara driver sets them: a P-ION saving a large app takes 2-3+ s, and a file open can -# take 10+ s on some devices. Resent every second instead, the device is sent copies -# while it is still busy with the first. -SAVE_SEND_RETRY = 5.0 -FILE_OPEN_SEND_RETRY = 15.0 - -# The DASP packet sizes Ontrol's Niagara driver asks for in its hello. -IDEAL_MAX = 256 -ABS_MAX = 256 - - class SoxException(Exception): pass @@ -678,13 +666,13 @@ class SoxClient: # ── Invoke Action ─────────────────────────────────────────── - def invoke(self, comp_id, slot_id, type_id=None, arg=None, send_retry=None): + def invoke(self, comp_id, slot_id, type_id=None, arg=None): req = _prepare_request(ord('i')) req.write_u2(comp_id) req.write_u1(slot_id) if arg is not None and type_id is not None: encode_value(req, type_id, arg) - res = self.request(req, send_retry) + res = self.request(req) res.check_response('I') # ── Add Component ─────────────────────────────────────────── @@ -950,7 +938,7 @@ class SoxClient: opened = closed = False try: try: - res = self.request(req, FILE_OPEN_SEND_RETRY) + res = self.request(req) except SoxException as e: # A P-ION on firmware 1.32 never answers a file get -- it drops off # the network for ~20 s, as a restart would -- even for a file it @@ -1002,21 +990,17 @@ class SoxClient: # ── Networking ────────────────────────────────────────────── - def request(self, req, send_retry=None): - results = self.requests([req], send_retry) + def request(self, req): + results = self.requests([req]) return results[0] - def requests(self, reqs, send_retry=None): - """Sends the batch and waits for every reply. - - send_retry (seconds) sets the DASP resend interval of these datagrams, for a - request the device is known to be slow to take. - """ + def requests(self, reqs): + """Sends the batch and waits for every reply.""" if self.is_closed: raise SoxException("SoxClient closed") if len(reqs) > 255: - return self._chunk_requests(reqs, send_retry) + return self._chunk_requests(reqs) with self._lock: for i, req in enumerate(reqs): @@ -1027,7 +1011,7 @@ class SoxClient: self._req_event.clear() for req in reqs: - self._session.send(req.to_bytes(), send_retry) + self._session.send(req.to_bytes()) deadline = time.monotonic() + 30.0 while True: @@ -1051,11 +1035,11 @@ class SoxClient: self._responses = None raise SoxException("Request timeout") - def _chunk_requests(self, reqs, send_retry=None): + def _chunk_requests(self, reqs): results = [] for i in range(0, len(reqs), 255): chunk = reqs[i:i + 255] - results.extend(self.requests(chunk, send_retry)) + results.extend(self.requests(chunk)) return results # called by receiver thread