You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
5/MG battery-pack poll is refused on every keepalive tick, on both platforms #1948
The 5/MG keepalive polls GET_BATTERY_PACK_INFO (151) every other tick, but the
send allowlist can never admit it on that path, so the call is refused every
time. Seen 40 times in 40 minutes of a Test Centre capture on an MG:
12:31:48 send(GET_BATTERY_PACK_INFO) skipped — no WHOOP 5/MG framing for this command yet
12:32:48 send(GET_BATTERY_PACK_INFO) skipped — no WHOOP 5/MG framing for this command yet
...
13:11:37 send(GET_BATTERY_PACK_INFO) skipped — no WHOOP 5/MG framing for this command yet
Why it can never pass
Android admits 151 only while a user-initiated probe is in flight:
The keepalive is not a probe, so the periodic send at the WHOOP5 branch of the
keep-alive tick is refused on every tick.
iOS is the same call with less around it: BLEManager.swift sends .getBatteryPackInfo on the same cadence, and its allowlist has no clause for
that command at all. FrameRouter says as much about its own decoder:
until now nothing sent the command, so the decoder had no caller and the
offsets have never been seen against a live strap
What it costs
Small but real: one refused command and one log line per minute per platform,
and a Test Centre battery capture on a 5/MG carries a wasted line every minute.
The bigger cost is the comment. Both platforms carry this at the call site:
The 5/MG battery pack rides the SAME cadence as the strap's own gauge, the
same question about the same physical thing, and a second timer would only be
a second thing to get wrong.
It never rides anything. On a 5/MG, packSocPct can only come from the pushed
pack-info event (109), which is now its only writer following #1945.
Options
Drop the periodic send on both platforms and let 109 be the documented single
source, correcting the comment.
Admit 151 on the cadence path, if a repeating poll of that opcode is actually
wanted, which would need the same hardware question answered that the probe
gate was written to ask.
Option 1 matches what the code already does in practice. Option 2 is a
behaviour change on a read-only opcode that has never been sent on a cadence to
any strap.
Found while reading an MG capture for an unrelated bond question.
Correcting the cost above: I wrote "one refused command and one log line per
minute per platform". That is right for Android and wrong for iOS.
The Swift allowlist has no clause for .getBatteryLevel either, not just for .getBatteryPackInfo. Both are sent from the same batteryPollDue block, so a
5/MG paid TWO dead sends and two skip lines per tick there, and the whole block
was dead rather than half of it.
Neither is needed: the strap's percent comes from the 0x2A19 read that enableLiveNotifications drives off the same keep-alive, and refreshBattery
already guards the same command with if selectedModel.deviceFamily == .whoop4,
so the fix follows a pattern the file had already established.
One more narrowing, on who actually paid that cost.
keepAliveMayRun refuses an unbonded 5/MG outright, so the tick never fires
there and the dead sends were only ever paid by a BONDED 5/MG. My note above
said the block was dead on a 5/MG without qualifying which, which overstates it
for exactly the straps #1635 is about.
It makes the removal easier to justify rather than harder: the same didBond
that lets the tick run has already fired enableLiveNotifications a few lines
above, and that is what drives the 0x2A19 read a 5/MG's percent comes from. The
read and the refused send were always on the same tick, so dropping the send
cannot strand the reading.
Landed in ad7e625ba: a 5/MG is no longer asked for battery it refuses to answer, so the dead sends and their skip lines are gone from the keepalive tick. Guarded on both platforms, by Whoop5BatteryPollGuardTests and ChargingAndReleaseTest.
Shipped in testing build 451 onward.
Recording the narrowing from the comments above, since it is the part worth keeping: keepAliveMayRun already refuses an unbonded 5/MG, so the dead sends were only ever paid by a BONDED one. The original note did not qualify which, and overstated the cost for exactly the straps #1635 is about.
Activity
ryanbr commentedon Sep 7, 2026
Correcting the cost above: I wrote "one refused command and one log line per
minute per platform". That is right for Android and wrong for iOS.
The Swift allowlist has no clause for
.getBatteryLeveleither, not just for.getBatteryPackInfo. Both are sent from the samebatteryPollDueblock, so a5/MG paid TWO dead sends and two skip lines per tick there, and the whole block
was dead rather than half of it.
Neither is needed: the strap's percent comes from the
0x2A19read thatenableLiveNotificationsdrives off the same keep-alive, andrefreshBatteryalready guards the same command with
if selectedModel.deviceFamily == .whoop4,so the fix follows a pattern the file had already established.
Option 1 is up in #1951, both platforms.
ryanbr commentedon Sep 7, 2026
One more narrowing, on who actually paid that cost.
keepAliveMayRunrefuses an unbonded 5/MG outright, so the tick never firesthere and the dead sends were only ever paid by a BONDED 5/MG. My note above
said the block was dead on a 5/MG without qualifying which, which overstates it
for exactly the straps #1635 is about.
It makes the removal easier to justify rather than harder: the same
didBondthat lets the tick run has already fired
enableLiveNotificationsa few linesabove, and that is what drives the
0x2A19read a 5/MG's percent comes from. Theread and the refused send were always on the same tick, so dropping the send
cannot strand the reading.
battery: stop asking a 5/MG for battery it refuses to answer (#1951)
ryanbr commentedon Sep 9, 2026
Landed in
ad7e625ba: a 5/MG is no longer asked for battery it refuses to answer, so the dead sends and their skip lines are gone from the keepalive tick. Guarded on both platforms, byWhoop5BatteryPollGuardTestsandChargingAndReleaseTest.Shipped in testing build 451 onward.
Recording the narrowing from the comments above, since it is the part worth keeping:
keepAliveMayRunalready refuses an unbonded 5/MG, so the dead sends were only ever paid by a BONDED one. The original note did not qualify which, and overstated the cost for exactly the straps #1635 is about.