Skip to content

Latest commit

 

History

History
377 lines (280 loc) · 9.8 KB

File metadata and controls

377 lines (280 loc) · 9.8 KB

RSPdx-R2 Python Implementation Specification

Goal

Build a Python package that controls SDRplay RSPdx-R2 hardware through SDRplay API v3.15 or later.

The implementation should be small and conservative at first. It should prove that the API can be loaded, the device can be discovered, and a short IQ capture can be performed safely.

Out of Scope for Initial Version

  • Full SDR receiver UI.
  • Demodulation.
  • Long-running recording service.
  • Automatic system service management.
  • API installation.
  • OS driver installation.
  • RSPduo dual-tuner behavior.
  • High-level DSP beyond basic sample conversion and optional power estimate.

Native API Flow

Use the SDRplay API in this order:

  1. sdrplay_api_Open
  2. sdrplay_api_ApiVersion
  3. Validate API version is at least 3.15.
  4. sdrplay_api_LockDeviceApi
  5. sdrplay_api_GetDevices
  6. Select an RSPdx-R2 device from the returned list.
  7. sdrplay_api_SelectDevice
  8. sdrplay_api_GetDeviceParams
  9. Configure device and tuner parameters.
  10. sdrplay_api_Init
  11. Receive streaming data through callbacks.
  12. sdrplay_api_Uninit
  13. sdrplay_api_ReleaseDevice
  14. sdrplay_api_UnlockDeviceApi
  15. sdrplay_api_Close

All cleanup steps must be best-effort and idempotent from the Python wrapper's point of view.

Native Library Loading

Implement library discovery in rspdx_r2/loader.py.

Search order:

  1. Explicit path passed by the caller.
  2. SDRPLAY_API_LIB environment variable.
  3. Platform defaults:
    • macOS/Linux: common names under /usr/local/lib.
    • Windows: SDRplay install directory if discoverable, then common DLL names.

The loader should not modify system paths or install anything.

Error Handling

Implement rspdx_r2/errors.py.

Requirements:

  • Wrap every SDRplay API return code.
  • Raise a custom SdrplayApiError on non-success status.
  • Include:
    • API function name.
    • numeric status code.
    • error string from sdrplay_api_GetErrorString when available.
  • Preserve context for failures during cleanup, but do not hide the original streaming or configuration exception.

ctypes Definitions

Implement rspdx_r2/structs.py and rspdx_r2/constants.py.

Only define the structs, enums, and constants required for the first version:

  • API function signatures for the flow above.
  • Device enumeration structures.
  • Device parameter structures needed for:
    • RF frequency.
    • Sample rate.
    • IF type.
    • Bandwidth.
    • Gain reduction / AGC.
    • RSPdx/RSPdx-R2 antenna selection.
    • RSPdx/RSPdx-R2 HDR settings.
    • Notch filters.
    • Bias-T.
  • Callback prototypes for streaming data and events.

Use names that match the SDRplay API specification closely enough that future maintenance can be done by comparing directly with sdrplay_api.h.

Configuration Model

Implement rspdx_r2/config.py.

Provide a dataclass similar to:

@dataclass
class RspDxR2Config:
    center_hz: float
    sample_rate_hz: float = 2_000_000
    bandwidth: str = "1_536"
    if_type: str = "zero_if"
    antenna: str = "A"
    agc_enabled: bool = True
    agc_setpoint_dbfs: int = -30
    lna_state: int | None = None
    gain_reduction_db: int | None = None
    hdr_enabled: bool = False
    hdr_bandwidth: str = "1_700"
    rf_notch_enabled: bool = False
    dab_notch_enabled: bool = False
    bias_t_enabled: bool = False

Validation requirements:

  • center_hz must be within the RSPdx-R2 usable range.
  • sample_rate_hz must be positive and within SDRplay API supported values.
  • antenna must be one of A, B, or C.
  • Bias-T defaults to False.
  • HDR defaults to False.
  • HDR should only be accepted for frequencies where RSPdx/RSPdx-R2 HDR mode is valid according to the API specification.

Device Wrapper

Implement rspdx_r2/device.py.

Provide a context manager:

with RspDxR2Device.open() as dev:
    print(dev.api_version)
    print(dev.device_info)

And streaming:

with RspDxR2Device.open() as dev:
    dev.configure(config)
    samples = dev.capture(duration_sec=1.0)

Requirements:

  • Hold callback references on the device object so Python garbage collection cannot invalidate them while streaming.
  • Make close() safe to call multiple times.
  • Use try/finally for every init/select/open boundary.
  • Handle Ctrl-C by stopping streaming and releasing the device.

Streaming

Implement rspdx_r2/stream.py.

Requirements:

  • Convert callback xi and xq int16 buffers into NumPy arrays.
  • Use interleaved IQ output or complex64 output, with the choice explicit.
  • Keep callback work minimal.
  • Use a bounded queue or ring buffer to avoid unbounded memory growth.
  • Support short captures by duration or sample count.

Recommended first output formats:

  • .npy complex64
  • raw interleaved int16 IQ

Export schema v1

The capture export schema version is fixed as:

schema_version = "rspdx-r2-capture-v1"

Required keys:

  • schema_version
  • source
  • export_format
  • center_hz
  • sample_rate_hz
  • sample_count
  • dtype

Optional keys:

  • npy_path
  • raw_path
  • raw_format
  • iq_order
  • bandwidth
  • if_type
  • agc_enabled
  • gain_reduction_db
  • lna_state

AI-CW-Decoder Capture Export Specification

The RSPdx-R2 capture export contract is versioned as rspdx-r2-capture-v1. This schema version is fixed for the v1 export format.

capture.json is the only integration entry point for downstream consumers such as AI-CW-Decoder. It references the generated IQ payload files and carries the metadata required to validate them.

Required Metadata

The metadata JSON must contain these keys:

  • schema_version: exactly "rspdx-r2-capture-v1".
  • source: exactly "rspdx-r2".
  • export_format: list of exported data formats.
  • center_hz: RF center frequency in Hz.
  • sample_rate_hz: sample rate in Hz.
  • duration_sec: requested capture duration in seconds.
  • antenna: selected antenna.
  • sample_count: number of complex IQ samples.
  • dtype: expected .npy dtype, currently "complex64".
  • npy_path: relative or absolute .npy path, or null.
  • raw_path: relative or absolute .raw path, or null.
  • raw_format: currently "interleaved_int16_iq".
  • iq_order: currently "I,Q".
  • created_by: producer identifier.

Relative npy_path and raw_path values must be resolved relative to the parent directory of capture.json, not the process working directory.

Export Formats

complex64_npy:

  • Load with np.load(path).
  • Loaded array dtype must be np.complex64.
  • Loaded array shape must be (sample_count,).

interleaved_int16_iq:

  • Load with np.fromfile(path, dtype=np.int16).
  • Raw layout is I0,Q0,I1,Q1,....
  • Raw element count must be sample_count * 2.
  • Convert with:
real = raw[0::2]
imag = raw[1::2]
iq = real.astype(np.float32) + 1j * imag.astype(np.float32)
iq = iq.astype(np.complex64)

Downstream Validation Rules

AI-CW-Decoder must validate:

  • schema_version is "rspdx-r2-capture-v1".
  • source is "rspdx-r2".
  • export_format contains the format it intends to read.
  • sample_count matches the loaded .npy shape or raw element count.
  • raw_format is "interleaved_int16_iq" before reading raw data.
  • iq_order is "I,Q" before reconstructing raw IQ data.

Any sample_count mismatch is an error. Any unsupported raw_format or iq_order is an error.

Realtime API

iter_iq_chunks() provides queue-backed realtime IQ delivery for integrations that should not wait for a complete capture file.

Signature:

iter_iq_chunks(
    duration_sec=None,
    chunk_samples=8192,
)

Behavior:

  • Yields NumPy complex64 arrays.
  • Uses a queue-backed stream path.
  • The queue must be bounded to avoid unbounded memory growth.
  • dropped_chunks must be tracked when chunks cannot be queued.
  • get_stream_stats() exposes stream counters, including dropped chunks.

AI-CW-Decoder realtime decoding should consume iter_iq_chunks() and keep DSP work outside the SDRplay callback path.

Examples

examples/api_version.py

Behavior:

  • Load API.
  • Print API version.
  • Exit non-zero on failure.

examples/list_devices.py

Behavior:

  • Load API.
  • Enumerate devices.
  • Print serial number, hardware version, and whether the device appears to be RSPdx-R2.
  • Do not select or initialize the device unless required by the API.

examples/capture_iq.py

Arguments:

  • --center-hz
  • --sample-rate
  • --duration-sec
  • --antenna
  • --out
  • --format with choices npy-complex64 and raw-int16
  • Optional --hdr
  • Optional --bias-t, default disabled

Behavior:

  • Validate arguments.
  • Open API.
  • Select RSPdx-R2.
  • Configure device.
  • Capture for the requested short duration.
  • Save output.
  • Release API resources.

Tests

Initial tests should avoid requiring physical hardware.

Add tests for:

  • Config validation.
  • Antenna name mapping.
  • Bias-T default is off.
  • HDR validation rejects invalid frequencies.
  • Error code to exception mapping.
  • Loader respects explicit path and SDRPLAY_API_LIB.

Hardware integration tests may be added later and should be opt-in, for example behind RSPDX_R2_ENABLE_HARDWARE_TESTS=1.

opencode/Qwen3 Prompt

Use this prompt when generating the first implementation:

Create the Python package described by README.md and SPEC.md.

Use ctypes to call SDRplay API v3.15 or later. Implement only the initial
scope: API loading, API version check, device enumeration, RSPdx-R2 selection,
basic configuration, short IQ capture, examples, and hardware-free unit tests.

Do not run sudo. Do not install packages. Do not modify system files or services.
Bias-T must default to off. HDR must default to off. Keep streaming callbacks
minimal and preserve callback references to avoid garbage collection issues.

Follow the file layout in README.md. Use pyproject.toml and requirements.txt.
Prefer clear, maintainable ctypes definitions that can be compared with the
SDRplay API v3.15 headers.