Conversation
`--ble` advertises one GATT service with a shot and a control characteristic, speaking phone schema version 2 (the only version; none was released before it). Shots carry event_id, shot_number, profile, carry_range, spin_source, launch_angle_confidence, final and enrichment, and hardware-enriched shots arrive provisional then final with one event_id. Phones get club_changed, profiles, power_status, shot_processing, session_cleared and shot_deleted events, and can get_club, set_club, get_profiles, set_active_profile and get_power_status through the same server functions Socket.IO uses. BLE is unauthenticated, so it is read-and-select only. Club selection becomes one Pi-owned value (apply_club_selection) that the kiosk, phones and simulators share and that is broadcast over Socket.IO and BLE. scripts/setup/configure_bluetooth.sh (offered by setup.sh) sets [GATT] Client = false so BlueZ stops asking iPhones to pair every 30 s. The optional `ble` extra pins bless 0.3.0 on Linux, and start-kiosk.sh --ble syncs it. Tests drive the real publisher over a loopback fake of Bless/BlueZ, and tests/fixtures/ble_goldens/ holds framed hex goldens for client test suites; they are byte-identical to what the current phone app was tested against on a Pi 5. Ported from jake-fishtech's feat/iOS-ble work and open-flight#282, split out so the network transport, catch-up, phone calibration and mock hardware simulation follow as separate PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9pYpWtxN9nJ6vpqbXwB7m
16 of 17 tasks
This branch has not been 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.
Reviewing this PR
Most of the +5,309 lines are tests and generated fixtures. The production code is about 1,120 lines:
src/openflight/ble/(864),src/openflight/server.pywiring (256)scripts/setup/configure_bluetooth.sh,setup.sh,start-kiosk.sh,pyproject.tomltests/test_ble_*.py,tests/ble_harness.py, server and transport teststests/fixtures/ble_goldens/, written byscripts/ble/generate_goldens.pyand checked by a testscripts/hardware-test/test_ble_advertise.py(advertising probe)docs/ios-ble.md, changelog, navSuggested reading order:
ble/protocol.py(the wire contract), thenble/publisher.py, then theserver.pydiff, thentests/test_ble_loopback.pyfor end-to-end behaviour. The goldens can be skimmed: they are generated, and a test fails if they drift from the encoder.What does this PR do?
Adds a Bluetooth LE connection for phone apps:
--bleadvertises one GATT service with a shot (notify) and a control (write + notify) characteristic, speaking phone schema version 2.event_id,shot_number, profile,carry_range,spin_source,launch_angle_confidence,finalandenrichment. Hardware-enriched shots arrive twice, provisional then final, with oneevent_id.club_changed,profiles,power_status,shot_processing,session_cleared,shot_deleted.hello,get_club,set_club,get_profiles,set_active_profile,get_power_status. Each calls the same server function as its Socket.IO counterpart, so the kiosk and every client see the same broadcasts.apply_club_selection), shared by the kiosk, phones and simulators and broadcast over Socket.IO and BLE.scripts/setup/configure_bluetooth.sh(offered bysetup.sh) sets[GATT] Client = falsein/etc/bluetooth/main.conf. Without it, BlueZ's own GATT client makes iOS show a pairing prompt every ~30 s and drops the link. Stock Raspberry Pi OS lists#Clientunder[CSIS], where bluetoothd ignores it, so the script writes it under[GATT].bleextra (bless==0.3.0on Linux).start-kiosk.sh --blesyncs it, and without BlueZ or the extra,--blelogs that Bluetooth is unavailable and carries on.Wire format and troubleshooting:
docs/ios-ble.md.Why was this required?
This is the first piece of #282, which reviewers asked to have split up (it was ~7,000 lines). This PR is the core that makes a phone app work over Bluetooth. It is v2-only: no app was released against the earlier version one, so the second (v1) characteristic pair and its compatibility code are not included.
Follow-ups, each a separate PR on top of this one:
/api/shots/streamand/api/clublast_event_id/Last-Event-ID)Breaking for unreleased apps: a phone app must discover the schema-2 characteristics (
ED365FE6…shot,7BA96E63…control). The KMP companion app has been updated for this. jake-fishtech's SwiftUI app onfeat/iOS-blestill expects the v1 pair and needs the same change.Automated tests
tests/test_ble_protocol.py: payloads,hello, the size budget (worst-case shot and 12 worst-case profile names fit one BLE message) and framing.tests/test_ble_publisher.py: delivery, queueing, subscription hooks, control reassembly, and startup isolation.tests/test_ble_loopback.py: end to end against the real server dispatch, using a loopback fake of Bless/BlueZ (tests/ble_harness.py). Covers latest-shot replay, provisional → final reaching every phone, club/profile/power commands, read-and-select enforcement, and one phone leaving while another keeps receiving.tests/test_ble_goldens.py+tests/fixtures/ble_goldens/: framed hex goldens for client test suites (scripts/ble/generate_goldens.py --checkfails on drift).tests/test_configure_bluetooth.py: the BlueZ config script, withsudo/systemctlstubbed so it is safe to run on a Pi.tests/test_phone_transport.py,tests/test_phone_transport_server.py,tests/test_control_commands.py,tests/test_server.py: server fan-out and command routing, and a failed WebSocket emit no longer stopping BLE.uv run pytest tests/: 1720 passed. The only failures are 11 OpenCV camera tests that already fail onmainwhencv2is not installed (fixed separately).Manual (human) testing
On a Raspberry Pi 5 (Raspberry Pi OS trixie, BlueZ 5.82) running
./scripts/start-kiosk.sh --mock --ble, with the KMP companion app on an iPhone:configure_bluetooth.sh, iOS showed a pairing prompt and the link dropped every ~33 s. btmon showed BlueZ's GATT client triggering an SMP Security Request. After the script, there was no SMP traffic and the connection held for 20+ minutes.[BLE] Client subscribed).shot_deleted,session_cleared,power_status, provisional → final shots, the failed-capture state, and the calibration error path.Checklist
uv run pytest tests/ -v), except the 11 pre-existing OpenCV failures noted aboveuv run pylint src/openflight/ --fail-under=9): 9.76, unchangeduv run ruff check src/openflight/)cd ui && npm run build): no UI changes; the kiosk built and ran it on the Picd ui && npm run lint): no UI changes🤖 Generated with Claude Code