Repository navigation
feat(phone): network shot stream, club API and reconnect catch-up - #297
Merged
Merged
Conversation
Adds the network transport for phone apps and catch-up of missed shots after a reconnect, on top of the BLE transport from open-flight#285. - GET /api/shots/stream serves the schema 2 phone messages as Server-Sent Events: current club, profiles and power status, then the session's shots, then live shots and events, with a 15 s heartbeat. Up to eight clients; 503 beyond that. - GET/POST /api/club reads and sets the club through apply_club_selection, and club changes are broadcast on the stream. - A reconnecting phone names the newest shot it has (last_event_id in BLE hello, or Last-Event-ID on the stream) and is sent that shot and every later current-session shot, oldest first, up to 20. Replays are the exact bytes sent live; cleared and deleted shots are never replayed. hello advertises the shot_catch_up feature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 9, 2026
A tester's phone missed a kiosk deletion and 35 of 55 shots taken while it was disconnected: catch-up never told the phone about deletions and sent only the newest 20 shots. - Catch-up now sends the shots deleted while the phone was away first, as shot_deleted events: those made after the named shot was first sent, or every remembered one (the last 100) when the anchor is unknown. PhoneShotCache orders sends and deletions with one counter. - The catch-up limit is 100 shots instead of 20. - A BLE catch-up leaves two delivery-queue slots free, so a live shot taken meanwhile no longer pushes a missed shot out of the queue. - hello advertises deletion_catch_up, and the network stream names the same features in an OpenFlight-Features header, so a phone app can tell a Pi that predates them and suggest an update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 tasks done
16 of 17 tasks
btripp
marked this pull request as ready for review
October 12, 2026 17:56
This was referenced Oct 12, 2026
jewbetcha
approved these changes
Oct 18, 2026
…stream # Conflicts: # docs/changelog.md
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
Production code is about 520 lines; the rest is tests (~1,115) and docs (~120).
src/openflight/shot_stream.py(new)src/openflight/phone_catch_up.py(new)hellocatch-upsrc/openflight/ble/publisher.py,ble/protocol.py(shot_catch_upanddeletion_catch_upfeatures)src/openflight/server.py:/api/shots/stream,/api/club,phone_catch_up(), stream fan-outSuggested reading order:
phone_catch_up.py(the rule),shot_stream.py, theserver.pydiff, thentests/test_shot_stream_catch_up.pyandtests/test_ble_catch_up.py.What does this PR do?
Follow-ups 1 and 2 from #285, in one PR because catch-up spans both transports:
GET /api/shots/streamsends the same schema 2 messages as BLE as Server-Sent Events. A stream opens with: ping, the currentclub_changed,profilesand (with--battery)power_status, then the session's shots, then live shots and events, with a heartbeat every 15 s. No flag needed: it exposes nothing the browser UI does not already broadcast. Up to eight clients,503beyond that.GET/POST /api/clubreads and sets the club through the sameapply_club_selectionas the kiosk and BLE; club changes are broadcast on the stream too.last_event_idin BLEhello, orLast-Event-ID/?last_event_id=on the stream) and gets that shot again plus every later current-session shot, oldest first, up to 100. Replays are the exact bytes last sent live (so a phone that only had a provisional shot gets the final); cleared and deleted shots are never replayed. Shots deleted on the kiosk while the phone was away are sent first, asshot_deletedevents (those after the named shot, or the last 100 remembered when the anchor is unknown). A BLE catch-up leaves two delivery-queue slots free, so a shot taken meanwhile doesn't push a missed one out.helloadvertisesshot_catch_upanddeletion_catch_up; the stream names the same features in anOpenFlight-Featuresheader so phone apps can tell an older Pi apart.Why was this required?
#285 shipped the BLE core of #282, which reviewers asked to have split up. This is the next piece. Phones without working BLE advertising (for example on the Pi kernel
6.18.34+rpt-rpi-2712regression) need an IP transport, and any phone that leaves the app or walks out of range currently loses every shot taken while away.This PR is independent of the other two follow-ups (phone-assisted IWR6843 tilt calibration, mock-mode hardware simulation); they can merge in any order.
Automated tests
tests/test_shot_stream.py: broker fan-out, seeding, heartbeat, subscriber limit, slow-client dropping.tests/test_phone_catch_up.py: the catch-up rule (named shot resent, unknown/invalid id → whole session, 100-shot cap), the shot cache, and the deletions since an anchor.tests/test_shot_stream_catch_up.py,tests/test_ble_catch_up.py: catch-up end to end on each transport, includinghellobefore subscribing, cleared/deleted shots, provisional → final, deletions missed while away, a 55-shot gap (a tester's), a live shot during catch-up, and the features header.tests/test_phone_transport.py,tests/test_phone_transport_server.py,tests/test_control_commands.py,tests/test_server.py:/api/club, the stream route, fan-out to SSE and BLE, and a failing transport not affecting the shot pipeline.tests/fixtures/ble_goldens/v2_response_hello.jsonregenerated for the new feature flag (scripts/ble/generate_goldens.py --checkpasses).uv run pytest tests/: 1858 passed, 11 skipped. On my Mac 13 camera tests (test_camera_ball_flight.py,test_camera_club_delivery.py,test_camera_club_motion.py) fail; they fail identically on the commit before this one, so they are unrelated.The phone app (btripp/openflight-mobile-kmp) runs its data layer against
openflight-server --mockfrom this branch: a shot taken and a shot deleted on the kiosk while the app was stopped both reach the phone on reconnect. Againstmainand #282 the deletion arrives over Wi-Fi (Socket.IOsession_state) but the shot does not, and the app shows an "update OpenFlight on your Pi" notice.Manual (human) testing
On a Mac with
openflight-server --mock --web-port 8765:GET /api/club→{"club":"driver","status":"current"};POST {"club":"7-iron"}applied and appeared asclub_changedon an opencurl -Nstream;POST {"club":"putter"}→400;?schema=1→400.event: shotwith anid:.Last-Event-IDset to the first shot replayed shots 1–3 in order; reconnecting with no id replayed all 3.503.Re-run after merging current
mainand the deletion catch-up commit: a realopenflight-server --mockon macOS, driven over HTTP, SSE and Socket.IO:GETreturns the current club. A validPOSTis applied and appears asclub_changedon an open stream. Unknown clubs, an empty body,"unknown"and a non-JSON body all return400, and the club stays the same.text/event-streamand carries anOpenFlight-Featuresheader that includesshot_catch_upanddeletion_catch_up. Eachevent: shotframe'sid:equals the payload'sevent_id.schema=1,3andabcreturn400.Last-Event-IDreplayed 1, 2, 3, 4, oldest first.?last_event_id=gives the same result, and the header wins when both are sent. No id, or an unknown one, replays the whole session.shot_deletedfirst, then shots 1, 2 and 4; shot 3 was not replayed. Anchoring on a shot taken after the delete sends noshot_deleted. An open stream getsshot_deletedlive.200, and the ninth gets503 Shot stream already has 8 clients. A: pingheartbeat arrives every 15 s.Not yet tested: BLE reconnect catch-up with the phone app on a Pi. No Pi was set up for this PR; I'll add the results here once one is.
Checklist
uv run pytest tests/ -v)uv run pylint src/openflight/ --fail-under=9): 9.61 after merging currentmain; no messages on lines this PR addsuv run ruff check src/openflight/)cd ui && npm run build): no UI changescd ui && npm run lint): no UI changesdocs/ios-ble.md,docs/changelog.md)🤖 Generated with Claude Code