Metadata-Version: 2.4
Name: esp-blfd
Version: 0.1.0
Summary: Streaming decoders for BLE log frames
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# BLE Log Frame Decoder User Guide

`esp-blfd` is a Python library for decoding binary BLE log frames. It provides:

- `FrameDecoder` for decoding every valid frame and resynchronizing after corrupt data;
- a Cython-accelerated backend with an automatic pure-Python fallback.

The library does not provide a command-line interface or interpret application-specific payload contents.

## Requirements and installation

Python 3.11 or later is required.

For development or execution from the source directory:

```bash
cd ble_log_frame_decoder
uv sync
uv run python -c "import ble_log_frame_decoder; print('ok')"
```

To install from local source into another Python environment:

```bash
python -m pip install /path/to/ble_log_frame_decoder
```

Installation attempts to compile the Cython extension. If a C build environment is unavailable on the current platform, installation can still complete and the package automatically uses its pure-Python backend. The public API and decoding results are unchanged.

## Selecting a frame format

Import `FrameFormat` and explicitly select the format of the input data:

| Enum value | String value | Purpose |
|---|---|---|
| `FrameFormat.BLE_LOG_V1_SUM32` | `ble-log-v1-sum32` | BLE log v1 with byte-sum checksum |
| `FrameFormat.BLE_LOG_V2_SUM32` | `ble-log-v2-sum32` | BLE log v2 with byte-sum checksum |
| `FrameFormat.BLE_LOG_V2_XOR32` | `ble-log-v2-xor32` | BLE log v2 with 32-bit XOR checksum |

The default is `BLE_LOG_V2_XOR32`. Frame envelopes can overlap between formats, so the library does not auto-detect the format. Always pass `format` explicitly unless the caller only accepts the default format.

## Quick start

### Decode an entire file at once

```python
from pathlib import Path

from ble_log_frame_decoder import FrameDecoder, FrameFormat

raw = Path("ble.log").read_bytes()
decoder = FrameDecoder(FrameFormat.BLE_LOG_V2_XOR32)
frames = decoder.feed(raw)
stats = decoder.finish(require_complete=True)

for frame in frames:
    print(frame.offset, frame.source_code, frame.sequence_number, frame.payload)
```

`feed()` returns every complete, valid frame in its input and automatically skips corrupt data. Pass a small file in one call, or use the same interface with chunks for large files and continuous input.

### Stream-decode a file

The streaming API is intended for large multi-frame files, serial data, and network data. Frames may cross arbitrary chunk boundaries:

```python
from ble_log_frame_decoder import FrameDecoder, FrameFormat


decoder = FrameDecoder(FrameFormat.BLE_LOG_V2_XOR32)

with open("ble.log", "rb") as log_file:
    while chunk := log_file.read(64 * 1024):
        for frame in decoder.feed(chunk):
            print(frame.offset, frame.source_code, frame.sequence_number)

stats = decoder.finish(require_complete=True)
print(stats)
```

`feed()` returns every frame that became complete with the current chunk. `feed()` cannot be called again after `finish()`.

`require_complete=True` only guarantees that no incomplete frame remains at the end of the stream. If the input must contain no garbage or corrupt data, also check:

```python
assert stats.discarded_bytes == 0
assert stats.resync_count == 0
```

## Returned objects

### `BleLogFrame`

| Field | Meaning |
|---|---|
| `source_code` | 8-bit log source code |
| `sequence_number` | 16-bit sequence number for v1; 24-bit for v2 |
| `payload` | A `bytes` copy of the payload |
| `offset` | Absolute byte offset of the frame in the full input stream |
| `size` | Full frame size, including header and checksum |
| `skipped_bytes_before` | Bytes discarded immediately before this frame was found; `0` for strict parsing |
| `type_code` | 8-bit v1 type code; `None` for v2 |

`BleLogFrame` is an immutable dataclass.

## Decoder statistics

`decoder.stats` and `decoder.finish()` return `DecodeStats`:

| Field | Meaning |
|---|---|
| `bytes_received` | Total bytes passed to `feed()` |
| `frames_decoded` | Number of valid frames returned |
| `discarded_bytes` | Total invalid bytes discarded during resynchronization |
| `resync_count` | Times a frame boundary was found after invalid data was discarded |
| `trailing_bytes` | Incomplete bytes remaining when `finish()` was called |
| `buffered_bytes` | Bytes currently retained in the internal buffer |
| `peak_buffered_bytes` | Peak internal buffer size during decoding |

Read `decoder.stats` at any point without finishing the decoder.

## Error handling

```python
from ble_log_frame_decoder import (
    FrameDecoder,
    IncompleteFrameError,
)

decoder = FrameDecoder(selected_format)
try:
    frames = decoder.feed(raw)
    decoder.finish(require_complete=True)
except IncompleteFrameError as exc:
    print(f"Incomplete data at the end of the stream, starting at {exc.offset}")
```

- `IncompleteFrameError`: with `require_complete=True`, the data at the end of the stream is not a complete frame. It inherits from `FrameDecodeError` and `ValueError` and exposes an `offset` attribute.
- Candidates with invalid lengths, excessive sizes, or checksum mismatches are skipped before the decoder attempts to resynchronize.
- Use each frame's `skipped_bytes_before` and the `FrameDecoder` statistics to detect skipped data.

## Limiting frame size

For data crossing an untrusted boundary, set `max_frame_size` below the protocol maximum:

```python
decoder = FrameDecoder(
    FrameFormat.BLE_LOG_V2_XOR32,
    max_frame_size=4096,
)
```

The value is the complete frame size, including header, payload, and checksum. Its valid range depends on the format:

| Format | Minimum | Protocol maximum |
|---|---:|---:|
| All BLE log formats | 10 | 65545 |

A candidate exceeding `max_frame_size` is skipped as invalid data before the decoder attempts to resynchronize.

## Wire format reference

All integers are little-endian. Every checksum field is an unsigned 32-bit integer.

### BLE log v1 sum32

```text
payload_length:u16 | source_code:u8 | type_code:u8 | sequence_number:u16
payload:payload_length | checksum:u32
```

The checksum is the sum of every frame byte before the checksum field, reduced modulo $2^{32}$.

### BLE log v2 sum32 / xor32

```text
payload_length:u16 | frame_meta:u32 | payload:payload_length | checksum:u32
```

The low 8 bits of `frame_meta` are `source_code`; the upper 24 bits are `sequence_number`.

- sum32: sum every frame byte before the checksum field, reduced modulo $2^{32}$;
- xor32: split the data before the checksum into little-endian 32-bit words and XOR them. A final group shorter than four bytes is zero-padded in its high bytes.

