Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,53 @@ bgl-read --format csv --output readings.csv

# List connected Contour devices
bgl-read --list

# Capture the raw packet stream, then convert it without the meter
bgl-read --format binary --progress --output session.bin
bgl-read --from-bytes session.bin --format csv --output readings.csv
```

### Reading the meter only once

Every run that talks to the meter is a full transfer of up to 800 records, so
producing four formats used to mean four transfers. `--format binary` writes
the raw HID packet stream to a file, and `--from-bytes` replays that file
through the same framing and parsing code the live read uses — so a single
transfer can produce every format, the `bytes` hex dump included.

```sh
bgl-read --format binary --progress --output session.bin
bgl-read --from-bytes session.bin --format records --output session.txt
bgl-read --from-bytes session.bin --format csv --output session.csv
bgl-read --from-bytes session.bin --format json --output session.json
bgl-read --from-bytes session.bin --format bytes --output session.hex
```

`bin/capture-bgl <prefix>` does exactly this and leaves the files in
`captures/<date>/`.

`--from-records FILE` is the narrower version: it re-parses a saved `records`
dump, which is enough for `csv` and `json` but cannot reproduce `bytes` or
`binary`, because the text dump does not carry the HID traffic. Both replay
flags are offline-only, so neither accepts `--progress`.

Damaged input is reported rather than quietly producing a short reading list:
`--from-bytes` rejects a truncated or foreign file outright, and
`--from-records` warns on stderr with a count of the lines it could not parse.

### Output formats

Formats are chosen with `--format`, and `--output FILE` writes to a file
instead of stdout. `binary` refuses to write to a terminal — give it `--output`
or pipe it somewhere.

| `--format` | Description |
|------------|------------------------------------------------------------------------|
| `json` | Device info and readings as pretty-printed JSON |
| `csv` | One reading per row, header included |
| `records` | Raw ASTM record text as received from the meter (useful for debugging) |
| `bytes` | Hex dump of every HID packet exchanged (TX and RX) |
| `binary` | Compact binary packet capture, replayable with `--from-bytes` |

Timestamps are in the meter's own local time (no timezone attached — the device
has no concept of timezone).
Expand Down Expand Up @@ -97,5 +134,6 @@ connected.
annotation field and excluded from output.
- `--format records` output is the **raw meter transcript**: the header (`H`)
line includes the meter's **password** and serial number, and the dump
contains every reading. Redact these before sharing dumps publicly (e.g. when
contains every reading. The `bytes` and `binary` captures hold that same
transcript verbatim. Redact these before sharing dumps publicly (e.g. when
filing issues).
29 changes: 19 additions & 10 deletions bin/capture-bgl
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ PRJ_DIR="$(cd "${BIN_DIR}/../" > /dev/null 2>&1 && pwd)"
CAP_DIR="${PRJ_DIR}/captures"
DATE_CAP=$(date "+%Y%m%d")
PREFIX="${1:-unknown}"
OUT_DIR="${CAP_DIR}/${DATE_CAP}"

# Ensure the captures directory exists
mkdir -p "${CAP_DIR}/${DATE_CAP}"
mkdir -p "${OUT_DIR}"

# Read records first, to reduce device calls
cargo run -- --format records --progress --output "${CAP_DIR}/${DATE_CAP}/${PREFIX}-bgl.txt"
# Generate CSV and JSON from the records capture
cargo run -- --from-records "${CAP_DIR}/${DATE_CAP}/${PREFIX}-bgl.txt" --format csv --output "${CAP_DIR}/${DATE_CAP}/${PREFIX}-bgl.csv"
cargo run -- --from-records "${CAP_DIR}/${DATE_CAP}/${PREFIX}-bgl.txt" --format json --output "${CAP_DIR}/${DATE_CAP}/${PREFIX}-bgl.json"
# Finally, re-capture to get the bytes
# TODO: Need to fix this so that a proper binary is generated and that can generate
# the records file, which can in turn generate the other formats?
cargo run -- --format bytes --progress --output "${CAP_DIR}/${DATE_CAP}/${PREFIX}-bgl.hex"
# Stage the run in a sibling directory on the same filesystem, so publishing it
# at the end is a rename. A failure part-way through then leaves the previous
# capture untouched instead of mixing fresh files with stale ones.
WORK_DIR="$(mktemp -d "${OUT_DIR}/.${PREFIX}-XXXXXX")"
trap 'rm -rf "${WORK_DIR}"' EXIT
STEM="${WORK_DIR}/${PREFIX}-bgl"

# The only time we touch the meter: capture the raw HID packet stream once.
cargo run -- --format binary --progress --output "${STEM}.bin"

# Every other format is replayed from that capture, offline.
cargo run -- --from-bytes "${STEM}.bin" --format records --output "${STEM}.txt"
cargo run -- --from-bytes "${STEM}.bin" --format csv --output "${STEM}.csv"
cargo run -- --from-bytes "${STEM}.bin" --format json --output "${STEM}.json"
cargo run -- --from-bytes "${STEM}.bin" --format bytes --output "${STEM}.hex"

mv "${WORK_DIR}"/* "${OUT_DIR}/"
37 changes: 33 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod protocol;
use anyhow::Result;
use clap::Parser;
use output::Format;
use std::io::{self, IsTerminal};
use std::path::PathBuf;

#[derive(Parser, Debug)]
Expand All @@ -30,6 +31,11 @@ struct Cli {
/// Parse a saved `--format records` file instead of reading from a meter
#[arg(long, value_name = "FILE", conflicts_with_all = ["list", "progress"])]
from_records: Option<PathBuf>,

/// Replay a saved `--format binary` capture instead of reading from a meter.
/// Unlike --from-records this reproduces every format, bytes included.
#[arg(long, value_name = "FILE", conflicts_with_all = ["list", "progress", "from_records"])]
from_bytes: Option<PathBuf>,
}

fn main() -> Result<()> {
Expand All @@ -41,20 +47,37 @@ fn main() -> Result<()> {
return Ok(());
}

// Binary captures are unreadable noise on a terminal, but piping them
// onward (e.g. into xxd) is legitimate — only block the former.
if matches!(cli.format, Format::Binary) && cli.output.is_none() && io::stdout().is_terminal() {
anyhow::bail!("--format binary writes raw bytes; use --output FILE or pipe it somewhere");
}

if let Some(path) = cli.from_bytes.as_deref() {
let packets = protocol::decode_packets(&std::fs::read(path)?)?;
let session = protocol::session_from_packets(packets)?;
return output::write(&session, cli.format, cli.output.as_deref());
}

if let Some(path) = cli.from_records.as_deref() {
let text = std::fs::read_to_string(path)?;
let session = protocol::session_from_records_text(&text);
if matches!(cli.format, Format::Bytes) {
if matches!(cli.format, Format::Bytes | Format::Binary) {
eprintln!(
"warning: --format bytes has no data when reading from a records file; output will be empty"
"warning: --format {} has no data when reading from a records file; output will be empty",
if matches!(cli.format, Format::Bytes) {
"bytes"
} else {
"binary"
}
);
}
return output::write(&session, cli.format, cli.output.as_deref());
}

let api = hidapi::HidApi::new()?;
let device = protocol::open_device(&api)?;
let capture = matches!(cli.format, Format::Bytes);
let capture = matches!(cli.format, Format::Bytes | Format::Binary);
let session = match protocol::fetch_all(&device, cli.progress, capture) {
Ok(session) => session,
Err(e) => {
Expand All @@ -71,7 +94,13 @@ fn main() -> Result<()> {
raw_records: Vec::new(),
raw_packets: e.packets,
};
output::write(&partial, Format::Bytes, cli.output.as_deref())?;
// Dump in whichever capture format was asked for, so a
// `--format binary` failure still leaves a replayable file.
let dump = match cli.format {
Format::Binary => Format::Binary,
_ => Format::Bytes,
};
output::write(&partial, dump, cli.output.as_deref())?;
}
return Err(e.error);
}
Expand Down
10 changes: 10 additions & 0 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub enum Format {
Records,
/// Hex dump of every HID packet sent and received
Bytes,
/// Raw HID packet capture, replayable offline with `--from-bytes`
Binary,
}

// ── Entry point ───────────────────────────────────────────────────────────────
Expand All @@ -37,6 +39,7 @@ fn write_to<W: Write>(session: &Session, format: Format, mut w: W) -> Result<()>
Format::Csv => write_csv(session, &mut w),
Format::Records => write_records(session, &mut w),
Format::Bytes => write_bytes(session, &mut w),
Format::Binary => write_binary(session, &mut w),
}
}

Expand Down Expand Up @@ -91,6 +94,13 @@ fn write_records<W: Write>(session: &Session, w: &mut W) -> Result<()> {
Ok(())
}

// ── Binary packet capture ─────────────────────────────────────────────────────

fn write_binary<W: Write>(session: &Session, w: &mut W) -> Result<()> {
w.write_all(&crate::protocol::encode_packets(&session.raw_packets))?;
Ok(())
}

// ── Raw HID bytes ─────────────────────────────────────────────────────────────

fn write_bytes<W: Write>(session: &Session, w: &mut W) -> Result<()> {
Expand Down
Loading
Loading