Repository navigation
Conversation
Contributor
|
…discovery The Mac iOS bench (#189) stood up the reservation daemon, signing and a bare app install, but the app-driver JOURNEY lane could not actually run on a real iPhone: a native Capacitor WKWebView loads the bundled web/dist at capacitor://localhost/ with no query string, so the `?driver=…&ble=real` seam (web/src/ui/app/main.ts) — which starts the journeys — was unreachable. And the iOS client posted to a build server that doesn't exist (:8765 /device-*?url=) instead of the real tools/ios_build_server.py (:8099 /run?task=…). Drive the iPhone the SAME way as Android by pointing Capacitor's server.url at the station-served app URL (which carries the query) so the WKWebView loads it over the LAN and enters driver mode; native BLE keeps bridging through the Capacitor Improv plugin. - web/capacitor.config.ts: set server.url from CAP_SERVER_URL when present (unset = load the bundle, as before). - tools/ios_build_server.py: accept a `server_url` param on /run and pass it as CAP_SERVER_URL into the task env so `cap sync` bakes it (env only, never argv; validated to an http(s) URL). - pi/hitl/phone/phone_target.py: IosBuildClient now hits the real /run?task= endpoint on :8099; the iOS targets thread the app URL through server_url on a cap-sync→build→install→launch chain (not a devicectl launch url=, which can't open a URL on a real device). Default bundle id fixed to dev.splanc.app. - pi/hitl/phone/launcher.py: serve the app on 0.0.0.0 so an off-box phone can fetch it (+ SO_REUSEADDR); the returned base stays loopback for the browser lane. - web/ios-config/apply.sh: NSAllowsLocalNetworking so ATS permits cleartext to the LAN station + device (narrower than arbitrary loads; App-Store-acceptable). Dynamic C6 serial-port discovery (owner requirement — never hardcode the renumbering /dev/cu.usbmodem*): pi/hitl/harness/serial_discovery.py resolves the port from the stable USB identity (Espressif VID 0x303a), matching the reserved board by its HITL_ADAPTER_SERIAL when several C6s are attached. Reusable on the Mac bench and the Linux rigs; a py_binary CLI prints the path for `esptool --port "$(…)"`. Pure selection logic is dependency-injected and unit-tested without pyserial/hardware. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ container orchestrator)
Follow-up to the driver wiring: drive the Mac iOS bench the same way as the Linux
rig DUTs — reserve from the container, execute host-natively on the Mac via the
darwin runner — with zero hand-run Mac commands.
hitl-darwin.nix (flake-provided hooks, so nothing is started by hand):
- launchd `ios-build-server` service: runs tools/ios_build_server.py on loopback
:8099 as the operator account (Xcode + signing keychain) against a Mac checkout.
Added when the host sets iosBuildUser + iosBuildWorkspace; signing/ASC secrets are
sourced at launch from ${stateDir}/ios-build.env (outside the nix store), never
committed. The reserved session drives it over 127.0.0.1.
- reserved session's python is now python3 + websockets (pyEnv), which the shipped
phone_e2e driver_server needs; the rest of the iOS lane is stdlib (real BLE runs
on the phone via the Capacitor plugin, so no bleak here).
- header documents the reservation-driven flow + the genuinely one-time human setup.
ios_reservation.py (container/CI orchestrator, //pi/hitl/phone:ios_reservation):
reserves the `ios-phone` unit, ships the harness + app + solver + C6 firmware bundle
into the reserved session as one tar, resolves the C6 serial port dynamically
in-session (serial_discovery), and runs phone_e2e ON THE MAC — HITL_STATION_IP =
the Mac's own LAN IP, IOS_BUILD_SERVER = the loopback launchd service, app dirs via
env (no bazel/runfiles on the Mac). Device identifiers stay out of the repo — the
UDID/adapter serial ride in from the reservation env. Pure payload/command
construction is unit-tested offline (ios_reservation_test).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…session SSH) The reserved-session SSH grant was rejected on the live Mac because the `hitl` account never existed: nix-darwin declares users.users.hitl but does NOT create a fresh macOS account (dscl -> eDSRecordNotFound), so sshd could never authenticate a non-existent user (its password/keyboard-interactive offer was a generic anti-enumeration response, not a real fallback). Everything else was correct — the sshd drop-in (Match User hitl -> the managed AuthorizedKeysFile, PermitUserEnvironment yes), StrictModes, and /var/lib/hitl perms all check out. Add an idempotent postActivation script that creates the account with sysadminctl (a proper auth-authority-bearing standard user, so pubkey SSH works; a random unused password keeps it key-only), hidden from the login window, uid 555, staff group, a STABLE /bin/bash shell (not a GC-able nix-store bash), home + ~/.ssh (0700) for the signing-secret env seam. No-op if the record already exists. One `darwin-rebuild switch` now creates the user AND deploys the launchd ios-build-server + websockets. Also flag the bind-mount hazard: /workspace is a bind-mount of the operator's day-to-day checkout, whose branch flips as container agents `git checkout`, so iosBuildWorkspace MUST be a DEDICATED clone pinned to a stable ref — the build server would otherwise build a random agent's branch. (The app content the iPhone loads is served from the tar the reserved session ships, so this checkout's web/dist staleness is irrelevant; it supplies only the native iOS project + config + plugins + signing.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stness) The dedicated build checkout may live on an external volume (e.g. /Volumes/MacMiniExt/Projects/splanc-iosbuild) that mounts late at boot or detaches. A plain KeepAlive=true would crash-loop while the exec path is missing. Gate KeepAlive on PathState of the server script instead: launchd runs the job only while that path exists — starting it when the volume mounts and stopping it cleanly when the volume is absent. RunAtLoad stays for the mounted-at-boot case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… port-conflict Three hardening fixes surfaced during the Mac bring-up: 1. User materialization no longer hangs. sysadminctl -addUser blocks on a GUI secure-token/FDE authorization dialog when run non-interactively on a FileVault Mac (it stalled the first switch until the operator clicked an invisible prompt; a headless/CI activation would hang forever, and macOS has no `timeout` to bound it). Create the account with plain `dscl` instead — no FDE dance, non-blocking — and `dscl -passwd` (root, no old password) for a proper pubkey-usable auth record (random unused password; password login stays disabled). Guard on UniqueID; every step `|| true` so activation can't be aborted. 2. Hermetic build-server PATH. Reference the nix tools (node/pnpm/cocoapods/git/ bazelisk/jq) by absolute store path (lib.makeBinPath), prepended — so a launchd env without /run/current-system/sw/bin can't cause `command not found`. bazelisk (only needed here for the one-time web-build) lives in this service PATH, not systemPackages; Xcode's xcodebuild/devicectl stay via /usr/bin + DEVELOPER_DIR. 3. ios_build_server fails LOUDLY on a port conflict. If :8099 is already held (e.g. a stale hand-started server from another checkout squatting the port and answering with the wrong workspace/PATH), print a clear message naming the `lsof -nP -iTCP:8099 -sTCP:LISTEN` check and exit non-zero, instead of a bare traceback / crash-loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ar/log The launchd service never started: state "spawn scheduled", active count 0, and no log file at all. Root cause: /var/log is 0755 root:wheel, so a UserName=kevin LaunchDaemon cannot create its StandardOut/ErrorPath there — launchd fails to set up the job and never spawns it (the missing log file is the tell; a TCC/exec error would have produced a log). Verified the code itself is fine: running the exact command as kevin binds and serves :8099. Point the logs at the service user's ~/Library/Logs (writable by them). No other service path needs /var write: the bazel cache is ~/.cache and web/dist + web/ios are written inside the (kevin-owned) workspace. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ority
The hitl user existed on the live Mac (UID 555, home, shell) but had NO
AuthenticationAuthority — the earlier hung sysadminctl created a partial record and
never finished the auth setup. macOS rejects EVERY login for such an account, pubkey
included, so the reserved-session ssh kept failing ("pubkey offered → Permission
denied") even though the user "exists". The dscl create-guard skipped the existing
user, so it never repaired it.
Move the auth-record materialization OUT of the create guard: whenever
AuthenticationAuthority is missing (a partial sysadminctl account OR a fresh dscl one),
`dscl -passwd` a random unused password to create ShadowHashData + the auth authority.
Idempotent (only when missing), non-blocking, password login stays unused.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…assword Verified on the live Mac: `dscl -passwd` populated ShadowHashData (a password) but left the account with NO AuthenticationAuthority key, while a working account (kevin) has `;ShadowHash;HASHLIST:…`. Without that pointer macOS authenticates nothing for the account. Create `AuthenticationAuthority ";ShadowHash;"` before setting the password. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h blocker) Reserved-session ssh failed even with a valid per-reservation key, correct authorized_keys perms, and a complete auth record. Root cause: macOS Remote Login is gated by the `com.apple.access_ssh` SACL group (which nests admin), and the non-admin `hitl` service account was not a member — so sshd rejects it at login regardless of the key. Add it to that group in the activation (idempotent), which grants ssh access without making the account an admin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(macOS ssh fix) Definitive root cause from the live Mac sshd-session log: Could not open user 'hitl' authorized keys '/var/lib/hitl/authorized_keys': Permission denied macOS sshd reads AuthorizedKeysFile AS THE TARGET USER, but the darwin runner writes the managed key file root:0600 inside the 0700 root state dir — so the hitl user can't open its own key file, and every reserved-session ssh was rejected. (On Linux sshd reads it as root, so this was never hit.) The runner rewrites the file 0600 on every reserve, so loosening file perms won't hold. Fix: in the Match User block, read the file via `AuthorizedKeysCommand /bin/cat …` with `AuthorizedKeysCommandUser root` — root can read the 0600 file, no world-readable perms needed, and command output isn't subject to the per-uid AuthorizedKeysFile read or StrictModes. This also overrides nix-darwin's global AuthorizedKeysCommand (which points at a per-user file we don't populate). AuthorizedKeysFile is kept as a root-read fallback/doc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d session The reserved Mac session's PATH lists /usr/bin before /run/current-system/sw/bin, so bare `python3` is the system interpreter (no websockets) — phone_e2e's driver_server import would fail. Prepend the nix system profile bin so `python3` is the nix-darwin pyEnv (websockets present). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing seam)
The device build found "No Accounts"/no signing certificate because the launchd
build server (runs as the build user) couldn't source its signing env: the file was
in ${stateDir} (/var/lib/hitl, 0700 root), unreadable by that user, so HITL_SIGN_*
was empty and signing fell back to a nonexistent Apple ID account. Same root-only
path trap as the ssh key file. Point iosBuildEnvFile at the build user's own
~/.config/hitl/ios-build.env (where setup-ios-signing.sh already keeps the keychain
pass + ASC key). The signing keychain HITL_SIGN_KEYCHAIN must likewise live in a
build-user-readable path, not /var/lib/hitl.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reserved session's serial_discovery needs pyserial to enumerate the C6's port (connect/config flashing). Add it to the nix pyEnv, and have discover_c6_cmd invoke the pyEnv python explicitly (the reserved session's bare python3 is the system one without pyserial). Non-fatal for the device-free smoke, required for the device lanes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…adless signing The device build unlocked the signing keychain but never granted apple codesigning tools non-interactive access to the private key. That works in an interactive login session (a direct build signed fine) but NOT from the headless launchd build server, which then failed "No signing certificate … with a private key found" — the cert is visible but the key access is ACL/partition-list gated. Add the same `security set-key-partition-list -S apple-tool:,apple:` the TestFlight prep already does, right after the unlock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… login session)
Code-signing (xcodebuild automatic signing / -allowProvisioningUpdates) and devicectl
only work from the operator's login (Aqua) GUI session. Verified live: the exact
device-build command signs in an interactive session ("BUILD SUCCEEDED") but the
system LaunchDaemon (Background session) fails "No signing certificate … with a
private key" for the same command, keychain, and env — the Background session can't
resolve the signing identity. Move the build server from launchd.daemons (system,
Background) to launchd.user.agents (the logged-in user's gui domain). Drop UserName
(an agent already runs as that user). Deploy now also runs `<sys>/activate-user` as
the operator to load the agent. Requires the operator stays logged in — fine for a
dedicated always-on bench Mac.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…chAgent) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iles)
The smoke drove the app on the iPhone but then hit FileNotFoundError on a journey
file: a prior run's /tmp payload tree has READ-ONLY dirs (web/dist + solver come from
read-only bazel runfiles), so `rm -rf` couldn't clear them ("Can't unlink … Permission
denied") and the next tar extraction was incomplete. chmod -R u+w before removing, and
again after extraction so the next run can clean up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… are symlinks) The payload is resolved from bazel runfiles, where the journey JSONs are SYMLINKS into the container tree. tar preserved them as symlinks, so on the Mac they were dangling -> phone_e2e hit FileNotFoundError: journeys/config.json (after the app had already launched + connected). Open the tar with dereference=True so real content ships. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-Fi AP (connect) For the connect/config/mapping journeys: add --flash-c6 (extract the shipped netstack flashbundle and esptool the C6 at the dynamically-discovered port, host-native on the Mac) and --wifi-ssid/--wifi-pass (forwarded to phone_e2e so the phone provisions the C6 onto that AP over real BLE). Intended for the Mac's Internet Sharing AP, which both the C6 and the iPhone join so the phone can reach the C6's wss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…adless)
The connect journey failed on iOS: the driver's provisionBle called
requestImprovDevice() (Web Bluetooth), which doesn't exist in the WKWebView
("Web Bluetooth is unavailable in this browser"). Production code branches on
isNativePlatform() to use the Capacitor BLE plugin (addDevice.ts →
chooseImprovDevice); the driver didn't. Add a HEADLESS native pick (no UI chooser):
scan the Capacitor BLE plugin for Improv and take the strongest-RSSI device (the DUT
next to the phone on the bench), adapted to the shared provisionViaBle seam. Browser/
emulator lane keeps Web Bluetooth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…olation) Two security hardenings for the Mac iOS bench's provisioning AP: 1. AP creds off the repo/CLI. phone_e2e no longer defaults HITL_WIFI_SSID to a hard-coded 'FugLink' SSID (which could silently join a stray network); it reads the SSID/pass from the reservation-session env (host-managed ~/.ssh/environment, the same seam the signing secrets ride) or an explicit --wifi-*, and fails loudly on a real-BLE provisioning journey when neither is set. ios_reservation's --wifi-* are now optional + discouraged in favour of the env. Documented the HITL_WIFI_SSID/HITL_WIFI_PASS seam in hitl-darwin.nix. 2. Isolated-NAT pf rules. A launchd job loads a com.apple/* pf sub-anchor that lets the Internet Sharing subnet (bridge100/192.168.2.0/24 = DUT+phone) reach only itself, DHCP, and mDNS/broadcast, and drops bench->home-LAN (192.168.68.0/24) + bench->tailnet (100.64.0.0/10) plus all other off-subnet unicast, so a provisioned DUT or the phone can't pivot off the bench. Re-applied on load + every 60s since Internet Sharing flushes pf on toggle. To keep the phone->station path intra-subnet (and thus allowed), ios_reservation now advertises the station at the bridge IP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ard + connect to it)
The iOS connect journey couldn't reach the reserved C6. Concurrent serial capture
(new tooling below) showed the reserved board was NEVER connected over BLE by the
phone — two harness bugs, not the WiFi stack (the wired join is clean):
- Stray-board pick: the headless iOS Improv picker took the strongest-RSSI advertiser
with no pinning, so on a bench with other rigs' C6s in BLE range the phone
provisioned a stray board. iOS/CoreBluetooth exposes no BLE MAC (only an opaque
per-app UUID), so we pin by the advertised NAME instead: ios_reservation reads the
reserved C6's Improv name from its serial ("[ble] advertising <name> as <mac>") and
threads it through phone_e2e -> the connect journey -> pickImprovDeviceHeadless,
which now accepts only that name (fails loudly rather than provision a stray). Empty
name keeps strongest-RSSI (single-device lanes / real users with a visible picker).
- Connect not wired to the provision result: on the iOS bench the phone reaches the
C6 directly, so the wss URL is the address the device just reported over Improv, not
a rig-forwarded input. The harness now remembers the last provisioned wss and the
connect command falls back to it when the journey passes no explicit device_ws.
Also adds the diagnostic that found this: ios_serial_diag.py (macOS pyserial wired
PROV + monitor-only capture; DTR-gated USBCDC) and ios_reservation --serial-monitor,
which captures the C6 console during a phone BLE provision (BLE/WiFi coex diagnosis).
Tests: ios_reservation_test (improv-name forwarding) + web typecheck/unit all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…obust scan, log progress Three fixes toward lights-out iOS journeys on a shared, crowded bench: - Drop BLE as soon as the Improv redirect arrives (net/improv.ts). The player shares one 2.4GHz radio between BLE and its WiFi/TLS stack and is heap-tight; holding the BLE central open past provisioning starved the wss TLS handshake on :443, so the app hung forever in "connecting" after a successful provision. Disconnecting the moment we have the redirect frees the radio for the very next step (wss://<ip>/ws). This is a SHIPPING-app fix (shared provisionViaBle), not just HITL. - Poll the BLE scan until the pinned board appears (driver/harness.ts). With 8+ "Led Widget <hex>" boards advertising at once on the bench, our reserved board can take several scan cycles to surface; the old fixed 5s window spuriously reported "not found". Now we scan up to 25s and select as soon as the pinned name shows. - Surface the app's driver events (driver_server.py): print status / error / milestone events so a failed run shows exactly which device the phone picked, how far the provision got, and the redirect URL it then dials. Tests: web typecheck + 78 web unit tests green.
Harden the iOS Improv BLE scan/enumeration path so a reserved board can be
picked by its advertised name reliably. This is the scan-side half of the iOS
HITL `connect` blocker; it is NOT RF/range (every DUT is in range, and RSSI 127
is CoreBluetooth's "unavailable" sentinel, not distance).
The heapless-netstack Improv peripheral advertises the 128-bit Improv service
UUID + Flags in the PRIMARY advertisement (which fills the 31-byte legacy PDU),
so the device NAME can only ride the SCAN RESPONSE. `scanImprovNative` scanned
with the plugin default `allowDuplicates: false`, so CoreBluetooth reported each
matching peripheral exactly ONCE — and that single discovery callback routinely
fires before the scan response arrives, so `localName` is empty and the board
surfaces nameless. A name-pinned headless pick then misses it.
- requestLEScan now passes `allowDuplicates: true`, so iOS re-reports each
peripheral on later advertising events (which carry the merged scan-response
name), letting the real name arrive within the scan window.
- scanImprovNative reports "" (not a "Splanc device" placeholder) when a sighting
carries no name, and callers keep the BEST name seen (never clobber a real name
with a not-yet-named sighting) and supply their own display fallback:
* harness.ts (HITL headless pick): merge-best-name; the "not found" error now
also logs each sighting's RSSI for diagnosis.
* blePicker.ts (shipping native chooser): refresh a row's label when the name
arrives on a later sighting.
Verified on the real iPhone + real C6 bench: with the fix the co-located Improv
boards surface WITH their names (880AAF, CA2BFE, 31687A, E2F5EF, CDFD25, 9C9E07,
2x ledmapper), each at a real RSSI.
KNOWN RESIDUAL (needs a firmware change, tracked separately): the reserved iOS-
bench C6 still surfaces nameless (as "(unnamed)@-60" — a real RSSI, i.e. actively
scanned) because its scan-response TX is starved by BLE/WiFi coex while it is the
radio-busy unprovisioned board (continuous Wi-Fi scan / DHCP). The netstack
exposes no GAP Device Name (0x2a00), so the name lives on-air only in the coex-
fragile scan response and there is no connect-to-identify fallback. The durable
fix is firmware: carry the name in the PRIMARY adv via BLE extended advertising,
or add a GAP 0x2a00 characteristic (+ a scanner connect-and-read fallback), or
give scan-response TX coex priority while advertising/unprovisioned.
Tests: //web:unit_tests 77/77, //web:web_ts_typecheck_test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fughilli
force-pushed
the
claude/ios-native-driver-wiring
branch
from
October 3, 2026 03:15
155aa6a to
65ce0f9
Compare
…ver GATT The reserved iOS-bench C6 surfaces to the iPhone at real RSSI but its scan-response NAME never reaches iOS: the name rides the scan response (the primary ADV is full with the Improv UUID) and BLE/WiFi coex on the busy unprovisioned board starves the SCAN_RSP TX reply window, so the board surfaces UNNAMED and the name-pinned pick can never match it. There was no GATT fallback for the name. Durable firmware fix: expose the name over GATT. - netstack/improv.rs: add a GAP Generic Access service (0x1800) with a Device Name characteristic (0x2A00, read) to the Improv GATT DB; set_device_name() updates it. iOS reads 0x2A00 on connect to populate CBPeripheral.name, so the name resolves independent of the coex-fragile scan response. - blehost/ble_ffi.rs: ns_ble_set_name now stores the live name and writes it to 0x2A00 as well as the scan response, and re-applies it whenever the service is rebuilt on a fresh connection (so a reconnect doesn't revert to the default). - Builds for esp32c6 and esp32c3; new host unit test for the 0x2A00 read. Scanner connect-and-read fallback (web/iOS): - capacitorImprov.ts: readGapDeviceName() connects to an unnamed peripheral and reads GAP 0x2A00 (recovering CBPeripheral.name on iOS, which reserves the GAP service), then disconnects. - driver/harness.ts: when no sighting carries the pinned name, connect-and-read the strongest unnamed candidate and fold the resolved name back into the match. - ui/screens/blePicker.ts: relabel a nameless row from its GATT name. Robust live-name resolution: - ios_serial_diag.py: add --app-reset (RTS/EN pulse, boot held high) that reboots into the application so the boot banner prints — unlike --reset, which can land the board in USB download mode where it never advertises. - ios_reservation.py: resolve_improv_name forces an app-reset and takes the LAST banner, so it reads the board's CURRENT NVS name every run (the shared bench board gets renamed by other activity; never hardcode it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ured
resolve_improv_name's app-reset pulsed RTS (EN) to reboot the C6 into the app
but left DTR deasserted. The Arduino USB-CDC gates its TX on DTR, so the boot
`[ble] advertising "<name>"` banner was produced but never transmitted — the
capture read empty and the name resolver returned "". On the live bench that
dropped the phone to the strongest-RSSI fallback, which provisioned a STRAY
board ("Led Widget CA2BFE") instead of the reserved c6-c, and the journey timed
out joining.
DTR is not a reset strap after the reset edge, so re-assert it once the chip is
running. Verified on the bench: the capture now reads the banner and resolves
the reserved board's live name ("HITL Test 97911", BLE addr d2:fe:ff:49:fd:8c,
build 170b337).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…olves On the live bench the reserved c6-c (busy, unprovisioned, new firmware) surfaced to the iPhone as the one UNNAMED board among 8 named idle boards — exactly the coex-starved scan-response. The connect-and-read fallback fired on it but came back empty: iOS reads GAP 0x2A00 ITSELF on connect and populates CBPeripheral.name LAZILY (it refuses an app read of the reserved GAP service), so a single immediate read raced ahead of it. readGapDeviceName now polls for a few seconds after connect — attempting the direct 0x2A00 read (which also triggers iOS service discovery → the GAP fetch) and the cached connected-device name each round — and the harness re-probes an unnamed device up to 4 times instead of once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stands up the Mac mini iOS HITL bench (Phase D) and makes the iOS app-driver journey lane run fully reservation-driven — one command from the container/CI, zero manual Mac steps — and gets
smokePASSING on the real iPhone.Verified
bazel run //pi/hitl/phone:ios_reservation -- --server http://mac-mini:8087 --journeys smoke --ble-mode virtualfrom the container drives the whole chain and ends in[phone] PASS smoke/ALL JOURNEYS PASSED: reserveios-phone→ darwin-runner ssh → ship the harness into the reserved session → runphone_e2eon the Mac → the launchd build-server (a user LaunchAgent) cap-syncs + signs + installs + launches the Capacitor app on the physical iPhone → the app loads the station URL, enters driver mode, connects back to the driver WS, runs the journey. Unit tests + builds green (//pi/hitl/tests:hitl_test,//pi/hitl/phone:phone_target_test,//pi/hitl/phone:ios_reservation_test).What it adds
Native iOS driver wiring — a native WKWebView loads the bundled
web/distwith no query string, so the?driver=…&ble=realseam (web/src/ui/app/main.ts) was unreachable. Point Capacitor'sserver.urlat the station-served app URL (which carries the query) viaCAP_SERVER_URL, so the WebView enters driver mode; native BLE stays on the Capacitor Improv plugin, and the C6's self-signed wss is handled by the existing@splanc/wss-bridgeplugin.phone_target.py's iOS client hits the realtools/ios_build_server.py/run?task=endpoint.launcher.serve_dirbinds 0.0.0.0; ATSNSAllowsLocalNetworking.Reservation-driven run path —
pi/hitl/phone/ios_reservation.py(container orchestrator) reserves the unit, ships the harness+app+solver+firmware as one dereferenced tar, discovers the C6 port dynamically (serial_discovery.py, Espressif VID 0x303a, by adapter serial), and runsphone_e2eon the Mac.hitl-darwin.nixmakes every piece a flake-provided service.The macOS bring-up (each commit fixes a distinct, live-diagnosed blocker):
dscl(sysadminctl hangs on the FDE dialog), with a properAuthenticationAuthority, and added to thecom.apple.access_sshSACL;root:0600authorized_keysviaAuthorizedKeysCommand … AuthorizedKeysCommandUser root(macOS sshd reads it as the user, which can't);system.primaryUser, logs to~/Library/Logs, a hermetic tool PATH, and a PathState gate; workspace on an internal path (a launchd daemon can't read/Volumes— TCC);set-key-partition-listfor non-interactive key access;Known follow-up
connect/config/mapping_*are the next journeys — they need the C6 flashed + provisioned over real BLE and the netstack staying associated on a reachable AP (the same constraint the Android lane hit). This PR delivers the bench + the native driver lane + smoke-green.🤖 Generated with Claude Code