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 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 14:52:18 +03:00
co-authored by Claude Opus 5.5
parent 2880c2a6f2
commit 5acca522bf
5 changed files with 26 additions and 54 deletions
+11 -27
View File
@@ -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