Measure dropped samples from stream timestamps instead of hiding them

Overflows were counted but the samples the driver dropped were simply
missing, so a 5 s capture with 300 overflows still read as a clean 5.000 s
file. Each read's hardware timestamp is now compared with where the
previous read ended; gaps are sized exactly, zero-filled by default so the
file timeline stays true, and reported as completed_with_gaps with a
warning, samples_dropped and a gap list. This also catches drops the
plugin never flags as overflows (libusb transfer timeouts).

Timestamps are differenced before rounding: rounding each absolute
timestamp to a sample index flips by +/-1 near half-sample boundaries
and produced phantom 1-sample gaps.

- stream buffers 128 x 64K samples (~280 ms at 30 Msps, was ~4 ms)
- reject out-of-range gain, frequency, rate and bandwidth instead of
  letting the plugin clamp silently; default bandwidth clamps to range
- spectrum returns a max-hold decimated summary (256 points) plus
  noise_floor_db; full arrays were 139 KB at fft_size 4096
- capture_iq background=True with stream_status polling, so
  stop_capture is reachable; device-changing tools refuse while it runs
- OSError returned in the error shape; status drops "unknown" fields
- README: gap accounting, keep captures off tmpfs; captures/ ignored
This commit is contained in:
2026-10-25 11:57:25 -06:00
parent 067067e1e7
commit 9db76af626
7 changed files with 642 additions and 178 deletions
+9 -3
View File
@@ -83,7 +83,7 @@ claude -p "show bladerf status" --mcp-config .mcp.json --allowedTools "mcp__mcbl
### IQ Capture
- `bladerf_capture_iq` - Stream samples to file (CF32/CS16)
- `bladerf_capture_spectrum` - Capture and compute FFT, power in dBFS
- `bladerf_stream_status` - Check capture progress
- `bladerf_stream_status` - Capture progress, or the result of a background capture
- `bladerf_stop_capture` - Abort running capture
### Advanced
@@ -103,7 +103,7 @@ claude -p "show bladerf status" --mcp-config .mcp.json --allowedTools "mcp__mcbl
> Set frequency to 915 MHz with 2 MHz sample rate
[Uses bladerf_set_frequency and bladerf_set_sample_rate]
> Capture 1 second of IQ data to /tmp/capture.cf32
> Capture 1 second of IQ data to ~/claude/sdr/mcbladerf/captures/capture.cf32
[Uses bladerf_capture_iq]
> Show me the spectrum around 100 MHz
@@ -112,7 +112,13 @@ claude -p "show bladerf status" --mcp-config .mcp.json --allowedTools "mcp__mcbl
## Capture Files and Readings
CF32 files are complex64 scaled to ±1.0. CS16 files are the device's raw SC16 Q11 integers, interleaved I/Q with full scale 2048, the same layout `bladeRF-cli` writes with `format=bin`. Spectrum power is dBFS, so a full-scale tone reads 0. Every capture reports `overflows` (dropped-sample events), `clipped_samples` and `peak_dbfs`, so check those before trusting the data.
CF32 files are complex64 scaled to ±1.0. CS16 files are the device's raw SC16 Q11 integers, interleaved I/Q with full scale 2048, the same layout `bladeRF-cli` writes with `format=bin`. Spectrum power is dBFS, so a full-scale tone reads 0. Every capture reports `clipped_samples` and `peak_dbfs`, so check those before trusting the data.
When the host falls behind, the driver drops samples, and not always with an overflow error (a libusb transfer timeout drops data silently). Each read carries a hardware timestamp, so every gap is measured exactly and, by default, zero-filled: sample N of the file is always N / rate seconds after the first. Such captures finish as `completed_with_gaps` with a `warning`, `samples_dropped` and a gap list in output-sample offsets. Pass `fill_gaps=false` to get a shorter file with the same report instead.
Long captures should use `background=true`: the call returns at once, `bladerf_stream_status` shows progress and then the result, and `bladerf_stop_capture` ends it early. Spectra come back as a 256-point max-hold summary with `noise_floor_db`; peak and floor are computed at full resolution. Out-of-range gain, frequency, rate or bandwidth values are rejected, not clamped. The sample rate resets to 4 MHz each time the device is opened, so always set it.
Write captures to disk, not `/tmp`. On many Linux systems `/tmp` is a tmpfs held in RAM, and 30 Msps CS16 is 120 MB per second, so a few long captures can push the machine into swap and the OOM killer. Once the host stalls, the stream drops samples. The project's `captures/` directory is gitignored for this.
Gains you leave out keep whatever the device last had, including values set by another process. The radio is often shared, so set all three RX gains explicitly.