fix(iphone): retry initial USB connect
CI build oscope-of / build-check (push) Has been cancelled

The iPhone USB source gave up permanently if the first connect_device()
failed (app not streaming on :7000 at startup -- phone locked, app not
launched/crashed, USB hiccup): start() returned False and the read thread
(which holds the reconnect loop) never ran. Auto-reconnect only covered a
mid-session drop, never a startup race.

Now start() launches the thread regardless and _run reconnects from the
top of its loop, so the initial connect retries with backoff exactly like
a mid-session drop -- the source connects whenever the iPhone app appears,
no data_only_viz restart needed.

Test: tests/test_iphone_usb_reconnect.py (start() keeps retrying when no
device). The 2 unrelated pre-existing collection errors are unchanged.
This commit is contained in:
L'électron rare
2026-06-30 09:34:23 +02:00
parent ce00b90f43
commit fc268405d2
2 changed files with 44 additions and 13 deletions
+19 -13
View File
@@ -91,12 +91,17 @@ class IphoneUSBSource:
self._opened = False
def start(self) -> bool:
# Try an immediate connect, but do NOT give up if the iPhone app isn't
# streaming yet: start the read thread regardless. _run retries the
# connect (with backoff) until the app appears, so a startup race (app
# not ready / phone locked at launch) recovers the same way a mid-session
# drop does -- no data_only_viz restart needed.
sock = connect_device()
if sock is None:
LOG.error("iphone usb: no device / connect failed")
return False
self._sock = sock
self._opened = True
LOG.warning("iphone usb: no device yet -- thread will keep retrying")
else:
self._sock = sock
self._opened = True
self._thread = threading.Thread(
target=self._run, args=(sock,), name="iphone_usb_src", daemon=True)
self._thread.start()
@@ -107,6 +112,13 @@ class IphoneUSBSource:
def _run(self, sock) -> None:
while not self._stop.is_set():
if sock is None: # no connection yet (startup or post-drop)
sock = self._reconnect()
if sock is None: # release() was called while waiting
break
self._sock = sock
self._opened = True
LOG.info("iphone usb connected")
try:
for tag, pid, payload in iter_frames(sock):
if self._stop.is_set():
@@ -161,15 +173,9 @@ class IphoneUSBSource:
with self._lock:
self._frame = None
self._opened = False
if self._stop.is_set():
break
LOG.info("iphone usb stream ended reconnecting...")
sock = self._reconnect()
if sock is None:
break
self._sock = sock
self._opened = True
LOG.info("iphone usb reconnected")
sock = None # forces the reconnect at the loop top
if not self._stop.is_set():
LOG.info("iphone usb stream ended -- reconnecting...")
self._opened = False
def _reconnect(self):
@@ -0,0 +1,25 @@
"""The iPhone USB source must keep retrying the initial connect, not give up
when the iPhone app isn't streaming yet. Auto-reconnect must cover a startup
race (app not ready when data_only_viz starts), not only a mid-session drop."""
import time
import data_only_viz.iphone_usb_source as m
def test_start_keeps_retrying_when_no_device(monkeypatch):
calls = {"n": 0}
def fake_connect():
calls["n"] += 1
return None # device never available during the test
monkeypatch.setattr(m, "connect_device", fake_connect)
monkeypatch.setattr(m, "iter_frames", lambda s: iter(()))
src = m.IphoneUSBSource()
try:
assert src.start() is True # does NOT give up on initial connect failure
time.sleep(0.25) # let the read thread attempt a few reconnects
assert calls["n"] >= 2, (
"start() should keep retrying; connect_device called %d time(s)" % calls["n"])
finally:
src.release()