Skip to content

Answer Apple's 2.1 request, fix three bugs found on the bike, and measure what riding apps really send - #41

Merged
sbroenne merged 16 commits into
mainfrom
review-2-1-reply
Aug 16, 2026
Merged

Answer Apple's 2.1 request, fix three bugs found on the bike, and measure what riding apps really send#41
sbroenne merged 16 commits into
mainfrom
review-2-1-reply

Conversation

@sbroenne

@sbroenne sbroenne commented Aug 14, 2026

Copy link
Copy Markdown
Owner

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.md is a shot list that can actually be filmed: launch and
    connect, 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

  • The Zwift Click was never found. Discovery ran once for three seconds and then
    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.
  • Every device state vanished while the riding app was disconnected. The footer
    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.
  • Waking the Click shifted a gear. Pairing one means pressing a button, and it
    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:

  • RealVelo never sets a wheel size, across a whole ride and 147 terrain updates.
    FulGaz sets one at every ride start. The app cannot assume either.
  • A riding app's reset does not wipe the wheel size. RealVelo sends one while
    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.
  • The trainer keeps an odd wheel size across a power cut. Recorded for the first
    time. Putting the wheel size back after a crash genuinely matters; nothing else
    will do it.

The probe could not wait

KickrProbe.run() executes inside settleTask, and subscribing to the speed channel
part-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.

AppTap now calls out anything unusual as it happens rather than leaving it buried in
a list of opcodes, and run-tool.sh kills a previous instance of the same tool, after
three AppTaps once appeared in one pairing list at the same time.

Still outstanding

  • The video is not recorded and nothing has been resubmitted to Apple.
  • Nothing on this branch has been ridden. The fixes above were confirmed on the bike
    without pedalling.
  • Zwift and MyWhoosh have not been traced.

Resolves #1

Copilot AI lite review requested due to automatic review settings August 14, 2026 10:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.md to make the recording mandatory, spell out Apple’s requirements, and rebuild the shot list around a realistic filming flow.
  • Updated docs/APP_STORE.md Stage 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.

Comment thread docs/APP_STORE.md
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 thread docs/DEMO_VIDEO.md
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.
@sbroenne sbroenne changed the title Answer Apple's 2.1 request, and plan a video we can actually shoot Answer Apple's 2.1 request, fix three bugs found on the bike, and measure what riding apps really send Aug 14, 2026
Stefan Brönner and others added 13 commits August 16, 2026 07:50
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
Stefan Brönner 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
@sbroenne
sbroenne merged commit 4658319 into main Aug 16, 2026
4 of 5 checks passed
@sbroenne
sbroenne deleted the review-2-1-reply branch August 16, 2026 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prove KICKR V5 wheel-circumference control on iPhone

2 participants