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.
- 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.
Use the SDRplay API in this order:
sdrplay_api_Opensdrplay_api_ApiVersion- Validate API version is at least
3.15. sdrplay_api_LockDeviceApisdrplay_api_GetDevices- Select an RSPdx-R2 device from the returned list.
sdrplay_api_SelectDevicesdrplay_api_GetDeviceParams- Configure device and tuner parameters.
sdrplay_api_Init- Receive streaming data through callbacks.
sdrplay_api_Uninitsdrplay_api_ReleaseDevicesdrplay_api_UnlockDeviceApisdrplay_api_Close
All cleanup steps must be best-effort and idempotent from the Python wrapper's point of view.
Implement library discovery in rspdx_r2/loader.py.
Search order:
- Explicit path passed by the caller.
SDRPLAY_API_LIBenvironment variable.- Platform defaults:
- macOS/Linux: common names under
/usr/local/lib. - Windows: SDRplay install directory if discoverable, then common DLL names.
- macOS/Linux: common names under
The loader should not modify system paths or install anything.
Implement rspdx_r2/errors.py.
Requirements:
- Wrap every SDRplay API return code.
- Raise a custom
SdrplayApiErroron non-success status. - Include:
- API function name.
- numeric status code.
- error string from
sdrplay_api_GetErrorStringwhen available.
- Preserve context for failures during cleanup, but do not hide the original streaming or configuration exception.
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.
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 = FalseValidation requirements:
center_hzmust be within the RSPdx-R2 usable range.sample_rate_hzmust be positive and within SDRplay API supported values.antennamust be one ofA,B, orC.- 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.
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/finallyfor every init/select/open boundary. - Handle Ctrl-C by stopping streaming and releasing the device.
Implement rspdx_r2/stream.py.
Requirements:
- Convert callback
xiandxqint16 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:
.npycomplex64- raw interleaved int16 IQ
The capture export schema version is fixed as:
schema_version = "rspdx-r2-capture-v1"
Required keys:
schema_versionsourceexport_formatcenter_hzsample_rate_hzsample_countdtype
Optional keys:
npy_pathraw_pathraw_formatiq_orderbandwidthif_typeagc_enabledgain_reduction_dblna_state
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.
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.npydtype, currently"complex64".npy_path: relative or absolute.npypath, ornull.raw_path: relative or absolute.rawpath, ornull.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.
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)AI-CW-Decoder must validate:
schema_versionis"rspdx-r2-capture-v1".sourceis"rspdx-r2".export_formatcontains the format it intends to read.sample_countmatches the loaded.npyshape or raw element count.raw_formatis"interleaved_int16_iq"before reading raw data.iq_orderis"I,Q"before reconstructing raw IQ data.
Any sample_count mismatch is an error. Any unsupported raw_format or
iq_order is an error.
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
complex64arrays. - Uses a queue-backed stream path.
- The queue must be bounded to avoid unbounded memory growth.
dropped_chunksmust 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.
Behavior:
- Load API.
- Print API version.
- Exit non-zero on failure.
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.
Arguments:
--center-hz--sample-rate--duration-sec--antenna--out--formatwith choicesnpy-complex64andraw-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.
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.
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.