Skip to content

Read the meter once and replay the capture offline - #57

Merged
InboundFrog merged 2 commits into
mainfrom
owner/binary-capture-replay
Sep 25, 2026
Merged

InboundFrog merged 2 commits into
mainfrom
owner/binary-capture-replay

Conversation

@InboundFrog

@InboundFrog InboundFrog commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #56 — review that one first; this PR's base is owner/reduce-device-re-reads, so the diff here is only the follow-up work.

Closes the TODO #56 left in bin/capture-bgl, and picks up the two smaller review findings from it.

The problem

Every run that talks to the meter is a full transfer of up to 800 records. #56 got the routine capture from four transfers down to two by deriving csv/json from a saved records dump, but bytes still needed its own read — and the data was already in hand: fetch_all returns readings, raw records and raw packets from a single session. Only output::write could emit just one of them.

The change

  • --format binary writes the raw HID packet stream: magic + version + packet size, then a direction byte per packet at a fixed stride.
  • --from-bytes FILE replays that capture through the same framing and record parsing the live read uses.

Packet reassembly and record accumulation moved into MessageAssembler and SessionBuilder, shared by both paths, so a replayed capture cannot drift from a live one. That sharing is the point — a second, parallel decoder would have been the bug waiting to happen.

Bad input fails loudly rather than decoding into a plausible but short reading list: wrong magic, wrong version, wrong packet size, a bad direction byte and a truncated tail are each rejected with their own message.

bin/capture-bgl now reads the meter once and derives all four other files from the capture. It also stages the run in a temp dir and moves it into place at the end, so a failure part-way through no longer leaves fresh files next to stale ones from an earlier run.

Separately: --from-records no longer discards unparseable lines in silence. It warns per line and with a total, because a truncated dump otherwise yields a short CSV that looks perfectly valid.

README documents --from-bytes, --from-records (previously undocumented) and the binary format, and notes that the binary capture carries the same meter password and serial the records dump does.

Testing

cargo test — 56 pass, cargo clippy --all-targets and cargo fmt --check clean.

Six new tests, the load-bearing one being replay_matches_text_parsing: it synthesises a capture from the existing fixture and asserts the replayed session matches parse_records_from_text on the same fixture — device, readings and raw frames. The fixture's H record is longer than one HID packet, so multi-packet reassembly is covered on the way through. Others cover the encode/decode round trip, byte-identical re-encoding after a file round trip, rejection of damaged and foreign files, a NAK-retried frame appearing only once, and an empty capture erroring rather than producing an empty session.

CLI paths were exercised by hand for the error cases (empty capture, hex dump passed to --from-bytes, junk line in a records file).

Verified against a physical meter

A Contour Next One was captured twice on the same day with no new readings in between — once on owner/reduce-device-re-reads, once on this branch — and the outputs compared.

File Difference
.csv byte-identical
.json one field, device_time
.txt one line, the H record

The H record carries two values that are new every session: the meter's per-session password nonce and the capture timestamp. Everything else matches.

The .hex needs explaining, because a naive diff reports ~3300 changed lines. 3300 of them are packet header lines (--- packet NNNN RX ---): the old capture has two extra packets at the front, so every later packet is renumbered and diff cannot resync. Normalise the numbering and the real diff is 18 lines:

diff <(sed 's/packet [0-9]*/packet/' old.hex) <(sed 's/packet [0-9]*/packet/' new.hex)

Those 18 lines are: two packets present only in the old capture (RX 04 05 = EOT+ENQ, then TX 06 = ACK), and the two H-record packets holding the password nonce, the timestamp and its ASTM checksum.

Parsing both dumps back into packets and aligning past that two-packet offset gives 0 direction mismatches and 1649 of 1651 packets byte-identical across two independent physical sessions — the only two that differ being the H-record packets above.

The two extra packets are the point rather than a discrepancy. decode_message already documents the meter packing EOT+ENQ when a session opens on the heels of a previous one; that is exactly what the old capture shows, because its hex dump came from a second device read moments after the records read. This branch's capture is a first session and goes straight to the H record.

That second read also meant the old .hex documented a different session from the old .txt — different password nonce, 32 seconds apart — so the debug dump never described the data actually shipped alongside it. Both now come from the one session.

Finally, replaying the captured .bin back to .hex reproduces the stored .hex byte for byte, so the conversion is deterministic.

🤖 Generated with Claude Code

Every run that talked to the meter did a full transfer of up to 800
records, so producing csv, json, records and bytes meant four transfers.
fetch_all already returns readings, raw records and raw packets from a
single session — only output::write could emit just one of them.

Add --format binary, which writes the raw HID packet stream, and
--from-bytes, which replays that file through the same framing and record
parsing the live read uses. Packet reassembly and record accumulation are
now shared (MessageAssembler, SessionBuilder) instead of duplicated, so a
replayed capture cannot drift from a live one; a test asserts the replayed
session matches what parsing the records text produces, and the fixture's
H record spans several packets so reassembly is covered too.

The capture format is magic + version + packet size, then a direction byte
per packet at a fixed stride, so a truncated or foreign file is rejected
outright rather than decoding into a plausible but short reading list.

capture-bgl now touches the meter once and derives the other four files
from that capture. It also stages the run in a temp dir and moves it into
place at the end, so a failure part-way through no longer leaves fresh
files sitting next to stale ones.

Also stop --from-records discarding unparseable lines in silence: it warns
per line and with a total, since a truncated dump would otherwise yield a
short CSV that looks perfectly valid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from owner/reduce-device-re-reads to main September 25, 2026 08:25
@InboundFrog
InboundFrog merged commit c3ce59a into main Sep 25, 2026
2 checks passed
@InboundFrog
InboundFrog deleted the owner/binary-capture-replay branch September 25, 2026 12:39
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