Skip to content

Extended-battery probe: clean, copyable output + capture diff for field mapping (#592) - #604

Merged
ryanbr merged 1 commit into
mainfrom
feat/battery-probe-capture-diff-592
Jul 19, 2026
Merged

ryanbr merged 1 commit into
mainfrom
feat/battery-probe-capture-diff-592

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner

Reworks the #592 probe output after the capture confirmed opcode 98 returns a rich 29-byte payload on the 4.0. Two things: make the result readable/copyable, and make multiple captures do the field-mapping (one frame can't, responsibly).

Cleaner output

A pure, unit-tested formatExtendedBatteryProbe renders the result as a clean report instead of a dense run-on. Your capture now reads:

#592 EXTENDED-BATTERY PROBE — WHOOP 4.0
Verdict: opcode 98 ACCEPTED — 29-byte payload

Raw frame (40 B):
aa2400fa24c6620d010165006bff820f0c0128000f05e90321120200010100001a0000004675fe58

Payload (29 B, CRC excluded):
  @00  0d 01 01 65 00 6b ff 82
  @08  0f 0c 01 28 00 0f 05 e9
  @16  03 21 12 02 00 01 01 00
  @24  00 1a 00 00 00

Voltage: 3.97 V  (mV=3970 @07) — the field NOOP already reads

Δ vs previous capture: first capture — probe again at another battery % to diff

Full raw hex stays on one line (copies cleanly); the payload is an offset-labelled hex grid; the voltage is decoded inline.

Field-mapping scaffold

Persists each payload (SharedPreferences, so captures hours/days apart still diff) and, on the next probe, shows a per-byte delta. The layout can't be read off a single frame — the fields are identified by what they track: capture at 60% and 40% → the byte(s) that moved are SoC/capacity; cold vs worn → the byte that drifted is temperature; a byte that only ever climbs is cycle count.

Tests

ExtendedBatteryProbeFormatTest pins it against the real 4.0 capture (98 accepted, mV=3970 → 3.97 V, hex grid, first-capture), the diff (a moved byte is flagged with its offset), a bare stub, and a 5/MG UNSUPPORTED(3) verdict — 4 cases, all JVM-pure.

Verification

  • Full Android module compiles; testFullDebugUnitTest green; i18n_audit.py --ci main exit 0; byte-scan confirms no stray control chars.
  • Read-only, additive; no new opcode, no automatic sends. Swift twin to follow with the probe twin.

Companion to #603 (surfaces the confirmed voltage on the Devices card now).

@ryanbr ryanbr changed the title Extended-battery probe: diff each capture vs the previous for field mapping (#592) Extended-battery probe: clean, copyable output + capture diff for field mapping (#592) Jul 19, 2026
@ryanbr
ryanbr force-pushed the feat/battery-probe-capture-diff-592 branch from f2baa39 to 43bce10 Compare July 19, 2026 03:52
…ld mapping (#592)

The 4.0 confirmed opcode 98 with a rich 29-byte payload, but the result was a
dense run-on and one frame can't map its fields responsibly. This reworks the
probe output:

- A pure, unit-tested `formatExtendedBatteryProbe` renders a clean report:
  a verdict header, the full raw hex on one copyable line, an offset-labelled
  payload hex grid (8 bytes/row), the decoded pack voltage, and a per-byte diff
  vs the previous capture.
- Field-mapping scaffold: persists each payload (SharedPreferences) and diffs
  the next one against it. A byte that MOVES between captures at different
  battery %/temperature is that field — SoC/capacity tracks charge, temperature
  drifts with wear, cycle count only ever climbs.
- ExtendedBatteryProbeFormatTest pins it against the REAL 4.0 capture (98
  accepted, mV=3970 → 3.97 V), the diff, a bare stub, and a 5/MG UNSUPPORTED
  verdict.

Read-only, additive; no new opcode, no automatic sends. Full module compiles;
testFullDebugUnitTest green; i18n audit --ci exit 0. Swift twin to follow.
@ryanbr
ryanbr force-pushed the feat/battery-probe-capture-diff-592 branch from 43bce10 to 9627555 Compare July 19, 2026 03:55
@ryanbr
ryanbr merged commit 744ad09 into main Jul 19, 2026
1 check passed
@ryanbr
ryanbr deleted the feat/battery-probe-capture-diff-592 branch July 19, 2026 04:00
ryanbr added a commit that referenced this pull request Jul 19, 2026
* iOS/macOS twin of the #592 extended-battery probe + voltage (#592)

Swift port of the Android probe tooling (#599/#601/#603/#604): the read-only
GET_EXTENDED_BATTERY_INFO(98) probe, its copyable result dialog, the strap
voltage on the Devices card, and the field-mapping capture diff.

- WhoopProtocol: ExtendedBatteryProbe.format(...) — the pure formatter (verdict,
  full raw hex, offset-labelled payload grid, decoded voltage, per-byte capture
  diff). Placed in the package (not app-side like Kotlin) so swift test covers it
  on Linux/CI; behaviour is byte-identical to the Kotlin formatter. 5 tests pin it
  against the real 4.0 capture, the diff, a bare stub, and a 5/MG UNSUPPORTED
  verdict.
- BLEManager: probeExtendedBatteryInfo() (+ 8s no-reply timeout, guard-then-set is
  race-free since BLE callbacks + the timer both run on main), clearExtended...,
  the frame[6] (4.0) and frame[10] (5/MG) COMMAND_RESPONSE dumps, the 5/MG
  allowlist entry for 98, prev-payload persisted in UserDefaults (noop.592.prevPayload).
- LiveState: batteryMv + extendedBatteryProbe (+ disconnect resets).
- FrameRouter: consume battery_mV → state.batteryMv (event + command response).
- AppModel: probeExtendedBatteryInfo()/clearExtendedBatteryProbe() forwards.
- DevicesView: the probe menu item, a read-only confirm dialog, a copyable result
  sheet (selectable text + PlatformPasteboard Copy), and the voltage readout beside
  the battery percent. Design tokens only.

Verified on Linux: WhoopProtocol builds + all 294 tests pass (incl. the 5 new);
swiftc -parse clean on every app-target file. App-target compile (BLEManager /
DevicesView / etc.) needs an app-build run — flagged.

* iOS #592 probe: localize the Devices probe/voltage strings (de/es/fr)

Parity fix for the Android side (#603/#601 extracted these to strings.xml). The
8 new DevicesView UI strings — the probe menu item, confirm dialog title +
message, Send/waiting/result-title, and the voltage readout + a11y label — were
plain literals (English-only, and a i18n-gate failure). Extract them to
Localizable.xcstrings with de/es/fr; the voltage uses SwiftUI specifier
interpolation so the number localizes too.

i18n audit --ci exit 0.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…ld mapping (ryanbr#592) (ryanbr#604)

The 4.0 confirmed opcode 98 with a rich 29-byte payload, but the result was a
dense run-on and one frame can't map its fields responsibly. This reworks the
probe output:

- A pure, unit-tested `formatExtendedBatteryProbe` renders a clean report:
  a verdict header, the full raw hex on one copyable line, an offset-labelled
  payload hex grid (8 bytes/row), the decoded pack voltage, and a per-byte diff
  vs the previous capture.
- Field-mapping scaffold: persists each payload (SharedPreferences) and diffs
  the next one against it. A byte that MOVES between captures at different
  battery %/temperature is that field — SoC/capacity tracks charge, temperature
  drifts with wear, cycle count only ever climbs.
- ExtendedBatteryProbeFormatTest pins it against the REAL 4.0 capture (98
  accepted, mV=3970 → 3.97 V), the diff, a bare stub, and a 5/MG UNSUPPORTED
  verdict.

Read-only, additive; no new opcode, no automatic sends. Full module compiles;
testFullDebugUnitTest green; i18n audit --ci exit 0. Swift twin to follow.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…anbr#605)

* iOS/macOS twin of the ryanbr#592 extended-battery probe + voltage (ryanbr#592)

Swift port of the Android probe tooling (ryanbr#599/ryanbr#601/ryanbr#603/ryanbr#604): the read-only
GET_EXTENDED_BATTERY_INFO(98) probe, its copyable result dialog, the strap
voltage on the Devices card, and the field-mapping capture diff.

- WhoopProtocol: ExtendedBatteryProbe.format(...) — the pure formatter (verdict,
  full raw hex, offset-labelled payload grid, decoded voltage, per-byte capture
  diff). Placed in the package (not app-side like Kotlin) so swift test covers it
  on Linux/CI; behaviour is byte-identical to the Kotlin formatter. 5 tests pin it
  against the real 4.0 capture, the diff, a bare stub, and a 5/MG UNSUPPORTED
  verdict.
- BLEManager: probeExtendedBatteryInfo() (+ 8s no-reply timeout, guard-then-set is
  race-free since BLE callbacks + the timer both run on main), clearExtended...,
  the frame[6] (4.0) and frame[10] (5/MG) COMMAND_RESPONSE dumps, the 5/MG
  allowlist entry for 98, prev-payload persisted in UserDefaults (noop.592.prevPayload).
- LiveState: batteryMv + extendedBatteryProbe (+ disconnect resets).
- FrameRouter: consume battery_mV → state.batteryMv (event + command response).
- AppModel: probeExtendedBatteryInfo()/clearExtendedBatteryProbe() forwards.
- DevicesView: the probe menu item, a read-only confirm dialog, a copyable result
  sheet (selectable text + PlatformPasteboard Copy), and the voltage readout beside
  the battery percent. Design tokens only.

Verified on Linux: WhoopProtocol builds + all 294 tests pass (incl. the 5 new);
swiftc -parse clean on every app-target file. App-target compile (BLEManager /
DevicesView / etc.) needs an app-build run — flagged.

* iOS ryanbr#592 probe: localize the Devices probe/voltage strings (de/es/fr)

Parity fix for the Android side (ryanbr#603/ryanbr#601 extracted these to strings.xml). The
8 new DevicesView UI strings — the probe menu item, confirm dialog title +
message, Send/waiting/result-title, and the voltage readout + a11y label — were
plain literals (English-only, and a i18n-gate failure). Extract them to
Localizable.xcstrings with de/es/fr; the voltage uses SwiftUI specifier
interpolation so the number localizes too.

i18n audit --ci exit 0.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…ld mapping (ryanbr#592) (ryanbr#604)

The 4.0 confirmed opcode 98 with a rich 29-byte payload, but the result was a
dense run-on and one frame can't map its fields responsibly. This reworks the
probe output:

- A pure, unit-tested `formatExtendedBatteryProbe` renders a clean report:
  a verdict header, the full raw hex on one copyable line, an offset-labelled
  payload hex grid (8 bytes/row), the decoded pack voltage, and a per-byte diff
  vs the previous capture.
- Field-mapping scaffold: persists each payload (SharedPreferences) and diffs
  the next one against it. A byte that MOVES between captures at different
  battery %/temperature is that field — SoC/capacity tracks charge, temperature
  drifts with wear, cycle count only ever climbs.
- ExtendedBatteryProbeFormatTest pins it against the REAL 4.0 capture (98
  accepted, mV=3970 → 3.97 V), the diff, a bare stub, and a 5/MG UNSUPPORTED
  verdict.

Read-only, additive; no new opcode, no automatic sends. Full module compiles;
testFullDebugUnitTest green; i18n audit --ci exit 0. Swift twin to follow.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…anbr#605)

* iOS/macOS twin of the ryanbr#592 extended-battery probe + voltage (ryanbr#592)

Swift port of the Android probe tooling (ryanbr#599/ryanbr#601/ryanbr#603/ryanbr#604): the read-only
GET_EXTENDED_BATTERY_INFO(98) probe, its copyable result dialog, the strap
voltage on the Devices card, and the field-mapping capture diff.

- WhoopProtocol: ExtendedBatteryProbe.format(...) — the pure formatter (verdict,
  full raw hex, offset-labelled payload grid, decoded voltage, per-byte capture
  diff). Placed in the package (not app-side like Kotlin) so swift test covers it
  on Linux/CI; behaviour is byte-identical to the Kotlin formatter. 5 tests pin it
  against the real 4.0 capture, the diff, a bare stub, and a 5/MG UNSUPPORTED
  verdict.
- BLEManager: probeExtendedBatteryInfo() (+ 8s no-reply timeout, guard-then-set is
  race-free since BLE callbacks + the timer both run on main), clearExtended...,
  the frame[6] (4.0) and frame[10] (5/MG) COMMAND_RESPONSE dumps, the 5/MG
  allowlist entry for 98, prev-payload persisted in UserDefaults (noop.592.prevPayload).
- LiveState: batteryMv + extendedBatteryProbe (+ disconnect resets).
- FrameRouter: consume battery_mV → state.batteryMv (event + command response).
- AppModel: probeExtendedBatteryInfo()/clearExtendedBatteryProbe() forwards.
- DevicesView: the probe menu item, a read-only confirm dialog, a copyable result
  sheet (selectable text + PlatformPasteboard Copy), and the voltage readout beside
  the battery percent. Design tokens only.

Verified on Linux: WhoopProtocol builds + all 294 tests pass (incl. the 5 new);
swiftc -parse clean on every app-target file. App-target compile (BLEManager /
DevicesView / etc.) needs an app-build run — flagged.

* iOS ryanbr#592 probe: localize the Devices probe/voltage strings (de/es/fr)

Parity fix for the Android side (ryanbr#603/ryanbr#601 extracted these to strings.xml). The
8 new DevicesView UI strings — the probe menu item, confirm dialog title +
message, Send/waiting/result-title, and the voltage readout + a11y label — were
plain literals (English-only, and a i18n-gate failure). Extract them to
Localizable.xcstrings with de/es/fr; the voltage uses SwiftUI specifier
interpolation so the number localizes too.

i18n audit --ci exit 0.
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.

1 participant