Answer Apple's 2.1 request, fix three bugs found on the bike, and measure what riding apps really send - #41
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the App Store submission documentation to align with Apple’s Guideline 2.1 “Information Needed” rejection, specifically clarifying that a physical-device screen recording is required and providing an updated, shootable recording plan plus matching App Review Notes text.
Changes:
- Reworked
docs/DEMO_VIDEO.mdto make the recording mandatory, spell out Apple’s requirements, and rebuild the shot list around a realistic filming flow. - Updated
docs/APP_STORE.mdStage 6 Notes template to mirror the live App Review Notes and added a Guideline 2.1 row to the rejection-reasons table.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| docs/DEMO_VIDEO.md | Makes the screen recording requirement explicit; defines Apple-required constraints; provides an updated shot list and App Review reply template. |
| docs/APP_STORE.md | Updates the App Review Notes copy and rejection table to reflect the 2.1 request and the now-required recording. |
Suppressed comments (1)
docs/DEMO_VIDEO.md:146
- The reply template hard-codes the recording device and iOS version ("iPhone 17 Pro running iOS 26.6"), which will be wrong if the actual recording is captured on a different model or after an iOS update. Since Apple’s requirement is "latest OS", the reply should describe that generically and avoid forcing a specific device/version.
> Thank you. A screen recording made on a physical iPhone 17 Pro running iOS 26.6
> is attached. It starts by launching the app from the Home screen, shows the
> Bluetooth permission prompt, the app finding and connecting to a Wahoo KICKR V5
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| virtual gears. | ||
| HOW TO SET UP AND REACH THE MAIN FEATURES (point 4) | ||
| No account, login, credentials or sample files are needed. No hardware is needed either. | ||
| On the first screen, tap "Try Demo". A blue "Demo Mode - Simulated" notice confirms no trainer is connected and Bluetooth is not in use. Demo Mode gives you the production gear picker, the gear ladder, the large Easier and Harder controls, virtual and real-bike gearing, Settings, and simulated trainer, Click, Headwind and riding-app status. Tap "Exit Demo" to return to real trainer discovery. Demo Mode is in-memory only: it does not scan, advertise, connect or send Bluetooth commands, and it does not replace saved equipment. |
Comment on lines
+26
to
+27
| - **It must be a physical device on the latest OS.** An iPhone 17 Pro on iOS 26.6 | ||
| is what the app is tested on. |
App Review rejected 1.0 (5) under Guideline 2.1, Information Needed. Demo Mode was meant to spare us a recording. It did not: the first thing they asked for was a screen recording made on a physical device. DEMO_VIDEO.md now says the recording is required, lists what Apple demands of it, adds a shot for the Bluetooth prompt (they ask to see permission prompts) and carries the reply to send with the file. APP_STORE.md now holds the review notes that are actually live, answering their seven questions in order, and records what really happens when a reviewer has no hardware. Tested-on line reads iPhone 17 Pro, iOS 26.6 - the version the phone itself reports. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
The old list assumed a full ride: pedalling, power and cadence moving, a riding app already paired. Shifting does not need any of that - the trainer confirms a gear change whether or not the pedals are turning. The recording is now three acts: launch and connect, shift on the real trainer, then Demo Mode as the answer to a reviewer who has no trainer. The riding app becomes an optional fourth act. Also swap the Reset Location and Privacy advice for deleting and reinstalling the app, which brings the Bluetooth prompt back without wiping every other permission on the phone, and correct the reply text, which claimed the video shows pedalling. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
Two bugs found while getting the phone ready to record. The Click was given one three-second look just after launch and then never looked for again. A Zwift Click sleeps and only advertises for a moment after a button press, so that window almost always closed before the Click had said anything - and once it closed, pressing the button did nothing, because nothing was listening any more. Discovery now keeps looking and adopts the Click the moment it appears, which is what the wake instructions have always promised. The one-shot window that other screens still use goes from three seconds to eight, because three was too short to be honest about. The shifting screen's footer replaced itself with a single problem line whenever any non-optional device was not connected. The riding app counts as non-optional, so before the riding app connects - the normal state of things for the first minute - the whole footer became one line about the riding app and the trainer and Click states disappeared. Waiting for the riding app is not a fault, so the footer now only surrenders to a real warning, and lists every device with its own state the rest of the time. That also stops VoiceOver announcing a problem that was never there. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
… wait AppTap now says when a riding app does something odd: a reset, repeated control requests, commands sent without asking for control, commands after a stop, an implausible or changing wheel size, unknown command bytes, and writes to channels it has no business writing to. Tracing RealVelo with it turned up a reset a second and a half after connecting, which lands while gears are already set up. The KickrProbe reset-test answers whether that matters by leaving a wheel size no trainer defaults to, sending the same reset, and working out what the trainer is riding on afterwards. It could not, at first. run() executes inside settleTask, and subscribing to the speed channel mid-run fired the callback that cancels it. A cancelled task's sleeps return at once, so every wait collapsed and the pedals were never given a chance to turn. Waiting is also patient now: the person doing this has to walk to the bike, and a deadline only makes them race the tool. run-tool.sh kills a previous instance of the same tool, after three AppTaps appeared in one pairing list at once. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
Turning a direct-drive trainer by hand takes real effort even with no resistance, so promising it will be light is wrong twice over: it reads as a fault when nothing is wrong, and it teaches people to ignore the one sign that does matter, which is the trainer pushing back harder the faster they go. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
RealVelo was traced on the wire for the first time. It never sets a wheel size across a whole ride, where FulGaz sets one at every ride start, so the app cannot assume either. It also sends a reset while connecting, which by the FTMS rules would return a trainer to its defaults and silently undo the current gear. That was measured on a real KICKR rather than argued about: leave a wheel size nothing defaults to, send the same reset, then work out what the trainer is riding on. It does not touch the wheel size, so passing a reset through is safe. The power-cut measurement is recorded for the first time as well, and it is the opposite answer: the trainer does keep an odd wheel size across a power cut, so putting it back after a crash genuinely matters. An earlier run of it was made with the probe that could not wait, so it is noted as re-measured since. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
A Click sleeps until a button is pressed, so pairing one means pressing a button — and it often connects with that button still down. The edge tracker assumed both buttons start up, so the first report read as a fresh press: a gear moved on its own, and half a second later the hold began and kept moving them until the rider let go. Reported from the bike as a shift five to ten seconds after connecting, which is the Click joining after the trainer. The first report after connecting is now only a baseline. The tracker is built fresh for every connection, so this is per connection rather than per launch, and a button already down at connect is simply where that button started. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
The first RealVelo capture advertised as "AppTap" so the rider could tell it apart from the real thing. That is a deviation, because a riding app may treat a name it does not recognise differently, and the findings were about to be written down as fact. Run again under the shipping name, RealVelo did the same thing in the same order within the same fraction of a second, and again set no wheel size at all. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
It advertised as "Virtual Gears" by default, which is the name the phone a few feet away is already using. A riding app that picks the phone instead looks exactly like a tap that sees nothing: connected on one screen, silent on the other, with no way to tell from either which device answered. It is "AppTap" now, and the real name stays available for checking whether a riding app treats it differently. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
macOS does not loop Bluetooth advertisements back to itself, so a riding app on this Mac never finds a tap on this Mac. It finds the phone instead and reports itself connected, which is indistinguishable from a tap that is not working. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
Reproduced twice on 2026-08-14: once with the Mac tap against MyWhoosh on an iPad, and once with the shipping app driving the real KICKR. Both showed 0 W and 0 rpm. MyWhoosh subscribes to all three channels, requests control, sends a terrain update, and then accepts 1608 indoor bike data notifications over 26 minutes without complaint while displaying nothing. Nothing on our side explains it: the feature characteristic declares cadence and power, the packet is spec-shaped, and Bluetooth accepted every push. The cause is recorded as unknown rather than guessed at. FulGaz and RealVelo still work, so this is specific to MyWhoosh. Said so plainly in the README and the requirements page rather than leaving readers with "ordinary FTMS riding apps". Also corrected the AppTap name note. A riding app shows the advertised name while scanning and the Mac's own name once connected, so both can appear in a pairing list at once, and no --name run has ever tested the connected name. The tap gained a count of the ride data it sends, which is what made it possible to rule out our own side. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
sbroenne
force-pushed
the
review-2-1-reply
branch
from
August 16, 2026 05:54
eb35428 to
b14abb8
Compare
added 3 commits
August 16, 2026 10:06
Advertise FTMS and Cycling Power together, make both live measurement characteristics readable, and keep the proxy screen awake while it is available. Record the four-build hardware proof and teach the diagnostic tools to enforce the working contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
Acknowledge the GPL-3.0 QZ project as the public behavioral comparison that led to the FulGaz Windows experiment, while clarifying that no QZ code is copied or bundled. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
Bump the app build number and record the trainer-proxy lifecycle, configurable wheel fallback, Cycling Power support, FulGaz Windows compatibility, and screen-awake behavior included in the uploaded build. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c3d711c-cb97-4bfa-a302-2d01fe5d2f1f
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.
Apple rejected 1.0 under Guideline 2.1 and asked for a screen recording made on a
physical device, plus written answers. The written answers are already live in App
Store Connect; this branch carries the shot list for the recording — and everything
found while getting a phone ready to shoot it.
Ready for Apple
docs/DEMO_VIDEO.mdis a shot list that can actually be filmed: launch andconnect, shifting on the real trainer, then Demo Mode. No pedalling, no riding
app required. It also holds the reply text, corrected to stop claiming the video
shows pedalling.
The video has not been recorded yet. Do not resubmit before it exists.
Three bugs, all found on the bike and all confirmed fixed there
stopped listening for good. A Click sleeps and only advertises briefly after a
button press, so that window had almost always closed first. It now keeps looking.
swapped itself for a single problem line whenever anything was not
.ok, and"waiting for the riding app" — normal for the first minute of every session — was
enough to hide the trainer and the Click. Only real faults do that now.
often connects with that button still down. The edge tracker read the first report
as a fresh press: a gear moved on its own, and half a second later the hold began
and kept moving them. The first report after connecting is now only a baseline.
What the hardware actually said
Three measurements, with their logs, in
DEVELOPMENT.md:FulGaz sets one at every ride start. The app cannot assume either.
connecting, which by the FTMS rules should return the trainer to its defaults and
silently undo the current gear. Measured on a real KICKR V5 rather than argued
about: leave a size nothing defaults to, send the same reset, work out what the
trainer is riding on. It survives, so passing resets through is safe.
time. Putting the wheel size back after a crash genuinely matters; nothing else
will do it.
The probe could not wait
KickrProbe.run()executes insidesettleTask, and subscribing to the speed channelpart-way through fired the callback that cancels it. A cancelled task's sleeps return
at once, so every timed wait collapsed and the pedals were never given a chance to
turn. Fixed, and waiting is patient now — the person doing this has to walk to the
bike, and a deadline only makes them race the tool. Hardware evidence recorded before
this fix should be treated with suspicion; the power-cut figure above was re-measured
after it.
AppTapnow calls out anything unusual as it happens rather than leaving it buried ina list of opcodes, and
run-tool.shkills a previous instance of the same tool, afterthree AppTaps once appeared in one pairing list at the same time.
Still outstanding
without pedalling.
Resolves #1