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
History offload costs 41 min (5/MG) to 2.4 h (4.0) of radio per day; the official app does 24 h in 1–3 min #1007
NOOP spends roughly 2.4 hours of active BLE streaming per day pulling the strap's history. The official app moves the same 24 hours in 1–3 minutes. That is 48–144× more radio airtime for identical data, and it is very likely the dominant term in #1005's battery report.
The measurement
Already in docs/BLE_REVERSE_ENGINEERING.md, from real hardware:
The strap streams type-47 records at a steady ~10 records/s. The records are 1 Hz, so that is ~10× real-time — a full day of history takes ~40 min, a night ~30 min.
History is re-offloaded about every 15 min while connected, mirroring the official app's cadence.
The duty cycle follows from the data rate, not the sync frequency: moving 86,400 records a day at 10/s costs 8,640 s ≈ 2.4 h of radio per day, about a 10% duty cycle, no matter how often the sync runs.
Radio time per 24 h of history
Duty cycle
Official app
1–3 min
0.07–0.21%
NOOP
~2.4 h
~10%
Why this is not a tuning problem
Both obvious levers were tested on a real worn WHOOP 4 and neither moved the rate:
ATT MTU 23 → 247 (a 104-byte type-47 frame drops from 6 notification packets to 1) — no change.
Connection interval 50 ms → 7.5 ms (the Linux equivalent of CONNECTION_PRIORITY_HIGH) — no change.
The rate is rock-steady across the whole drain, which is the signature of firmware-side pacing of the per-record HISTORY stream. The existing doc already draws the conclusion: matching the official app "would require a different bulk / flash-page transfer command, not link tuning, and that command is not yet reverse-engineered."
So this is a protocol gap, not an optimisation. Until that command is found, every alternative — connection priority, MTU, sync cadence, batching — is a rounding error against 2.4 h/day of airtime.
What would close it
An HCI capture of the official app performing a full history sync is the artefact that would settle it. docs/WHOOP5_DEEP_DATA.md already asks for one for a related reason. What matters here is the command that initiates the bulk transfer and the framing of what comes back — a flash-page or block-range read rather than the per-record stream NOOP uses today.
Clean-room rules apply as everywhere else: a re-derived offset or command number with attribution, not decompiled implementation.
The ~10 rec/s figure was measured on a WHOOP 4.0. The 5/MG offload is ack-gated and may pace differently; worth confirming separately before quoting the 10% figure at a 5/MG user.
Correcting the headline figure, and adding the avenues that are actually reachable.
The 5/MG number was 3.5x too pessimistic
I quoted the WHOOP 4 rate (~10 records/s) as the general case. BLE_REVERSE_ENGINEERING.md also carries a hardware-verified WHOOP 5 measurement I did not use: "One 90 s capture pulled 3193 CRC-valid type-47 frames" — 35.5 records/s.
Radio time per day of history
Duty cycle
WHOOP 4
144 min
10%
WHOOP 5
41 min
2.8%
Official app
1–3 min
0.07–0.21%
Still 15–40x the official app, so the issue stands. But 41 minutes a day is a different claim from 2.4 hours, and #1005's reporter is on a 5.0.
The measurement that should come before any fix
That 35.5 rec/s came from whoop_capture.py, which acks almost immediately. The app does not. It persists the decoded rows, then the trim cursor, and only then acks — and the strap does not advance until acked, so per-chunk latency caps throughput directly.
From the same capture: ~11.4 records per chunk, a chunk every ~320 ms.
Nobody has compared the app's offload rate against the tool's. That single number decides what this issue even is:
App ≈ 35 rec/s → we are firmware-limited, and only a bulk transfer command helps.
App materially slower → we are limiting ourselves, and that part is fixable today with no protocol work.
It is measurable from a strap log on any device, and it should come first.
One concrete inefficiency, honestly sized
PrefsTrimCursorStore.set does prefs.edit().putLong(...).commit() — a synchronous whole-file XML rewrite plus fsync, per chunk, inside the ack path. The comment is right that durability must precede the ack, so .apply() would be wrong.
But the rows already land in a Room transaction. Folding the cursor into that transaction gives the same ordering with one durable write instead of two. At ~3 chunks/s this is worth single-digit percent, not a fix for this issue — and the Swift twin has the same shape via store.setCursor on UserDefaults, so it would be a both-platforms change.
Avenues toward the real fix
An HCI capture of the official app syncing a full night's history. The definitive artefact, and WHOOP5_DEEP_DATA.md item 4 already asks for one for a related reason. What matters is the command that initiates the bulk transfer and the framing of what comes back.
Confirm the plain path's ceiling first.goose #24 shows a Gen5 band streaming type-47 history to a third-party app with no config write at all. Worth establishing what a clocked 5/MG already returns through the get_data_range/send_historical_data loop NOOP runs today, before assuming a write path is needed.
I have deliberately not searched the command table and called a candidate likely: nothing in the 80 documented commands reads as a bulk or flash-page transfer, which is consistent with the doc's conclusion that it is simply not reverse-engineered yet.
What will not work
Link tuning. Already tested on real hardware and disproved: ATT MTU 23 → 247 and connection interval 50 ms → 7.5 ms both left the rate unmoved. The pacing is firmware-side.
changed the title [-]History offload costs ~2.4 h of radio per day; the official app does 24 h in 1–3 min (bulk transfer command not yet found)[/-][+]History offload costs 41 min (5/MG) to 2.4 h (4.0) of radio per day; the official app does 24 h in 1–3 min[/+]on Aug 1, 2026
Independent 5/MG numbers from a ~29 h backlog drained today, plus two client-side costs that I think complicate the "purely firmware-paced" conclusion.
Your 35.5 rec/s figure reproduces — but only at the top of the range
I measured throughput as history-time covered per wall-second, decoding the per-record timestamp out of the frames themselves. Since the records are 1 Hz, N× real-time == N records/s and the units are directly comparable to yours.
window
rate
first 23 min
~36× real-time
middle stretch
~7.5×
after restarts stopped
~16×
The opening ~36× lands essentially on your hardware-verified 35.5 rec/s, which I take as independent confirmation of the ceiling on a 5/MG.
But it is not rock-steady. Same strap, same connection, one continuous drain — 4.8× spread. Firmware pacing should not vary like that, and the variation tracks something visible in the log: sessions ending reason=timeout and immediately auto-continuing. The slow stretch had three session boundaries in a 2000-line tail; the fast stretch had zero and one long-running session (sessionRows 31,004 and climbing).
So I would split the gap in two. 35 rec/s → 1-3 min is the protocol gap you describe.7.5 rec/s → 35 rec/s is ours, and it is reachable without finding any new command.
Client-side cost 1: the strap log re-serializes ~4 MB per line
LiveState.append(log:) calls persistTail(log) on every appended line, and persistTail does Array(lines.suffix(2000)) + UserDefaults.standard.set(...). Its comment says "Synchronous and cheap (a single small array write)… the persisted blob stays a few hundred KB at most."
That assumption breaks whenever a strap emits records that fail to decode. Backfiller hex-dumps up to 8 full frames per chunk (rejected.prefix(8)). My frames are 1584 B, so each dump is 3,168 characters, and 1,307 of my 2,000 tail lines were hex — a ~4 MB blob, rebuilt and re-serialized on every line, ~10 lines per 2-second chunk. redactPii() also runs a regex over each 3,168-char string.
Not a regression — #91 landed in v1.69, #30/#136 in v1.92 — and it is dormant for any strap whose records decode. Which is probably why it has not surfaced before. Cheap fixes: debounce persistTail, cap the persisted line length, and skip the hex dump when the payload is all zeros.
Client-side cost 2: half my frames carried no payload
Of every 52 frames per chunk, exactly 26 were rejected, and all 1,307 I captured were byte-identically shaped:
size 1584 B (all 1307)
nonzero fraction 0.015 (mean = min = max)
nonzero offsets 0-20 (header: magic aa, seq, LE unix ts, +1s per frame)
1580-1583 (CRC32)
offsets 21-1579 ZERO in every frame
Structurally valid, correctly timestamped, and empty — 1,559 zero bytes each. If that generalises, half the airtime is carrying nothing.
Strong caveat: this may be self-inflicted and specific to my strap. These records begin at a datable instant — the raw-archive table holds 499,624 rows starting 2026-07-27T17:06:58Z at 0.97/s, exactly one per second, and is empty before that. That is the same day I wrote config flags to the strap while probing #103/#891. So the likeliest reading is that a flag I set causes the strap to allocate a per-second buffer nothing fills. I have not yet cleared the flag to confirm, and I would not want this counted toward the general figure until someone reproduces it on a strap that has never been written to.
Worth checking whether anyone else drains with ~50% rejects. If so it is not my flag, and it is half the offload budget.
Method note
The per-frame timestamp gives an exact progress figure without needing GET_DATA_RANGE — useful, because on my strap that reply was first absent and then returned an epoch-era value:
recovered through Aug 3 01:38:22
window Aug 2 11:43 -> Aug 3 15:56 (28.2 h)
progress 49.3% (13.9 h done, 14.3 h left)
Happy to capture more on the 5/MG if a specific measurement would help.
Correcting my own comment above: the rate variation is real, but my explanation for it does not hold up. Please do not act on the session-restart attribution.
The measurements that stand are the absolute ones — frontier timestamps decoded from the frames, which are independent of anything else going on:
peak ~36x real-time, matching your hardware-verified 35.5 rec/s
lower sustained rates later in the same drain
What does not stand is my claim that the slowdown tracksreason=timeout session restarts. Four confounds I should have accounted for:
I was reading the app container repeatedly to take the measurements — roughly every 10-15 minutes across the drain. That read takes a usage assertion on the app and is documented in this project as able to relaunch or perturb it. My instrument was plausibly perturbing the thing it measured, during the window I then called slow.
The app was force-quit and reopened mid-drain, with two manual syncs, as part of recovering a wedged strap.
Thermal state unknown. Phone on cable, sustained BLE + CPU for about an hour. I have no data either way.
The session-boundary count itself is biased. I counted boundaries in a fixed-length 2000-line rolling log tail. Those hex dumps consume the tail budget, so a faster drain emits more lines and the window covers less wall-time. "Zero session boundaries during the fast stretch" may be the tail having rolled past them, not fewer restarts. The correlation may be an artifact of the instrument.
So my "35 -> 1-3 min is the protocol gap, 7.5 -> 35 is ours" split is not evidenced. The ceiling reproduces; whether the shortfall beneath it is client-side is open.
The two code-level findings are unaffected, since neither depends on that timing data: persistTail re-serializing the tail on every appended line is a plain reading of LiveState.append(log:), and the 1584 B all-zero frame structure is a byte census over 1,307 captured frames.
A clean throughput measurement would need no container reads during the drain, a known thermal state, and no app restarts — I can try to produce one on the 5/MG if it is useful.
A 5/MG data point for the "confirm separately" note
The issue asks for the 5/MG pacing to be confirmed before quoting the ~10% duty cycle at a 5/MG user. Here is one measurement from a WHOOP 5.0, firmware 50.36.2.0, never paired with the official app (Bluetooth-only, computed namespace).
Method — and its limits. This is not a record-rate counter. It is inferred at the app-log level: how far our own frontier advanced, divided by the wall-clock duration of the offload session that delivered it. Records are taken as 1 Hz, as in the issue. One connection, one sample.
The window. The strap reconnected at 21:28:32 after ~10 minutes disconnected, so this is a genuine backlog drain rather than a steady-state trickle:
Result. 595 s of history delivered in ~6 s of session, i.e. ~99 records/s, ~99x real-time — roughly 10x the ~10 rec/s measured on the 4.0.
Extrapolated the same way the issue does, 86,400 records/day at ~99 rec/s is ~15 min of radio per day, ~1% duty cycle, versus the ~2.4 h / ~10% quoted for the 4.0. So the 10% figure does look like it should not be quoted at a 5/MG user, though the gap to the official app's 1–3 min remains large.
Caveats. Single sample, single connection, one firmware. The frontier-based method is coarser than counting notifications, and a longer backlog might pace differently once the ack-gating has more to chew on. Happy to repeat it over a multi-day backlog if that would help, or to hand over the raw strap log.
Separately, this session's chain stopped at 3/24 auto-continues because the strap reported HISTORY_COMPLETE and the trim stopped advancing — so on 5/MG the per-connection cap does not appear to be the binding constraint either; the gap between triggers is.
Correcting my own comment: retract the ~99x figure
I posted a 5/MG measurement above without having read this thread's existing comments first. @vishk23 had already posted 5/MG numbers on 3 August, with a better-instrumented method, and my figure disagrees with theirs by roughly 3x. Theirs is the one to trust; please disregard my ~99x and the ~1% duty cycle derived from it.
Why mine is wrong. We were computing the same quantity — history-time covered per wall-second — but the denominators are not comparable. @vishk23 decodes the per-record timestamp out of the frames. I took two log lines:
and called that 6 s. That denominator has two defects I should have stated before quoting a number off it:
It may not bracket the transfer. The link connected at 21:28:32, two seconds before the "session started" line, and I have no evidence the last frame lands before the "session ended" line rather than after. If the true window is ~16 s rather than 6 s, the result is ~37x — which is @vishk23's peak, and consistent with the hardware-verified 35.5 rec/s.
One-second log resolution on a 6-second measurement is a ±17% error bar before any of the above.
Separately, my numerator assumed the 595 s of frontier advance corresponds to 595 banked records. That holds only if the strap banked continuously at 1 Hz across the whole window, which I did not verify.
What I still think stands, since none of it depends on the rate:
the device context: WHOOP 5.0, firmware 50.36.2.0, never paired with the official app;
the chain ended at 3/24 auto-continues via HISTORY_COMPLETE with the trim no longer advancing, so on this unit the per-connection cap was not the binding constraint;
the window followed a reconnect, so it was a real backlog drain rather than a steady-state trickle.
Apologies for the noise. I still have the raw strap log for this session and can hand it over if a second data point at frame level would be useful — but next time I will instrument it the way @vishk23 did instead of inferring it from app logs.
Landed a small log-hygiene slice from this thread: #1341 (merged cacdf242).
@vishk23's byte census found a strap emitting byte-identically empty rejected frames (valid header + CRC, all-zero payload) — ~4 MB of all-00 in the strap log, since the Backfiller hex-dumps up to 8 full rejected frames/chunk for firmware-mapping. An all-zero frame has no record layout to map, so #1341 adds a pure isEmptyRecordFrame (both platforms) and skips those frames' hex dump, keeping the WARNING … decoded to 0 rows count plus a compact N/M all-zero, hex skipped line so the occurrence is still visible.
Scope — this does not close the issue. It's log/CPU hygiene only; it does not reduce the offload radio airtime (the strap still sends the frames). The core cost here is still the protocol gap — the per-record type-47 stream vs the official app's bulk transfer — which needs the un-RE'd bulk/flash-page command, and the definitive next artefact remains an HCI capture of the official app doing a full history sync.
For the record on the other client-side items in the thread: the persistTail re-serialization is already fixed on main (batched via persistEveryNLines), and the trim-cursor fold into the Room transaction needs a schema migration, so it's a separate change rather than a quick win. Leaving this open as the protocol-gap tracker.
Activity
ryanbr commentedon Aug 1, 2026
Correcting the headline figure, and adding the avenues that are actually reachable.
The 5/MG number was 3.5x too pessimistic
I quoted the WHOOP 4 rate (~10 records/s) as the general case.
BLE_REVERSE_ENGINEERING.mdalso carries a hardware-verified WHOOP 5 measurement I did not use: "One 90 s capture pulled 3193 CRC-valid type-47 frames" — 35.5 records/s.Still 15–40x the official app, so the issue stands. But 41 minutes a day is a different claim from 2.4 hours, and #1005's reporter is on a 5.0.
The measurement that should come before any fix
That 35.5 rec/s came from
whoop_capture.py, which acks almost immediately. The app does not. It persists the decoded rows, then the trim cursor, and only then acks — and the strap does not advance until acked, so per-chunk latency caps throughput directly.From the same capture: ~11.4 records per chunk, a chunk every ~320 ms.
Nobody has compared the app's offload rate against the tool's. That single number decides what this issue even is:
It is measurable from a strap log on any device, and it should come first.
One concrete inefficiency, honestly sized
PrefsTrimCursorStore.setdoesprefs.edit().putLong(...).commit()— a synchronous whole-file XML rewrite plus fsync, per chunk, inside the ack path. The comment is right that durability must precede the ack, so.apply()would be wrong.But the rows already land in a Room transaction. Folding the cursor into that transaction gives the same ordering with one durable write instead of two. At ~3 chunks/s this is worth single-digit percent, not a fix for this issue — and the Swift twin has the same shape via
store.setCursoron UserDefaults, so it would be a both-platforms change.Avenues toward the real fix
WHOOP5_DEEP_DATA.mditem 4 already asks for one for a related reason. What matters is the command that initiates the bulk transfer and the framing of what comes back.get_data_range/send_historical_dataloop NOOP runs today, before assuming a write path is needed.I have deliberately not searched the command table and called a candidate likely: nothing in the 80 documented commands reads as a bulk or flash-page transfer, which is consistent with the doc's conclusion that it is simply not reverse-engineered yet.
What will not work
Link tuning. Already tested on real hardware and disproved: ATT MTU 23 → 247 and connection interval 50 ms → 7.5 ms both left the rate unmoved. The pacing is firmware-side.
[-]History offload costs ~2.4 h of radio per day; the official app does 24 h in 1–3 min (bulk transfer command not yet found)[/-][+]History offload costs 41 min (5/MG) to 2.4 h (4.0) of radio per day; the official app does 24 h in 1–3 min[/+]Also report a burst the disconnect cut short (#1007)
Correct the backfillStartedAtMs doc (#1007)
Report what each offload burst actually achieved (#1007) (#1027)
vishk23 commentedon Aug 3, 2026
Independent 5/MG numbers from a ~29 h backlog drained today, plus two client-side costs that I think complicate the "purely firmware-paced" conclusion.
Your 35.5 rec/s figure reproduces — but only at the top of the range
I measured throughput as history-time covered per wall-second, decoding the per-record timestamp out of the frames themselves. Since the records are 1 Hz, N× real-time == N records/s and the units are directly comparable to yours.
The opening ~36× lands essentially on your hardware-verified 35.5 rec/s, which I take as independent confirmation of the ceiling on a 5/MG.
But it is not rock-steady. Same strap, same connection, one continuous drain — 4.8× spread. Firmware pacing should not vary like that, and the variation tracks something visible in the log: sessions ending
reason=timeoutand immediately auto-continuing. The slow stretch had three session boundaries in a 2000-line tail; the fast stretch had zero and one long-running session (sessionRows31,004 and climbing).So I would split the gap in two. 35 rec/s → 1-3 min is the protocol gap you describe. 7.5 rec/s → 35 rec/s is ours, and it is reachable without finding any new command.
Client-side cost 1: the strap log re-serializes ~4 MB per line
LiveState.append(log:)callspersistTail(log)on every appended line, andpersistTaildoesArray(lines.suffix(2000))+UserDefaults.standard.set(...). Its comment says "Synchronous and cheap (a single small array write)… the persisted blob stays a few hundred KB at most."That assumption breaks whenever a strap emits records that fail to decode.
Backfillerhex-dumps up to 8 full frames per chunk (rejected.prefix(8)). My frames are 1584 B, so each dump is 3,168 characters, and 1,307 of my 2,000 tail lines were hex — a ~4 MB blob, rebuilt and re-serialized on every line, ~10 lines per 2-second chunk.redactPii()also runs a regex over each 3,168-char string.Not a regression —
#91landed in v1.69,#30/#136in v1.92 — and it is dormant for any strap whose records decode. Which is probably why it has not surfaced before. Cheap fixes: debouncepersistTail, cap the persisted line length, and skip the hex dump when the payload is all zeros.Client-side cost 2: half my frames carried no payload
Of every 52 frames per chunk, exactly 26 were rejected, and all 1,307 I captured were byte-identically shaped:
Structurally valid, correctly timestamped, and empty — 1,559 zero bytes each. If that generalises, half the airtime is carrying nothing.
Strong caveat: this may be self-inflicted and specific to my strap. These records begin at a datable instant — the raw-archive table holds 499,624 rows starting 2026-07-27T17:06:58Z at 0.97/s, exactly one per second, and is empty before that. That is the same day I wrote config flags to the strap while probing #103/#891. So the likeliest reading is that a flag I set causes the strap to allocate a per-second buffer nothing fills. I have not yet cleared the flag to confirm, and I would not want this counted toward the general figure until someone reproduces it on a strap that has never been written to.
Worth checking whether anyone else drains with ~50% rejects. If so it is not my flag, and it is half the offload budget.
Method note
The per-frame timestamp gives an exact progress figure without needing
GET_DATA_RANGE— useful, because on my strap that reply was first absent and then returned an epoch-era value:Happy to capture more on the 5/MG if a specific measurement would help.
vishk23 commentedon Aug 3, 2026
Correcting my own comment above: the rate variation is real, but my explanation for it does not hold up. Please do not act on the session-restart attribution.
The measurements that stand are the absolute ones — frontier timestamps decoded from the frames, which are independent of anything else going on:
What does not stand is my claim that the slowdown tracks
reason=timeoutsession restarts. Four confounds I should have accounted for:So my "35 -> 1-3 min is the protocol gap, 7.5 -> 35 is ours" split is not evidenced. The ceiling reproduces; whether the shortfall beneath it is client-side is open.
The two code-level findings are unaffected, since neither depends on that timing data:
persistTailre-serializing the tail on every appended line is a plain reading ofLiveState.append(log:), and the 1584 B all-zero frame structure is a byte census over 1,307 captured frames.A clean throughput measurement would need no container reads during the drain, a known thermal state, and no app restarts — I can try to produce one on the 5/MG if it is useful.
sempowa-hub commentedon Aug 8, 2026
A 5/MG data point for the "confirm separately" note
The issue asks for the 5/MG pacing to be confirmed before quoting the ~10% duty cycle at a 5/MG user. Here is one measurement from a WHOOP 5.0, firmware 50.36.2.0, never paired with the official app (Bluetooth-only, computed namespace).
Method — and its limits. This is not a record-rate counter. It is inferred at the app-log level: how far our own frontier advanced, divided by the wall-clock duration of the offload session that delivered it. Records are taken as 1 Hz, as in the issue. One connection, one sample.
The window. The strap reconnected at
21:28:32after ~10 minutes disconnected, so this is a genuine backlog drain rather than a steady-state trickle:Result. 595 s of history delivered in ~6 s of session, i.e. ~99 records/s, ~99x real-time — roughly 10x the ~10 rec/s measured on the 4.0.
Extrapolated the same way the issue does, 86,400 records/day at ~99 rec/s is ~15 min of radio per day, ~1% duty cycle, versus the ~2.4 h / ~10% quoted for the 4.0. So the 10% figure does look like it should not be quoted at a 5/MG user, though the gap to the official app's 1–3 min remains large.
Caveats. Single sample, single connection, one firmware. The frontier-based method is coarser than counting notifications, and a longer backlog might pace differently once the ack-gating has more to chew on. Happy to repeat it over a multi-day backlog if that would help, or to hand over the raw strap log.
Separately, this session's chain stopped at
3/24auto-continues because the strap reportedHISTORY_COMPLETEand the trim stopped advancing — so on 5/MG the per-connection cap does not appear to be the binding constraint either; the gap between triggers is.sempowa-hub commentedon Aug 9, 2026
Correcting my own comment: retract the ~99x figure
I posted a 5/MG measurement above without having read this thread's existing comments first. @vishk23 had already posted 5/MG numbers on 3 August, with a better-instrumented method, and my figure disagrees with theirs by roughly 3x. Theirs is the one to trust; please disregard my ~99x and the ~1% duty cycle derived from it.
Why mine is wrong. We were computing the same quantity — history-time covered per wall-second — but the denominators are not comparable. @vishk23 decodes the per-record timestamp out of the frames. I took two log lines:
and called that 6 s. That denominator has two defects I should have stated before quoting a number off it:
21:28:32, two seconds before the "session started" line, and I have no evidence the last frame lands before the "session ended" line rather than after. If the true window is ~16 s rather than 6 s, the result is ~37x — which is @vishk23's peak, and consistent with the hardware-verified 35.5 rec/s.Separately, my numerator assumed the 595 s of frontier advance corresponds to 595 banked records. That holds only if the strap banked continuously at 1 Hz across the whole window, which I did not verify.
What I still think stands, since none of it depends on the rate:
HISTORY_COMPLETEwith the trim no longer advancing, so on this unit the per-connection cap was not the binding constraint;Apologies for the noise. I still have the raw strap log for this session and can hand it over if a second data point at frame level would be useful — but next time I will instrument it the way @vishk23 did instead of inferring it from app logs.
ryanbr commentedon Aug 15, 2026
Landed a small log-hygiene slice from this thread: #1341 (merged
cacdf242).@vishk23's byte census found a strap emitting byte-identically empty rejected frames (valid header + CRC, all-zero payload) — ~4 MB of all-
00in the strap log, since the Backfiller hex-dumps up to 8 full rejected frames/chunk for firmware-mapping. An all-zero frame has no record layout to map, so #1341 adds a pureisEmptyRecordFrame(both platforms) and skips those frames' hex dump, keeping theWARNING … decoded to 0 rowscount plus a compactN/M all-zero, hex skippedline so the occurrence is still visible.Scope — this does not close the issue. It's log/CPU hygiene only; it does not reduce the offload radio airtime (the strap still sends the frames). The core cost here is still the protocol gap — the per-record type-47 stream vs the official app's bulk transfer — which needs the un-RE'd bulk/flash-page command, and the definitive next artefact remains an HCI capture of the official app doing a full history sync.
For the record on the other client-side items in the thread: the
persistTailre-serialization is already fixed onmain(batched viapersistEveryNLines), and the trim-cursor fold into the Room transaction needs a schema migration, so it's a separate change rather than a quick win. Leaving this open as the protocol-gap tracker.perf(offload): skip the hex dump of all-zero (empty) rejected frames (r…
Report what each offload burst actually achieved (ryanbr#1007) (ryanb…
perf(offload): skip the hex dump of all-zero (empty) rejected frames (r…