Skip to content

Commit d89d5ce

Browse files
Staacksclaude
andcommitted
iOS hardware target for the experiments sweep; not-loadable judged by evidence
Two lab findings. The experiments suite shells out to t1_experiments, whose iOS class only knew simctl - so on the MacBook's real devices every launch failed. It now takes --ios-target simulator|device; device mode uses devicectl with the '--' separator, port 80 on the device and the caller's existing forward, and the lab passes it for every iOS device. And 'remote API not reachable' was a hard error on Android while iOS treated it as scoping - but a Pixel 9 Pro declines depth.phyphox for want of depth hardware exactly as the simulator declines its missing sensors. The verdict is now platform-independent AND evidence-based: provisional not-loadable during the sweep, converted back into a real failure afterwards only if NOTHING on that target reached the API, which is precisely when a broken switch or forward is the explanation. Verified on the Pixel 9 Pro: depth alone reports the switch question, depth plus doppler correctly scopes depth and exits green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 81337d7 commit d89d5ce

3 files changed

Lines changed: 63 additions & 24 deletions

File tree

tools/lab/lab.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ hosts:
1111
android: /home/dicon/phyphox/git/phyphox-android/app/regular/release/phyphox-release.apk
1212
ios: /Users/sebastianstaacks/phyphox.ipa
1313
devices:
14-
iphone-14-pro: {platform: ios, serial: "00008120-001C31520EC0C01E", port: 80}
15-
iphone-8: {platform: ios, serial: "abff2ed0b2ee61af08467e4381063a5e45da4ad0", port: 80}
16-
ipad-pro: {platform: ios, serial: "00008103-0015196202A3001E", port: 80}
17-
pixel-9-pro: {platform: android, serial: "53291FDAP0026B", port: 8080}
18-
pixel-3: {platform: android, serial: "8C5X1J8JL", port: 8080}
19-
nexus-5x: {platform: android, serial: "025e9f6567c4d0e5", port: 8080}
20-
galaxy-a3: {platform: android, serial: "33007f6cd2ab1445", port: 8080}
14+
iphone-14-pro: {platform: ios, serial: "00008120-001C31520EC0C01E", port: 8181}
15+
iphone-8: {platform: ios, serial: "abff2ed0b2ee61af08467e4381063a5e45da4ad0", port: 8182}
16+
ipad-pro: {platform: ios, serial: "00008103-0015196202A3001E", port: 8183}
17+
pixel-9-pro: {platform: android, serial: "53291FDAP0026B", port: 8184}
18+
pixel-3: {platform: android, serial: "8C5X1J8JL", port: 8185}
19+
nexus-5x: {platform: android, serial: "025e9f6567c4d0e5", port: 8186}
20+
galaxy-a3: {platform: android, serial: "33007f6cd2ab1445", port: 8187}
2121

2222
linuxbox: # the split: Android (+ boards, phase 6) here,
2323
devices: # iOS stays with the MacBook
24-
pixel-9-pro: {platform: android, serial: "53291FDAP0026B", port: 8080}
25-
pixel-3: {platform: android, serial: "8C5X1J8JL", port: 8080}
26-
nexus-5x: {platform: android, serial: "025e9f6567c4d0e5", port: 8080}
27-
galaxy-a3: {platform: android, serial: "33007f6cd2ab1445", port: 8080}
24+
pixel-9-pro: {platform: android, serial: "53291FDAP0026B", port: 8184}
25+
pixel-3: {platform: android, serial: "8C5X1J8JL", port: 8185}
26+
nexus-5x: {platform: android, serial: "025e9f6567c4d0e5", port: 8186}
27+
galaxy-a3: {platform: android, serial: "33007f6cd2ab1445", port: 8187}

tools/lab/suites.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ def run_experiments_suite(dev, args):
227227
"--serial", getattr(dev, "serial", None) or dev.udid,
228228
"--port", str(dev.port), "--seconds", str(args.seconds),
229229
"--require-rows", "--out", out]
230+
if dev.platform == "ios":
231+
# lab devices are real hardware: devicectl, port 80 on the
232+
# device, and the forward this handle already established
233+
cmd += ["--ios-target", "device"]
230234
r = subprocess.run(cmd, capture_output=True, text=True, timeout=7200)
231235
findings = []
232236
if r.returncode != 0:

tools/t1_experiments.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,15 @@ def inject(self, t):
195195

196196

197197
class IOS:
198-
def __init__(self, udid, port):
198+
"""Simulator by default (what CI drives); --ios-target device switches
199+
to devicectl for real hardware, where the app serves port 80 and the
200+
host-side forward is set up by the caller (the lab driver's
201+
IOSDevice.prepare) rather than here."""
202+
203+
def __init__(self, udid, port, target="simulator"):
199204
self.udid = udid or "booted"
200205
self.port = port
206+
self.target = target
201207

202208
def launch(self, asset_path):
203209
url = "phyphox://asset=" + urllib.parse.quote(asset_path, safe="")
@@ -207,6 +213,15 @@ def launch(self, asset_path):
207213
# report a false ok. -phyphoxRemotePort keeps the served port and
208214
# the driver's base URL in step; -phyphoxAutoConfirm accepts the
209215
# dialogs a headless run cannot tap (network privacy).
216+
if self.target == "device":
217+
# hardware: devicectl needs "--" before the app's own
218+
# dash-prefixed arguments, and the app serves port 80 there
219+
r = sh(["xcrun", "devicectl", "device", "process", "launch",
220+
"--terminate-existing", "--device", self.udid, "--",
221+
IOS_BUNDLE, "-phyphoxUrl", url, "-phyphoxRemote",
222+
"-phyphoxRemotePort", "80", "-phyphoxAutoConfirm"],
223+
timeout=60)
224+
return r.returncode == 0
210225
r = sh(["xcrun", "simctl", "launch", "--terminate-running-process",
211226
self.udid, IOS_BUNDLE,
212227
"-phyphoxUrl", url, "-phyphoxRemote",
@@ -218,6 +233,8 @@ def cleanup(self):
218233
pass
219234

220235
def stop_app(self):
236+
if self.target == "device":
237+
return # --terminate-existing on launch
221238
sh(["xcrun", "simctl", "terminate", self.udid, IOS_BUNDLE])
222239

223240
def inject(self, t):
@@ -285,16 +302,16 @@ def run_experiment(dev, base, rel, path, args):
285302
if elapsed is not None:
286303
elapsed += args.api_wait
287304
if elapsed is None:
288-
if args.platform == "ios":
289-
# the simulator lacks most sensors; the app declines the
290-
# experiment ("sensor not available") and returns to the
291-
# collection. That is the scoped iOS subset, not a failure -
292-
# but a hung loadable experiment ends up here too, which is
293-
# why the slow path above gets its own verdict.
294-
result["not_loadable"] = True
295-
return result
296-
result["errors"].append("remote API not reachable (is remote access "
297-
"enabled for launched experiments?)")
305+
# The app declines an experiment the target cannot run ("sensor
306+
# not available") and returns to the collection, so no API comes
307+
# up. That is scoping, not failure - on EITHER platform: the iOS
308+
# simulator lacks most sensors, and a phone without depth
309+
# hardware declines depth.phyphox just the same (Pixel 9 Pro,
310+
# found by the lab 2026-08-26). It is only indistinguishable
311+
# from a broken remote switch in the abstract - run_all() has
312+
# the evidence and reclassifies afterwards, so this verdict is
313+
# provisional.
314+
result["not_loadable"] = True
298315
return result
299316
if elapsed > args.api_wait:
300317
result["slow_api"] = round(elapsed, 1)
@@ -373,6 +390,11 @@ def main():
373390
ap.add_argument("--require-rows", action="store_true",
374391
help="an export set without rows is a finding")
375392
ap.add_argument("--include-bluetooth", action="store_true")
393+
ap.add_argument("--ios-target", choices=["simulator", "device"],
394+
default="simulator",
395+
help="iOS only: simulator (simctl, the CI default) or "
396+
"device (devicectl; the caller provides the port "
397+
"forward)")
376398
ap.add_argument("--out", default="t1-results.json")
377399
args = ap.parse_args()
378400

@@ -394,14 +416,26 @@ def main():
394416
if args.platform == "android":
395417
dev = Android(args.serial, args.port)
396418
else:
397-
dev = IOS(args.serial, args.port)
419+
dev = IOS(args.serial, args.port, args.ios_target)
398420
base = f"http://127.0.0.1:{args.port}"
399421

400422
results, hard_failures = [], 0
401423
try:
402424
run_all(dev, base, collection, experiments, args, results)
403425
finally:
404426
dev.cleanup()
427+
428+
# Provisional not-loadable verdicts, judged with the whole run as
429+
# evidence: if NOTHING reached the remote API, the switch or the
430+
# forward is broken and every one of them is a real failure; if some
431+
# experiments did, the app simply declined the others.
432+
if not any(r.get("remote") for r in results):
433+
for r in results:
434+
if r.pop("not_loadable", None):
435+
r["errors"].append(
436+
"remote API not reachable, and no experiment on this "
437+
"target reached it - remote-enable switch or port "
438+
"forward broken?")
405439
hard_failures = sum(
406440
1 for r in results
407441
if not r.get("skipped") and not r.get("not_loadable")
@@ -424,7 +458,8 @@ def run_all(dev, base, collection, experiments, args, results):
424458
print(f" - skipped: {r['skipped']}")
425459
continue
426460
if r.get("not_loadable"):
427-
print(" - not loadable on this target (simulator subset)")
461+
print(" - not loadable on this target (the app declined it - "
462+
"hardware or simulator lacks what it needs)")
428463
continue
429464
if r.get("slow_api"):
430465
print(f" ~ remote API answered late ({r['slow_api']} s) - "

0 commit comments

Comments
 (0)