diff --git a/README.md b/README.md
index c953818..a9d1241 100644
--- a/README.md
+++ b/README.md
@@ -9,17 +9,25 @@
-
+
+
+
+
+
-
+
+
+
+
+
# RIA Toolkit OSS
@@ -32,15 +40,13 @@ RIA Toolkit OSS is the open-source version of the RIA Toolkit, providing the fun
- Fundamental recording augmentations and impairments for radio ML dataset preparation.
-- (Coming soon) A unified interface for interacting with software-defined radios, including [USRP](https://www.ettus.com/products/), [BladeRF](https://www.nuand.com/), [PlutoSDR](https://www.analog.com/en/resources/evaluation-hardware-and-software/evaluation-boards-kits/adalm-pluto.html), [RTL-SDR](https://www.rtl-sdr.com/), [HackRF](https://greatscottgadgets.com/hackrf/), and [thinkRF](https://thinkrf.com/).
-
-- (Coming soon) Basic model training and testing utilities.
+- A unified interface for interacting with software-defined radios, including [USRP](https://www.ettus.com/products/), [BladeRF](https://www.nuand.com/), [PlutoSDR](https://www.analog.com/en/resources/evaluation-hardware-and-software/evaluation-boards-kits/adalm-pluto.html), and [bladeRF](https://www.nuand.com/bladerf-1/). (Support for [RTL-SDR](https://www.rtl-sdr.com/), [HackRF](https://greatscottgadgets.com/hackrf/), and [thinkRF](https://thinkrf.com/) coming soon!).
## 💡 Want More RIA?
- **[RIA Toolkit](https://qoherent.ai/riatoolkit/)**: The full, unthrottled set of tools for developing, testing, and deploying radio intelligence applications.
-- **[RIA Hub](https://qoherent.ai/riahub/)**: Wield the RIA Toolkit, plus purpose-built automations, directly in your browser, without the need to write code or set up infrastructure. Additionally, unlock access to Qoherent's rich IP library as well as community projects.
+- **[RIA Hub](https://qoherent.ai/riahub/)**: Wield the RIA Toolkit, plus purpose-built automations, directly in your browser without the need to write code or set up infrastructure. Additionally, unlock access to Qoherent's rich IP library as well as community projects.
- **[RIA RAN](https://qoherent.ai/intelligent-5g-ran/)**: Radio intelligence solutions engineered to seamlessly integrate with existing RAN environments, including ORAN-compliant networks.
@@ -48,11 +54,13 @@ RIA Toolkit OSS is the open-source version of the RIA Toolkit, providing the fun
RIA Hub Toolkit OSS can be installed either as a Conda package or as a standard Python package.
+Please note that SDR drivers must be installed separately. Refer to the relevant guide in the project documentation for setup instructions: [SDR Guides](https://ria-toolkit-oss.readthedocs.io/en/latest/sdr_guides/).
+
### Installation with Conda (recommended)
Conda package for RIA Toolkit OSS are available on RIA Hub: [RIA Hub Conda Package Registry: `ria-toolkit-oss`](https://riahub.ai/qoherent/-/packages/conda/ria-toolkit-oss).
-RIA Toolkit OSS can be installed into any Conda environment. However, it is recommended to install within the base environment of [Radioconda](https://github.com/radioconda/radioconda-installer), which includes [GNU Radio](https://www.gnuradio.org/) and several pre-configured libraries for common SDR devices. Detailed instructions for installing and setting up Radioconda are available in the project README.
+RIA Toolkit OSS can be installed into any Conda environment. However, it is recommended to install within the base environment of [Radioconda](https://github.com/radioconda/radioconda-installer), which includes [GNU Radio](https://www.gnuradio.org/) and several pre-configured libraries for common SDR devices.
Please follow the steps below to install RIA Toolkit OSS using Conda:
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 3e59f96..dac544b 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -36,7 +36,12 @@ todo_include_todos = True
templates_path = ['.templates']
exclude_patterns = []
-autodoc_mock_imports = ['uhd', 'adi', 'iio', 'rtlsdr']
+# These modules are required for SDR hardware support, but are not listed
+# as Python dependencies in pyproject.toml because they must be installed
+# separately (often with system-level drivers or vendor packages).
+# We mock them here so Sphinx can build the documentation without requiring
+# the actual hardware libraries to be present.
+autodoc_mock_imports = ['uhd', 'adi', 'iio', 'bladerf']
autodoc_default_options = {
'members': True,
@@ -47,7 +52,9 @@ version_link = f"{sys.version_info.major}.{sys.version_info.minor}"
intersphinx_mapping = {'python': (f'https://docs.python.org/{version_link}', None),
'numpy': ('https://numpy.org/doc/stable', None),
'scipy': ('https://docs.scipy.org/doc/scipy', None),
- 'matplotlib': ('https://matplotlib.org/stable', None)}
+ 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None),
+ 'h5py': ('https://docs.h5py.org/en/stable', None),
+ 'plotly': ('https://plotly.com/python-api-reference', None)}
def autodoc_process_docstring(app, what, name, obj, options, lines):
diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst
new file mode 100644
index 0000000..f018b6f
--- /dev/null
+++ b/docs/source/examples/index.rst
@@ -0,0 +1,13 @@
+.. _examples:
+
+########
+Examples
+########
+
+This section contains usage examples designed to help you get started with RIA Toolkit OSS.
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ SDR Examples
diff --git a/docs/source/examples/sdr/index.rst b/docs/source/examples/sdr/index.rst
new file mode 100644
index 0000000..c563bee
--- /dev/null
+++ b/docs/source/examples/sdr/index.rst
@@ -0,0 +1,17 @@
+.. _examples:
+
+############
+SDR Examples
+############
+
+This section contains examples of how to use the SDR package in RIA Toolkit OSS, such as receiving and transmitting signals.
+
+Please note that additional setup is required for most SDR devices after installing the toolkit.
+For more information, refer to the :ref:`sdr_guides`.
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ Example 1: SDR Reception
+ Example 2: SDR Transmission
diff --git a/docs/source/examples/sdr/rx.rst b/docs/source/examples/sdr/rx.rst
new file mode 100644
index 0000000..8fd2e3e
--- /dev/null
+++ b/docs/source/examples/sdr/rx.rst
@@ -0,0 +1,58 @@
+.. _rx:
+
+Example 1: SDR Reception
+========================
+
+.. contents::
+ :local:
+
+Introduction
+------------
+
+The following examples demonstrate how to initialize an SDR, record a signal, and transmit a custom waveform.
+These examples assume familiarity with Python and SDR concepts.
+
+In this example, we use the [bladeRF](https://www.nuand.com/bladerf-1/). However, because
+this package presents a common interface for all SDR devices, the same code can be used
+to interface with additional supported radios.
+
+Code
+----
+
+In this example, we initialize the `Blade` SDR, configure it to record a signal for a specified duration.
+
+.. code-block:: python
+
+ import time
+
+ from ria_toolkit_oss.datatypes.recording import Recording
+ from ria_toolkit_oss.sdr.blade import Blade
+
+ my_radio = Blade()
+ print(my_radio)
+ print(type(my_radio))
+
+ my_radio.init_rx(
+ sample_rate=1e6,
+ center_frequency=2.44e9,
+ gain=50,
+ channel=0,
+ )
+
+ rx_time = 0.01
+ start = time.time()
+ my_rec = my_radio.record(rx_time=rx_time)
+ end = time.time()
+
+ print(f"Total time: {end - start} seconds")
+ print(f"Length of the recording: {len(my_rec)} samples")
+
+.. todo::
+
+ Add one extra step to the end of this example to visualize the received signal
+
+Conclusion
+----------
+
+This example demonstrates how to use the ``Blade`` class to receive samples into a ``Recording`` object. By customizing the parameters,
+we can adapt this example to various signal processing and SDR tasks.
diff --git a/docs/source/examples/sdr/tx.rst b/docs/source/examples/sdr/tx.rst
new file mode 100644
index 0000000..4975f1d
--- /dev/null
+++ b/docs/source/examples/sdr/tx.rst
@@ -0,0 +1,69 @@
+.. _tx:
+
+Example 2: SDR Transmission
+===========================
+
+.. contents::
+ :local:
+
+Introduction
+------------
+
+This example illustrates how to generate a custom chirp signal and transmit it using the ``Blade`` SDR. The waveform is
+created using the ``numpy`` library and encapsulated in a ``Recording`` object.
+
+Code
+----
+
+.. code-block:: python
+
+ import time
+
+ import numpy as np
+
+ from ria_toolkit_oss.datatypes.recording import Recording
+ from ria_toolkit_oss.sdr.blade import Blade
+
+ # Parameters
+ num_samples = 1_000_000 # Total number of samples
+ num_chirps = 10 # Number of upchirps
+ sample_rate = 1e6 # Sample rate in Hz (arbitrary choice for normalization)
+ chirp_duration = num_samples // num_chirps / sample_rate # Duration of each chirp in seconds
+ f_start = 0 # Start frequency of the chirp (normalized)
+ f_end = 0.5 * sample_rate # End frequency of the chirp (normalized to Nyquist)
+
+ # Generate IQ data as a series of chirps
+ t = np.linspace(
+ 0, chirp_duration, num_samples // num_chirps, endpoint=False
+ )
+ chirp = np.exp(
+ 2j
+ * np.pi
+ * (t * f_start + (f_end - f_start) / (2 * chirp_duration) * t**2)
+ )
+ iq_data = np.tile(chirp, num_chirps)[:num_samples].astype("complex64")
+
+ # Wrap in Recording object
+ iq_data = Recording(data=iq_data)
+
+ # Initialize and configure the radio
+ my_radio = Blade()
+ my_radio.init_tx(
+ sample_rate=sample_rate,
+ center_frequency=2.44e9,
+ gain=50,
+ channel=0,
+ )
+
+ # Transmit the recording
+ start = time.time()
+ my_radio.transmit_recording(recording=iq_data, tx_time=10)
+ end = time.time()
+
+ print(f"Transmission complete. Total time: {end - start:.4f} seconds")
+
+Conclusion
+----------
+
+This example demonstrates how to use the ``Blade`` class to transmit a custom waveform. By customizing the parameters,
+we can adapt this example to various signal processing and SDR tasks.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 9bcee52..b4a2fa3 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -5,11 +5,9 @@ RIA Toolkit OSS Documentation
:maxdepth: 2
Introduction
- Datatypes Package
- IO Package
- Transforms Package
- Utils Package
- Viz Package
+ SDR Guides
+ Examples
+ RIA Toolkit OSS
Indices and tables
==================
diff --git a/docs/source/intro/getting_started.rst b/docs/source/intro/getting_started.rst
index ed8e14b..928e753 100644
--- a/docs/source/intro/getting_started.rst
+++ b/docs/source/intro/getting_started.rst
@@ -4,4 +4,4 @@ Getting Started
.. todo::
Getting started instructions are coming soon! In the meantime, feel free
- to explore the project documentation, where many components include usage examples.
+ to explore the project documentation. Many components include usage examples.
diff --git a/docs/source/intro/installation.rst b/docs/source/intro/installation.rst
index 7204c1a..584b13f 100644
--- a/docs/source/intro/installation.rst
+++ b/docs/source/intro/installation.rst
@@ -3,6 +3,9 @@ Installation
RIA Hub Toolkit OSS can be installed either as a Conda package or as a standard Python package.
+Please note that SDR drivers must be installed separately. Refer to the relevant guide in the
+:ref:`SDR Guides ` section of the documentation for addition setup instructions.
+
We want your experience with RIA Toolkit OSS to be as smooth and frictionless as possible. If you run into any
issues during installation, please reach out to our support team: ``support@qoherent.ai``.
diff --git a/docs/source/ria_toolkit_oss/index.rst b/docs/source/ria_toolkit_oss/index.rst
new file mode 100644
index 0000000..42eb31f
--- /dev/null
+++ b/docs/source/ria_toolkit_oss/index.rst
@@ -0,0 +1,19 @@
+.. _ria_toolkit_oss:
+
+###############
+RIA Toolkit OSS
+###############
+
+This section provides the Sphinx-generated API reference for the RIA Toolkit OSS, including
+class and function signatures, and doctest examples where available.
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ Datatypes Package
+ SDR Package
+ IO Package
+ Transforms Package
+ Utils Package
+ Viz Package
diff --git a/docs/source/ria_toolkit_oss/ria_toolkit_oss.sdr.rst b/docs/source/ria_toolkit_oss/ria_toolkit_oss.sdr.rst
new file mode 100644
index 0000000..0ac9cfe
--- /dev/null
+++ b/docs/source/ria_toolkit_oss/ria_toolkit_oss.sdr.rst
@@ -0,0 +1,16 @@
+SDR Package (ria_toolkit_oss.sdr)
+=================================
+
+.. automodule:: ria_toolkit_oss.sdr
+ :members:
+ :undoc-members:
+ :inherited-members:
+ :show-inheritance:
+
+Radio Classes
+-------------
+
+.. autoclass:: ria_toolkit_oss.sdr.usrp.USRP
+.. autoclass:: ria_toolkit_oss.sdr.blade.Blade
+.. autoclass:: ria_toolkit_oss.sdr.hackrf.HackRF
+.. autoclass:: ria_toolkit_oss.sdr.pluto.Pluto
diff --git a/docs/source/sdr_guides/blade.rst b/docs/source/sdr_guides/blade.rst
new file mode 100644
index 0000000..41641b6
--- /dev/null
+++ b/docs/source/sdr_guides/blade.rst
@@ -0,0 +1,77 @@
+.. _blade:
+
+BladeRF
+=======
+
+The BladeRF is a versatile software-defined radio (SDR) platform developed by Nuand. It is designed for a wide
+range of applications, from wireless communication research to field deployments. BladeRF devices are known
+for their high performance, flexibility, and extensive open-source support, making them suitable for both
+hobbyists and professionals. The BladeRF is based on the Analog Devices AD9361 RF transceiver, which provides
+wide frequency coverage and high bandwidth.
+
+Supported Models
+----------------
+
+- **BladeRF 2.0 Micro xA4:** A compact model with a 49 kLE FPGA, ideal for portable applications.
+- **BladeRF 2.0 Micro xA9:** A higher-end version of the Micro with a 115 kLE FPGA, offering more processing power in a small form factor.
+
+Key Features
+------------
+
+- **Frequency Range:** Typically from 47 MHz to 6 GHz, covering a wide range of wireless communication bands.
+- **Bandwidth:** Up to 56 MHz, allowing for wideband signal processing.
+- **FPGA:** Integrated FPGA (varies by model) for real-time processing and custom logic development.
+- **Connectivity:** USB 3.0 interface for high-speed data transfer, with options for GPIO, SPI, and other I/O.
+
+Hackability
+-----------
+
+- **Expansion:** The BladeRF features GPIO, expansion headers, and add-on boards, allowing users to extend the
+ functionality of the device for specific applications, such as additional RF front ends.
+- **Frequency and Bandwidth Modification:** Advanced users can modify the BladeRF's settings and firmware to
+ explore different frequency bands and optimize the bandwidth for their specific use cases.
+
+Limitations
+-----------
+
+- The complexity of FPGA development may present a steep learning curve for users unfamiliar with hardware
+ description languages (HDL).
+- Bandwidth is capped at 56 MHz, which might not be sufficient for ultra-wideband applications.
+- USB 3.0 connectivity is required for optimal performance; using USB 2.0 will significantly limit data
+ transfer rates.
+
+Set up instructions (Linux, Radioconda)
+---------------------------------------
+
+1. Activate your Radioconda environment.
+
+ .. code-block:: bash
+
+ conda activate
+
+2. Install the base dependencies and drivers (*Easy method*):
+
+ .. code-block:: bash
+
+ sudo add-apt-repository ppa:nuandllc/bladerf
+ sudo apt-get update
+ sudo apt-get install bladerf
+ sudo apt-get install libbladerf-dev
+ sudo apt-get install bladerf-fpga-hostedxa4 # Necessary for installation of bladeRF 2.0 Micro A4.
+
+3. Install a ``udev`` rule by creating a link into your Radioconda installation:
+
+ .. code-block:: bash
+
+ sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bladerf1.rules /etc/udev/rules.d/88-radioconda-nuand-bladerf1.rules
+ sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bladerf2.rules /etc/udev/rules.d/88-radioconda-nuand-bladerf2.rules
+ sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bootloader.rules /etc/udev/rules.d/88-radioconda-nuand-bootloader.rules
+ sudo udevadm control --reload
+ sudo udevadm trigger
+
+Further Information
+-------------------
+
+- `Official BladeRF Website `_
+- `BladeRF GitHub Repository `_
+- `BladeRF Setup with Radioconda `_
diff --git a/docs/source/sdr_guides/hackrf.rst b/docs/source/sdr_guides/hackrf.rst
new file mode 100644
index 0000000..6f65a6d
--- /dev/null
+++ b/docs/source/sdr_guides/hackrf.rst
@@ -0,0 +1,83 @@
+.. _hackrf:
+
+HackRF
+======
+
+The HackRF One is a portable and affordable software-defined radio developed by Great Scott Gadgets. It is an
+open source hardware platform that is designed to enable test and development of modern and next generation
+radio technologies.
+
+The HackRF is based on the Analog Devices MAX2839 transceiver chip, which supports both transmission and
+reception of signals across a wide frequency range, combined with a MAX5864 RF front-end chip and a
+RFFC5072 wideband synthesizer/VCO.
+
+Supported models
+----------------
+
+- **HackRF One:** The standard model with a frequency range of 1 MHz to 6 GHz and a bandwidth of up to 20 MHz.
+- **Opera Cake for HackRF:** An antenna switching add-on board for HackRF One that is configured with command-line software.
+
+Key features
+------------
+
+- **Frequency Range:** 1 MHz to 6 GHz.
+- **Bandwidth:** 2 MHz to 20 MHz.
+- **Connectivity:** USB 2.0 interface with support for power, data, and firmware updates.
+- **Software Support:** Compatible with GNU Radio, SDR#, and other SDR frameworks.
+- **Onboard Processing:** ARM-based LPC4320 processor for digital signal processing and interfacing over USB.
+
+Hackability
+-----------
+
+.. todo::
+
+ Add information regarding HackRF hackability
+
+Limitations
+-----------
+
+- Bandwidth is limited to 20 MHz.
+- USB 2.0 connectivity might limit data transfer rates compared to USB 3.0 or Ethernet-based SDRs.
+
+Set up instructions (Linux, Radioconda)
+---------------------------------------
+
+1. Activate your Radioconda environment:
+
+ .. code-block:: bash
+
+ conda activate
+
+2. Install the System Package (Ubuntu / Debian):
+
+ .. code-block:: bash
+
+ sudo apt-get update
+ sudo apt-get install hackrf
+
+3. Install a ``udev`` rule by creating a link into your Radioconda installation:
+
+ .. code-block:: bash
+
+ sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/53-hackrf.rules /etc/udev/rules.d/53-radioconda-hackrf.rules
+ sudo udevadm control --reload
+ sudo udevadm trigger
+
+ Make sure your user account belongs to the plugdev group in order to access your device:
+
+ .. code-block:: bash
+
+ sudo usermod -a -G plugdev
+
+.. note::
+
+ You may have to restart your system for changes to take effect.
+
+Further information
+-------------------
+
+- `Official HackRF Website `_
+- `HackRF Project Documentation `_
+- `HackRF Software Installation Guide `_
+- `HackRF GitHub Repository `_
+- `HackRF Setup with Radioconda `_
diff --git a/docs/source/sdr_guides/index.rst b/docs/source/sdr_guides/index.rst
new file mode 100644
index 0000000..552b9b3
--- /dev/null
+++ b/docs/source/sdr_guides/index.rst
@@ -0,0 +1,16 @@
+.. _sdr_guides:
+
+##########
+SDR Guides
+##########
+
+This section contains guides for the various SDR devices supported by the toolkit. Each guide details the supported models,
+their key capabilities and limitations, as well as additional information needed for setup and configuration.
+
+.. toctree::
+ :maxdepth: 2
+
+ USRP
+ BladeRF
+ PlutoSDR
+ HackRF
diff --git a/docs/source/sdr_guides/pluto.rst b/docs/source/sdr_guides/pluto.rst
new file mode 100644
index 0000000..2eaa475
--- /dev/null
+++ b/docs/source/sdr_guides/pluto.rst
@@ -0,0 +1,116 @@
+.. _pluto:
+
+PlutoSDR
+========
+
+The ADALM-PLUTO (PlutoSDR) is a portable and affordable software-defined radio developed by Analog Devices.
+It is designed for learning, experimenting, and prototyping in the field of wireless communication. The PlutoSDR
+is popular among students, educators, and hobbyists due to its versatility and ease of use.
+
+The PlutoSDR is based on the AD9363 transceiver chip, which supports both transmission and reception of signals
+across a wide frequency range. The device is supported by a robust open-source ecosystem, making it ideal for
+hands-on learning and rapid prototyping.
+
+Supported models
+----------------
+
+- **ADALM-PLUTO:** The standard model with a frequency range of 325 MHz to 3.8 GHz and a bandwidth of up to 20 MHz.
+- **Modified ADALM-PLUTO:** Some users modify their PlutoSDR to extend the frequency range to approximately 70 MHz
+ to 6 GHz by applying firmware patches with unqualified RF performance.
+
+Key features
+------------
+
+- **Frequency Range:** 325 MHz to 3.8 GHz (standard), expandable with modifications.
+- **Bandwidth:** Up to 20 MHz, can be increased to 56 MHz with firmware modifications.
+- **Connectivity:** USB 2.0 interface with support for power, data, and firmware updates.
+- **Software Support:** Compatible with GNU Radio, MATLAB, Simulink, and other SDR frameworks.
+- **Onboard Processing:** Integrated ARM Cortex-A9 processor for custom applications and signal processing.
+
+Hackability
+------------
+
+- **Frequency Range and Bandwidth:** The default frequency range of 325 MHz to 3.8 GHz can be expanded to
+ approximately 70 MHz to 6 GHz, and the bandwidth can be increased from 20 MHz to 56 MHz by modifying
+ the device's firmware.
+- **2x2 MIMO:** On Rev C models, users can unlock 2x2 MIMO (Multiple Input Multiple Output) functionality by
+ wiring UFL to SMA connectors to the device's PCB, effectively turning the device into a dual-channel SDR.
+
+Limitations
+-----------
+
+- Bandwidth is limited to 20 MHz by default, but can be increased to 56 MHz with modifications, which may
+ affect stability.
+- USB 2.0 connectivity might limit data transfer rates compared to USB 3.0 or Ethernet-based SDRs.
+
+Set up instructions (Linux, Radioconda)
+---------------------------------------
+
+1. Activate your Radioconda environment:
+
+ .. code-block:: bash
+
+ conda activate
+
+2. Install system dependencies:
+
+ .. code-block:: bash
+
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential \
+ git \
+ libxml2-dev \
+ bison \
+ flex \
+ libcdk5-dev \
+ cmake \
+ libusb-1.0-0-dev \
+ libavahi-client-dev \
+ libavahi-common-dev \
+ libaio-dev
+
+3. Install a ``udev`` rule by creating a link into your Radioconda installation:
+
+ .. code-block:: bash
+
+ sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/90-libiio.rules /etc/udev/rules.d/90-radioconda-libiio.rules
+ sudo udevadm control --reload
+ sudo udevadm trigger
+
+ Once you can talk to the hardware, you may want to perform the post-install steps detailed on the `PlutoSDR Documentation `_.
+
+4. (Optional) Building ``libiio`` or ``libad9361-iio`` from source:
+
+ This step is only required if you want the latest version of these libraries not provided in Radioconda.
+
+ .. code-block:: bash
+
+ # Build libiio from source
+ cd ~
+ git clone --branch v0.23 https://github.com/analogdevicesinc/libiio.git
+ cd libiio
+ mkdir -p build
+ cd build
+ cmake -DPYTHON_BINDINGS=ON ..
+ make -j"$(nproc)"
+ sudo make install
+ sudo ldconfig
+
+ .. code-block:: bash
+
+ # Build libad9361-iio from source
+ cd ~
+ git clone https://github.com/analogdevicesinc/libad9361-iio.git
+ cd libad9361-iio
+ mkdir -p build
+ cd build
+ cmake ..
+ make -j"$(nproc)"
+ sudo make install
+
+Further information
+-------------------
+
+- `PlutoSDR Documentation `_
+- `PlutoSDR Setup with Radioconda `_
\ No newline at end of file
diff --git a/docs/source/sdr_guides/usrp.rst b/docs/source/sdr_guides/usrp.rst
new file mode 100644
index 0000000..e4aa614
--- /dev/null
+++ b/docs/source/sdr_guides/usrp.rst
@@ -0,0 +1,92 @@
+.. _usrp:
+
+USRP
+====
+
+The USRP (Universal Software Radio Peripheral) product line is a series of software-defined radios (SDRs)
+developed by Ettus Research. These devices are widely used in academia, industry, and research for various
+wireless communication applications, ranging from simple experimentation to complex signal processing tasks.
+
+USRP devices offer a flexible platform that can be used with various software frameworks, including GNU Radio
+and the USRP Hardware Driver (UHD). The product line includes both entry-level models for hobbyists and
+advanced models for professional and research use.
+
+Supported models
+----------------
+
+- **USRP B200/B210:** Compact, single-board, full-duplex, with a wide frequency range.
+- **USRP N200/N210:** High-performance models with increased bandwidth and connectivity options.
+- **USRP X300/X310:** High-end models featuring large bandwidth, multiple MIMO channels, and support for GPSDO.
+- **USRP E310/E320:** Embedded devices with onboard processing capabilities.
+- **USRP B200mini:** Ultra-compact model for portable and embedded applications.
+
+Key features
+------------
+
+- **Frequency Range:** Typically covers from DC to 6 GHz, depending on the model and daughter boards used.
+- **Bandwidth:** Varies by model, up to 160 MHz in some high-end versions.
+- **Connectivity:** Includes USB 3.0, Ethernet, and PCIe interfaces depending on the model.
+- **Software Support:** Compatible with UHD, GNU Radio, and other SDR frameworks.
+
+Hackability
+-----------
+
+- The UHD library is fully open source and can be modified to meet user untention.
+- Certain USRP models have "RFNoC" which streamlines the inclusion of custom FPGA processing in a USRP.
+
+Limitations
+-----------
+
+- Some models may have limited bandwidth or processing capabilities.
+- Compatibility with certain software tools may vary depending on the version of the UHD.
+- Price range can be a consideration, especially for high-end models.
+
+Set up instructions (Linux, Radioconda)
+---------------------------------------
+
+1. Activate your Radioconda environment:
+
+ .. code-block:: bash
+
+ conda activate
+
+2. Install UHD and Python bindings:
+
+ .. code-block:: bash
+
+ conda install conda-forge::uhd
+
+3. Download UHD images:
+
+ .. code-block:: bash
+
+ uhd_images_downloader
+
+4. Verify access to your device:
+
+ .. code-block:: bash
+
+ uhd_find_devices
+
+ For USB devices only (e.g. B series), install a ``udev`` rule by creating a link into your Radioconda installation.
+
+ .. code-block:: bash
+
+ sudo ln -s $CONDA_PREFIX/lib/uhd/utils/uhd-usrp.rules /etc/udev/rules.d/radioconda-uhd-usrp.rules
+ sudo udevadm control --reload
+ sudo udevadm trigger
+
+5. (Optional) Update firmware/FPGA images:
+
+ .. code-block:: bash
+
+ uhd_usrp_probe
+
+ This will ensure your device is running the latest firmware and FPGA versions.
+
+Further information
+-------------------
+
+- `Official USRP Website `_
+- `USRP Documentation `_
+- `USRP Setup with Radioconda `_
diff --git a/poetry.lock b/poetry.lock
index 1cdcac7..1717dd8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -154,6 +154,104 @@ files = [
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
]
+[[package]]
+name = "cffi"
+version = "2.0.0"
+description = "Foreign Function Interface for Python calling C code."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+markers = "implementation_name == \"pypy\""
+files = [
+ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
+ {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
+ {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
+ {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
+ {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
+ {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
+ {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
+ {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
+ {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
+ {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
+ {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
+ {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
+ {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
+ {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
+ {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
+ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
+ {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
+]
+
+[package.dependencies]
+pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
+
[[package]]
name = "chardet"
version = "5.2.0"
@@ -877,6 +975,19 @@ files = [
{file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"},
]
+[[package]]
+name = "pycparser"
+version = "2.23"
+description = "C parser in Python"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+markers = "implementation_name == \"pypy\""
+files = [
+ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"},
+ {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"},
+]
+
[[package]]
name = "pyflakes"
version = "3.4.0"
@@ -1005,6 +1116,111 @@ files = [
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
]
+[[package]]
+name = "pyzmq"
+version = "27.1.0"
+description = "Python bindings for 0MQ"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"},
+ {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"},
+ {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"},
+ {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"},
+ {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"},
+ {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"},
+ {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"},
+ {file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"},
+ {file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"},
+ {file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"},
+ {file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"},
+ {file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"},
+ {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"},
+ {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"},
+ {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"},
+ {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"},
+ {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"},
+ {file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"},
+ {file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"},
+ {file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"},
+ {file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"},
+ {file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"},
+ {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"},
+ {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"},
+ {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"},
+ {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"},
+ {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"},
+ {file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"},
+ {file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"},
+ {file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"},
+ {file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"},
+ {file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"},
+ {file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"},
+ {file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"},
+ {file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"},
+ {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"},
+ {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"},
+ {file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"},
+ {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"},
+ {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"},
+ {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"},
+ {file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"},
+ {file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"},
+ {file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"},
+ {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"},
+ {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"},
+ {file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"},
+ {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"},
+ {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"},
+ {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"},
+ {file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"},
+ {file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"},
+ {file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"},
+ {file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"},
+ {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"},
+ {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"},
+ {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"},
+ {file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"},
+ {file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"},
+ {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"},
+ {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"},
+ {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"},
+ {file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"},
+ {file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"},
+ {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"},
+ {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"},
+ {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"},
+ {file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"},
+ {file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"},
+ {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"},
+ {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"},
+ {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"},
+ {file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"},
+ {file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"},
+]
+
+[package.dependencies]
+cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+
[[package]]
name = "quantiphy"
version = "2.20"
@@ -1920,4 +2136,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.10"
-content-hash = "8fafbb6cdc3f1490399a4cea9520376c1743cf30b3430d9c3ad35ff5754bb850"
+content-hash = "546dd85a2ad750359310ff22acfe7bfd3ca764f025d19e3fd48a50cd431e64e5"
diff --git a/pyproject.toml b/pyproject.toml
index 594b724..0d30df7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,6 @@ authors = [
{ name = "Qoherent Inc.", email = "info@qoherent.ai" },
]
maintainers = [
- { name = "Michael Luciuk", email = "michael@qoherent.ai" },
{ name = "Benjamin Chinnery", email = "ben@qoherent.ai" },
{ name = "Ashkan Beigi", email = "ash@qoherent.ai" },
]
@@ -44,7 +43,8 @@ dependencies = [
"quantiphy (>=2.20,<3.0)",
"plotly (>=6.3.0,<7.0.0)",
"h5py (>=3.14.0,<4.0.0)",
- "pandas (>=2.3.2,<3.0.0)"
+ "pandas (>=2.3.2,<3.0.0)",
+ "pyzmq (>=27.1.0,<28.0.0)",
]
[tool.poetry]
@@ -85,7 +85,7 @@ target-version = ["py310"]
exclude = '''
/(
\.git
- | \.github
+ | \.riahub
| \.tox
| build
| dist
@@ -96,6 +96,7 @@ exclude = '''
| \.env
| \.idea
| \.vscode
+ | _external
)/
'''
diff --git a/src/ria_toolkit_oss/datatypes/annotation.py b/src/ria_toolkit_oss/datatypes/annotation.py
index a3aacd2..d565e17 100644
--- a/src/ria_toolkit_oss/datatypes/annotation.py
+++ b/src/ria_toolkit_oss/datatypes/annotation.py
@@ -57,7 +57,7 @@ class Annotation:
def is_valid(self) -> bool:
"""
- Check that the annotation sample count is > 0 and the freq_lower_edge 0`` and the ``freq_lower_edge < freq_upper_edge``.
:returns: True if valid, False if not.
"""
@@ -96,9 +96,9 @@ class Annotation:
def __eq__(self, other: Annotation) -> bool:
return self.__dict__ == other.__dict__
- def to_sigmf_format(self):
+ def to_sigmf_format(self) -> dict:
"""
- Returns a JSON dictionary representing this annotation formatted to be saved in a .sigmf-meta file.
+ Returns a JSON dictionary representation, formatted for saving in a ``.sigmf-meta`` file.
"""
annotation_dict = {SigMFFile.START_INDEX_KEY: self.sample_start, SigMFFile.LENGTH_INDEX_KEY: self.sample_count}
@@ -119,7 +119,8 @@ class Annotation:
def _is_jsonable(x: Any) -> bool:
"""
- :return: True if x is JSON serializable, False otherwise.
+ :return: True if ``x`` is JSON serializable, False otherwise.
+ :rtype: bool
"""
try:
json.dumps(x)
diff --git a/src/ria_toolkit_oss/datatypes/recording.py b/src/ria_toolkit_oss/datatypes/recording.py
index 58ddd49..91dbaf3 100644
--- a/src/ria_toolkit_oss/datatypes/recording.py
+++ b/src/ria_toolkit_oss/datatypes/recording.py
@@ -276,7 +276,13 @@ class Recording:
:return: A new recording with the same metadata and data, with dtype.
- TODO: Add example usage.
+
+ **Examples:**
+
+ .. todo::
+
+ Usage examples coming soon!
+
"""
# Rather than check for a valid datatype, let's cast and check the result. This makes it easier to provide
# cross-platform support where the types are aliased across platforms.
diff --git a/src/ria_toolkit_oss/sdr/__init__.py b/src/ria_toolkit_oss/sdr/__init__.py
new file mode 100644
index 0000000..d89418e
--- /dev/null
+++ b/src/ria_toolkit_oss/sdr/__init__.py
@@ -0,0 +1,9 @@
+"""
+This package provides a unified API for working with a variety of software-defined radios.
+It streamlines tasks involving signal reception and transmission, as well as common administrative
+operations such as detecting and configuring available devices.
+"""
+
+__all__ = ["SDR"]
+
+from .sdr import SDR
diff --git a/src/ria_toolkit_oss/sdr/_external/libhackrf.py b/src/ria_toolkit_oss/sdr/_external/libhackrf.py
new file mode 100644
index 0000000..905ce8b
--- /dev/null
+++ b/src/ria_toolkit_oss/sdr/_external/libhackrf.py
@@ -0,0 +1,714 @@
+# Original work by Dressel, from the pyhackrf project: https://github.com/dressel/pyhackrf
+
+import logging
+import os
+import time
+from ctypes import *
+
+import numpy as np
+
+try:
+ from itertools import izip
+except ImportError:
+ izip = zip
+
+path = os.path.dirname(__file__)
+logging.basicConfig()
+logger = logging.getLogger("HackRf Core")
+logger.setLevel(logging.DEBUG)
+
+# libhackrf = CDLL('/usr/local/lib/libhackrf.so')
+libhackrf = CDLL("libhackrf.so.0")
+
+
+def enum(*sequential, **named):
+ enums = dict(zip(sequential, range(len(sequential))), **named)
+ return type("Enum", (), enums)
+
+
+HackRfVendorRequest = enum(
+ HACKRF_VENDOR_REQUEST_SET_TRANSCEIVER_MODE=1,
+ HACKRF_VENDOR_REQUEST_MAX2837_WRITE=2,
+ HACKRF_VENDOR_REQUEST_MAX2837_READ=3,
+ HACKRF_VENDOR_REQUEST_SI5351C_WRITE=4,
+ HACKRF_VENDOR_REQUEST_SI5351C_READ=5,
+ HACKRF_VENDOR_REQUEST_SAMPLE_RATE_SET=6,
+ HACKRF_VENDOR_REQUEST_BASEBAND_FILTER_BANDWIDTH_SET=7,
+ HACKRF_VENDOR_REQUEST_RFFC5071_WRITE=8,
+ HACKRF_VENDOR_REQUEST_RFFC5071_READ=9,
+ HACKRF_VENDOR_REQUEST_SPIFLASH_ERASE=10,
+ HACKRF_VENDOR_REQUEST_SPIFLASH_WRITE=11,
+ HACKRF_VENDOR_REQUEST_SPIFLASH_READ=12,
+ HACKRF_VENDOR_REQUEST_CPLD_WRITE=13,
+ HACKRF_VENDOR_REQUEST_BOARD_ID_READ=14,
+ HACKRF_VENDOR_REQUEST_VERSION_STRING_READ=15,
+ HACKRF_VENDOR_REQUEST_SET_FREQ=16,
+ HACKRF_VENDOR_REQUEST_AMP_ENABLE=17,
+ HACKRF_VENDOR_REQUEST_BOARD_PARTID_SERIALNO_READ=18,
+ HACKRF_VENDOR_REQUEST_SET_LNA_GAIN=19,
+ HACKRF_VENDOR_REQUEST_SET_VGA_GAIN=20,
+ HACKRF_VENDOR_REQUEST_SET_TXVGA_GAIN=21,
+)
+
+HackRfConstants = enum(
+ LIBUSB_ENDPOINT_IN=0x80,
+ LIBUSB_ENDPOINT_OUT=0x00,
+ HACKRF_DEVICE_OUT=0x40,
+ HACKRF_DEVICE_IN=0xC0,
+ HACKRF_USB_VID=0x1D50,
+ HACKRF_USB_PID=0x6089,
+)
+
+HackRfError = enum(
+ HACKRF_SUCCESS=0,
+ HACKRF_TRUE=1,
+ HACKRF_ERROR_INVALID_PARAM=-2,
+ HACKRF_ERROR_NOT_FOUND=-5,
+ HACKRF_ERROR_BUSY=-6,
+ HACKRF_ERROR_NO_MEM=-11,
+ HACKRF_ERROR_LIBUSB=-1000,
+ HACKRF_ERROR_THREAD=-1001,
+ HACKRF_ERROR_STREAMING_THREAD_ERR=-1002,
+ HACKRF_ERROR_STREAMING_STOPPED=-1003,
+ HACKRF_ERROR_STREAMING_EXIT_CALLED=-1004,
+ HACKRF_ERROR_OTHER=-9999,
+ # Python defaults to returning none
+ HACKRF_ERROR=None,
+)
+
+HackRfTranscieverMode = enum(
+ HACKRF_TRANSCEIVER_MODE_OFF=0, HACKRF_TRANSCEIVER_MODE_RECEIVE=1, HACKRF_TRANSCEIVER_MODE_TRANSMIT=2
+)
+
+# Data structures
+_libusb_device_handle = c_void_p
+_pthread_t = c_ulong
+
+p_hackrf_device = c_void_p
+
+
+class hackrf_transfer(Structure):
+ _fields_ = [
+ ("device", p_hackrf_device),
+ ("buffer", POINTER(c_byte)),
+ ("buffer_length", c_int),
+ ("valid_length", c_int),
+ ("rx_ctx", c_void_p),
+ ("tx_ctx", c_void_p),
+ ]
+
+
+class read_partid_serialno_t(Structure):
+ _fields_ = [("part_id", c_uint32 * 2), ("serial_no", c_uint32 * 4)]
+
+
+class hackrf_device_list_t(Structure):
+ _fields_ = [
+ ("serial_numbers", POINTER(c_char_p)),
+ ("usb_board_ids", c_void_p),
+ ("usb_device_index", POINTER(c_int)),
+ ("devicecount", c_int),
+ ("usb_devices", POINTER(c_void_p)),
+ ("usb_devicecount", c_int),
+ ]
+
+
+#
+# _callback = CFUNCTYPE(c_int, POINTER(hackrf_transfer))
+_callback = CFUNCTYPE(c_int, POINTER(hackrf_transfer))
+
+
+# extern ADDAPI int ADDCALL hackrf_init();
+libhackrf.hackrf_init.restype = c_int
+libhackrf.hackrf_init.argtypes = []
+# extern ADDAPI int ADDCALL hackrf_exit();
+libhackrf.hackrf_exit.restype = c_int
+libhackrf.hackrf_exit.argtypes = []
+# extern ADDAPI int ADDCALL hackrf_open(hackrf_device** device);
+libhackrf.hackrf_open.restype = c_int
+libhackrf.hackrf_open.argtypes = [POINTER(p_hackrf_device)]
+# extern ADDAPI int ADDCALL hackrf_open_by_serial
+# (const char* const desired_serial_number, hackrf_device** device);
+# TODO: check that this one works
+f = libhackrf.hackrf_open_by_serial
+f.restype = c_int
+f.argtypes = [POINTER(p_hackrf_device)]
+
+# extern ADDAPI int ADDCALL hackrf_device_list_open
+# (hackrf_device_list_t *list, int idx, hackrf_device** device);
+f = libhackrf.hackrf_device_list_open
+f.restype = c_int
+f.arg_types = [POINTER(hackrf_device_list_t), c_int, POINTER(p_hackrf_device)]
+# f.arg_types = [hackrf_device_list_t, c_int, POINTER(p_hackrf_device)]
+
+# extern ADDAPI int ADDCALL hackrf_close(hackrf_device* device);
+libhackrf.hackrf_close.restype = c_int
+libhackrf.hackrf_close.argtypes = [p_hackrf_device]
+
+
+# extern ADDAPI int ADDCALL hackrf_set_sample_rate(hackrf_device*
+# device, const double freq_hz);
+libhackrf.hackrf_set_sample_rate.restype = c_int
+libhackrf.hackrf_set_sample_rate.argtypes = [p_hackrf_device, c_double]
+
+# GAIN SETTINGS
+# extern ADDAPI int ADDCALL hackrf_set_amp_enable(hackrf_device*
+# device, const uint8_t value);
+libhackrf.hackrf_set_amp_enable.restype = c_int
+libhackrf.hackrf_set_amp_enable.argtypes = [p_hackrf_device, c_uint8]
+# extern ADDAPI int ADDCALL hackrf_set_lna_gain(hackrf_device* device,
+# uint32_t value);
+libhackrf.hackrf_set_lna_gain.restype = c_int
+libhackrf.hackrf_set_lna_gain.argtypes = [p_hackrf_device, c_uint32]
+# extern ADDAPI int ADDCALL hackrf_set_vga_gain(hackrf_device* device,
+# uint32_t value);
+libhackrf.hackrf_set_vga_gain.restype = c_int
+libhackrf.hackrf_set_vga_gain.argtypes = [p_hackrf_device, c_uint32]
+
+# START AND STOP RX
+# extern ADDAPI int ADDCALL hackrf_start_rx(hackrf_device* device,
+# hackrf_sample_block_cb_fn callback, void* rx_ctx);
+libhackrf.hackrf_start_rx.restype = c_int
+libhackrf.hackrf_start_rx.argtypes = [p_hackrf_device, _callback, c_void_p]
+# extern ADDAPI int ADDCALL hackrf_stop_rx(hackrf_device* device);
+libhackrf.hackrf_stop_rx.restype = c_int
+libhackrf.hackrf_stop_rx.argtypes = [p_hackrf_device]
+
+# extern ADDAPI hackrf_device_list_t* ADDCALL hackrf_device_list();
+f = libhackrf.hackrf_device_list
+f.restype = POINTER(hackrf_device_list_t)
+f.argtypes = []
+
+
+def hackrf_device_list():
+ return libhackrf.hackrf_device_list()
+
+
+# dictionary containing all hackrf_devices in use
+_hackrf_dict = dict()
+
+
+def get_dict():
+ return _hackrf_dict
+
+
+def read_samples_cb(hackrf_transfer):
+
+ # let's access the contents
+ c = hackrf_transfer.contents
+
+ # c.device is an int representing the pointer to the hackrf device
+ # we can get the pointer with p_hackrf_device(c.device)
+ this_hackrf = _hackrf_dict[c.device]
+
+ if len(this_hackrf.buffer) == this_hackrf.num_bytes:
+ this_hackrf.still_sampling = False
+ return 0
+
+ # like == case, but cut down the buffer to size
+ if len(this_hackrf.buffer) > this_hackrf.num_bytes:
+ this_hackrf.still_sampling = False
+ this_hackrf.buffer = this_hackrf.buffer[0 : this_hackrf.num_bytes]
+ return 0
+
+ # grab the buffer data and concatenate it
+ values = cast(c.buffer, POINTER(c_byte * c.buffer_length)).contents
+ this_hackrf.buffer = this_hackrf.buffer + bytearray(values)
+
+ # print("len(bd) = ",len(this_hackrf.buffer))
+
+ return 0
+
+
+rs_callback = _callback(read_samples_cb)
+
+
+## extern ADDAPI int ADDCALL hackrf_start_tx(hackrf_device* device,
+## hackrf_sample_block_cb_fn callback, void* tx_ctx);
+# libhackrf.hackrf_start_tx.restype = c_int
+# libhackrf.hackrf_start_tx.argtypes = [POINTER(hackrf_device), _callback, c_void_p]
+## extern ADDAPI int ADDCALL hackrf_stop_tx(hackrf_device* device);
+# libhackrf.hackrf_stop_tx.restype = c_int
+# libhackrf.hackrf_stop_tx.argtypes = [POINTER(hackrf_device)]
+# extern ADDAPI int ADDCALL hackrf_is_streaming(hackrf_device* device);
+libhackrf.hackrf_is_streaming.restype = c_int
+libhackrf.hackrf_is_streaming.argtypes = [p_hackrf_device]
+## extern ADDAPI int ADDCALL hackrf_max2837_read(hackrf_device* device,
+## uint8_t register_number, uint16_t* value);
+# libhackrf.hackrf_max2837_read.restype = c_int
+# libhackrf.hackrf_max2837_read.argtypes = [
+# POINTER(hackrf_device), c_uint8, POINTER(c_uint16)]
+## extern ADDAPI int ADDCALL hackrf_max2837_write(hackrf_device* device,
+## uint8_t register_number, uint16_t value);
+# libhackrf.hackrf_max2837_write.restype = c_int
+# libhackrf.hackrf_max2837_write.argtypes = [POINTER(hackrf_device), c_uint8, c_uint16]
+## extern ADDAPI int ADDCALL hackrf_si5351c_read(hackrf_device* device,
+## uint16_t register_number, uint16_t* value);
+# libhackrf.hackrf_si5351c_read.restype = c_int
+# libhackrf.hackrf_si5351c_read.argtypes = [
+# POINTER(hackrf_device), c_uint16, POINTER(c_uint16)]
+## extern ADDAPI int ADDCALL hackrf_si5351c_write(hackrf_device* device,
+## uint16_t register_number, uint16_t value);
+# libhackrf.hackrf_si5351c_write.restype = c_int
+# libhackrf.hackrf_si5351c_write.argtypes = [POINTER(hackrf_device), c_uint16, c_uint16]
+## extern ADDAPI int ADDCALL
+## hackrf_set_baseband_filter_bandwidth(hackrf_device* device, const
+## uint32_t bandwidth_hz);
+# libhackrf.hackrf_set_baseband_filter_bandwidth.restype = c_int
+# libhackrf.hackrf_set_baseband_filter_bandwidth.argtypes = [
+# POINTER(hackrf_device), c_uint32]
+## extern ADDAPI int ADDCALL hackrf_rffc5071_read(hackrf_device* device,
+## uint8_t register_number, uint16_t* value);
+# libhackrf.hackrf_rffc5071_read.restype = c_int
+# libhackrf.hackrf_rffc5071_read.argtypes = [
+# POINTER(hackrf_device), c_uint8, POINTER(c_uint16)]
+## extern ADDAPI int ADDCALL hackrf_rffc5071_write(hackrf_device*
+## device, uint8_t register_number, uint16_t value);
+# libhackrf.hackrf_rffc5071_write.restype = c_int
+# libhackrf.hackrf_rffc5071_write.argtypes = [POINTER(hackrf_device), c_uint8, c_uint16]
+## extern ADDAPI int ADDCALL hackrf_spiflash_erase(hackrf_device*
+## device);
+# libhackrf.hackrf_spiflash_erase.restype = c_int
+# libhackrf.hackrf_spiflash_erase.argtypes = [POINTER(hackrf_device)]
+## extern ADDAPI int ADDCALL hackrf_spiflash_write(hackrf_device*
+## device, const uint32_t address, const uint16_t length, unsigned char*
+## const data);
+# libhackrf.hackrf_spiflash_write.restype = c_int
+# libhackrf.hackrf_spiflash_write.argtypes = [
+# POINTER(hackrf_device), c_uint32, c_uint16, POINTER(c_ubyte)]
+## extern ADDAPI int ADDCALL hackrf_spiflash_read(hackrf_device* device,
+## const uint32_t address, const uint16_t length, unsigned char* data);
+# libhackrf.hackrf_spiflash_read.restype = c_int
+# libhackrf.hackrf_spiflash_read.argtypes = [
+# POINTER(hackrf_device), c_uint32, c_uint16, POINTER(c_ubyte)]
+## extern ADDAPI int ADDCALL hackrf_cpld_write(hackrf_device* device,
+## unsigned char* const data, const unsigned int total_length);
+# libhackrf.hackrf_cpld_write.restype = c_int
+# libhackrf.hackrf_cpld_write.argtypes = [POINTER(hackrf_device), POINTER(c_ubyte), c_uint]
+## extern ADDAPI int ADDCALL hackrf_board_id_read(hackrf_device* device,
+## uint8_t* value);
+# libhackrf.hackrf_board_id_read.restype = c_int
+# libhackrf.hackrf_board_id_read.argtypes = [POINTER(hackrf_device), POINTER(c_uint8)]
+## extern ADDAPI int ADDCALL hackrf_version_string_read(hackrf_device*
+## device, char* version, uint8_t length);
+# libhackrf.hackrf_version_string_read.restype = c_int
+# libhackrf.hackrf_version_string_read.argtypes = [POINTER(hackrf_device), POINTER(c_char), c_uint8]
+# extern ADDAPI int ADDCALL hackrf_set_freq(hackrf_device* device,
+# const uint64_t freq_hz);
+libhackrf.hackrf_set_freq.restype = c_int
+libhackrf.hackrf_set_freq.argtypes = [p_hackrf_device, c_uint64]
+#
+## extern ADDAPI int ADDCALL hackrf_set_freq_explicit(hackrf_device* device,
+## const uint64_t if_freq_hz, const uint64_t lo_freq_hz,
+## const enum rf_path_filter path);,
+## libhackrf.hackrf_set_freq_explicit.restype = c_int
+## libhackrf.hackrf_set_freq_explicit.argtypes = [c_uint64,
+## c_uint64, ]
+#
+## extern ADDAPI int ADDCALL
+## hackrf_set_sample_rate_manual(hackrf_device* device, const uint32_t
+## freq_hz, const uint32_t divider);
+# libhackrf.hackrf_set_sample_rate_manual.restype = c_int
+# libhackrf.hackrf_set_sample_rate_manual.argtypes = [
+# POINTER(hackrf_device), c_uint32, c_uint32]
+#
+# extern ADDAPI int ADDCALL
+# hackrf_board_partid_serialno_read(hackrf_device* device,
+# read_partid_serialno_t* read_partid_serialno);
+f = libhackrf.hackrf_board_partid_serialno_read
+f.restype = c_int
+f.argtypes = [p_hackrf_device, POINTER(read_partid_serialno_t)]
+
+## extern ADDAPI int ADDCALL hackrf_set_txvga_gain(hackrf_device*
+## device, uint32_t value);
+# libhackrf.hackrf_set_txvga_gain.restype = c_int
+# libhackrf.hackrf_set_txvga_gain.argtypes = [POINTER(hackrf_device), c_uint32]
+## extern ADDAPI int ADDCALL hackrf_set_antenna_enable(hackrf_device*
+## device, const uint8_t value);
+# libhackrf.hackrf_set_antenna_enable.restype = c_int
+# libhackrf.hackrf_set_antenna_enable.argtypes = [POINTER(hackrf_device), c_uint8]
+#
+## extern ADDAPI const char* ADDCALL hackrf_error_name(enum hackrf_error errcode);
+## libhackrf.hackrf_error_name.restype = POINTER(c_char)
+## libhackrf.hackrf_error_name.argtypes = []
+#
+## extern ADDAPI const char* ADDCALL hackrf_board_id_name(enum hackrf_board_id board_id);
+## libhackrf.hackrf_board_id_name.restype = POINTER(c_char)
+## libhackrf.hackrf_board_id_name.argtypes = []
+#
+## extern ADDAPI const char* ADDCALL hackrf_filter_path_name(const enum rf_path_filter path);
+## libhackrf.hackrf_filter_path_name.restype = POINTER(c_char)
+## libhackrf.hackrf_filter_path_name.argtypes = []
+#
+
+libhackrf.hackrf_si5351c_write.restype = c_int
+libhackrf.hackrf_si5351c_write.argtypes = [POINTER(p_hackrf_device), c_uint16, c_uint16]
+
+PLL_SOURCE_CLKIN = 0x01
+PLL_SOURCE_INTERNAL = 0x00
+
+# Callback type for transmit
+_tx_callback = CFUNCTYPE(c_int, POINTER(hackrf_transfer))
+
+# extern ADDAPI int ADDCALL hackrf_start_tx(hackrf_device* device, hackrf_sample_block_cb_fn callback, void* tx_ctx);
+libhackrf.hackrf_start_tx.restype = c_int
+libhackrf.hackrf_start_tx.argtypes = [p_hackrf_device, _tx_callback, c_void_p]
+
+# extern ADDAPI int ADDCALL hackrf_stop_tx(hackrf_device* device);
+libhackrf.hackrf_stop_tx.restype = c_int
+libhackrf.hackrf_stop_tx.argtypes = [p_hackrf_device]
+
+# Gain settings for transmit
+# extern ADDAPI int ADDCALL hackrf_set_txvga_gain(hackrf_device* device, uint32_t value);
+libhackrf.hackrf_set_txvga_gain.restype = c_int
+libhackrf.hackrf_set_txvga_gain.argtypes = [p_hackrf_device, c_uint32]
+
+
+# Helper function to convert complex64 samples to bytes
+def iq2bytes(samples):
+ """
+ Convert complex64 samples to interleaved int8 bytes for HackRF transmission.
+
+ :param samples: NumPy array of complex64 samples.
+ :return: Bytes object containing interleaved I/Q samples as int8.
+ """
+ # Normalize samples to the range -1 to +1
+ samples = samples / np.max(np.abs(samples))
+
+ # Scale to the range -127 to +127
+ samples_scaled = samples * 127.0
+
+ # Ensure the values are within the int8 range
+ samples_scaled = np.clip(samples_scaled, -127, 127)
+
+ # Separate real and imaginary parts and convert to int8
+ i_samples = np.real(samples_scaled).astype(np.int8)
+ q_samples = np.imag(samples_scaled).astype(np.int8)
+
+ # Interleave I and Q samples
+ interleaved = np.empty(i_samples.size + q_samples.size, dtype=np.int8)
+ interleaved[0::2] = i_samples
+ interleaved[1::2] = q_samples
+
+ return interleaved.tobytes()
+
+
+# Helper function to get error names
+def get_error_name(error_code):
+ libhackrf.hackrf_error_name.restype = c_char_p
+ libhackrf.hackrf_error_name.argtypes = [c_int]
+ return libhackrf.hackrf_error_name(error_code).decode("utf-8")
+
+
+class HackRF(object):
+
+ _center_freq = 100e6
+ _sample_rate = 20e6
+ device_opened = False
+
+ def __init__(self, device_index=0):
+ self.open(device_index)
+
+ # TODO: initialize defaults here
+ self.disable_amp()
+ self.set_lna_gain(16)
+ self.set_vga_gain(16)
+ self.set_txvga_gain(0)
+
+ self.active_clock_source = None
+
+ self.buffer = bytearray()
+ self.num_bytes = 16 * 262144
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+ def open(self, device_index=0):
+
+ # pointer to device structure
+ self.dev_p = p_hackrf_device(None)
+
+ hdl = hackrf_device_list()
+ result = libhackrf.hackrf_device_list_open(hdl, device_index, pointer(self.dev_p))
+ if result != 0:
+ raise IOError("Error code %d when opening HackRF" % (result))
+
+ # This is how I used to do it...
+ # Note I only pass in the dev_p here, but it worked.
+ # But above, I have to pass in a pointer(self.dev_p)
+ # They should both take the same thing
+ # result = libhackrf.hackrf_open(self.dev_p)
+ # if result != 0:
+ # raise IOError('Error code %d when opening HackRF' % (result))
+
+ # self.dev_p.value returns the integer value of the pointer
+
+ _hackrf_dict[self.dev_p.value] = self
+ # print("self.dev_p.value = ", self.dev_p.value)
+
+ self.device_opened = True
+
+ def close(self):
+ if not self.device_opened:
+ return
+
+ libhackrf.hackrf_close(self.dev_p)
+ self.device_opened = False
+
+ def __del__(self):
+ print("del function is being called")
+ self.close()
+
+ # sleep_time in seconds
+ # I used to have just pass in the while loop
+ def read_samples(self, num_samples=131072, sleep_time=0.05):
+
+ num_bytes = 2 * num_samples
+ self.num_bytes = int(num_bytes)
+
+ self.buffer = bytearray()
+
+ # start receiving
+ result = libhackrf.hackrf_start_rx(self.dev_p, rs_callback, None)
+ if result != 0:
+ raise IOError("Error in hackrf_start_rx")
+ self.still_sampling = True # this does get called
+
+ while self.still_sampling:
+ if sleep_time:
+ time.sleep(sleep_time)
+
+ # stop receiving
+ result = libhackrf.hackrf_stop_rx(self.dev_p)
+ if result != 0:
+ raise IOError("Error in hackrf_stop_rx")
+
+ # convert samples to iq
+ iq = bytes2iq(self.buffer)
+
+ return iq
+
+ # setting the center frequency
+ def set_freq(self, freq):
+ freq = int(freq)
+ result = libhackrf.hackrf_set_freq(self.dev_p, freq)
+ if result != 0:
+ raise IOError("Error code %d when setting frequency to %d Hz" % (result, freq))
+
+ self._center_freq = freq
+ return
+
+ def get_freq(self):
+ return self._center_freq
+
+ center_freq = property(get_freq, set_freq)
+
+ # sample rate
+ def set_sample_rate(self, rate):
+ result = libhackrf.hackrf_set_sample_rate(self.dev_p, rate)
+ if result != 0:
+ # TODO: make this error message better
+ raise IOError("Sample rate set failure")
+ self._sample_rate = rate
+ return
+
+ def get_sample_rate(self):
+ return self._sample_rate
+
+ sample_rate = property(get_sample_rate, set_sample_rate)
+
+ def get_serial_no(self):
+ return get_serial_no(self.dev_p)
+
+ def enable_amp(self):
+ result = libhackrf.hackrf_set_amp_enable(self.dev_p, 1)
+ if result != 0:
+ # TODO: make this a better message
+ raise IOError("error enabling amp")
+ return 0
+
+ def disable_amp(self):
+ result = libhackrf.hackrf_set_amp_enable(self.dev_p, 0)
+ if result != 0:
+ # TODO: make this a better message
+ raise IOError("error disabling amp")
+ return 0
+
+ # rounds down to multiple of 8 (15 -> 8, 39 -> 32), etc.
+ # internally, hackrf_set_lna_gain does the same thing
+ # But we take care of it so we can keep track of the correct gain
+ def set_lna_gain(self, gain):
+ gain -= gain % 8 # round DOWN to multiple of 8
+ result = libhackrf.hackrf_set_lna_gain(self.dev_p, gain)
+ if result != 0:
+ # TODO: make this a better message
+ raise IOError("error setting lna gain")
+ self._lna_gain = gain
+ print("LNA gain set to", gain, "dB.")
+ return 0
+
+ def get_lna_gain(self):
+ return self._lna_gain
+
+ lna_gain = property(get_lna_gain, set_lna_gain)
+
+ def set_vga_gain(self, gain):
+ gain -= gain % 2
+ result = libhackrf.hackrf_set_vga_gain(self.dev_p, gain)
+ if result != 0:
+ # TODO: make this a better message
+ raise IOError("error setting vga gain")
+ self._vga_gain = gain
+ print("VGA gain set to", gain, "dB.")
+ return 0
+
+ def get_vga_gain(self):
+ return self._vga_gain
+
+ vga_gain = property(get_vga_gain, set_vga_gain)
+
+ # rx_cb_fn is a callback function (in python)
+ def start_rx(self, rx_cb_fn):
+ rx_cb = _callback(rx_cb_fn)
+ result = libhackrf.hackrf_start_rx(self.dev_p, rx_cb, None)
+ if result != 0:
+ raise IOError("start_rx failure")
+
+ def stop_rx(self):
+ result = libhackrf.hackrf_stop_rx(self.dev_p)
+ if result != 0:
+ raise IOError("stop_rx failure")
+
+ # Add transmit gain property
+ def set_txvga_gain(self, gain):
+ if gain < 0 or gain > 47:
+ raise ValueError("TXVGA gain must be between 0 and 47 dB")
+ result = libhackrf.hackrf_set_txvga_gain(self.dev_p, gain)
+ if result != 0:
+ error_name = get_error_name(result)
+ raise IOError(f"Error setting TXVGA gain: {error_name} (Code {result})")
+ self._txvga_gain = gain
+ print(f"TXVGA gain set to {gain} dB.")
+ return 0
+
+ def get_txvga_gain(self):
+ return self._txvga_gain
+
+ txvga_gain = property(get_txvga_gain, set_txvga_gain)
+
+ # Method to start transmission
+ def start_tx(self, samples, repeat=False):
+ """
+ Start transmitting samples.
+
+ :param samples: A numpy array of complex64 samples to transmit.
+ :param repeat: If True, the samples will be transmitted in a loop.
+ """
+ if not isinstance(samples, np.ndarray) or samples.dtype != np.complex64:
+ raise ValueError("Samples must be a numpy array of complex64")
+
+ # Scale samples to int8 range
+ if np.max(samples) > 1:
+ raise ValueError
+ samples_scaled = samples * 127.0
+ samples_scaled = np.clip(samples_scaled, -127, 127) # Ensure values are within int8 range
+ samples_scaled = samples_scaled.astype(np.complex64) # Ensure correct data type
+
+ self.tx_buffer = samples
+ self.tx_repeat = repeat
+ self.tx_index = 0 # Index to keep track of where we are in the buffer
+
+ # Convert the callback function to the required C callback
+ self._tx_cb = _tx_callback(self._tx_callback)
+ result = libhackrf.hackrf_start_tx(self.dev_p, self._tx_cb, None)
+ if result != 0:
+ error_name = get_error_name(result)
+ raise IOError(f"Error starting transmission: {error_name} (Code {result})")
+
+ # Method to stop transmission
+ def stop_tx(self):
+ result = libhackrf.hackrf_stop_tx(self.dev_p)
+ if result != 0:
+ error_name = get_error_name(result)
+ raise IOError(f"Error stopping transmission: {error_name} (Code {result})")
+
+ # The transmit callback function
+ def _tx_callback(self, hackrf_transfer):
+ c = hackrf_transfer.contents
+
+ # Determine how many bytes we need to send
+ bytes_to_send = c.valid_length
+
+ # Prepare the data to send
+ end_index = self.tx_index + bytes_to_send // 2 # Each sample is 2 bytes (I and Q)
+
+ if end_index > len(self.tx_buffer):
+ if self.tx_repeat:
+ # Loop back to the start
+ end_index = end_index % len(self.tx_buffer)
+ data = np.concatenate((self.tx_buffer[self.tx_index :], self.tx_buffer[:end_index]))
+ self.tx_index = end_index
+ else:
+ # Fill the remaining buffer with zeros
+ data = self.tx_buffer[self.tx_index :]
+ padding = np.zeros(end_index - len(self.tx_buffer), dtype=np.complex64)
+ data = np.concatenate((data, padding))
+ self.tx_index = len(self.tx_buffer)
+ else:
+ data = self.tx_buffer[self.tx_index : end_index]
+ self.tx_index = end_index
+
+ # Convert complex64 samples to bytes
+ iq_bytes = iq2bytes(data)
+
+ # Copy data to the buffer
+ memmove(c.buffer, iq_bytes, len(iq_bytes))
+
+ # If we've reached the end of the buffer and not repeating, stop transmission
+ if self.tx_index >= len(self.tx_buffer) and not self.tx_repeat:
+ return -1 # Returning -1 stops the transmission
+
+ return 0 # Continue transmission
+
+ def set_clock_source(self, source_str):
+ pass
+
+
+# returns serial number as a string
+# it is too big to be a single number, so make it a string
+# the returned string matches the hackrf_info output
+def get_serial_no(dev_p):
+ sn = read_partid_serialno_t()
+ result = libhackrf.hackrf_board_partid_serialno_read(dev_p, sn)
+ if result != 0:
+ raise IOError("Error %d while getting serial number" % (result))
+
+ # convert the serial number to a string
+ sn_str = ""
+ for i in range(0, 4):
+ sni = sn.serial_no[i]
+ if sni == 0:
+ sn_str += "00000000"
+ else:
+ sn_str += hex(sni)[2:-1]
+
+ return sn_str
+
+
+# converts byte array to iq values
+def bytes2iq(data):
+ values = np.array(data).astype(np.int8)
+ iq = values.astype(np.float64).view(np.complex128)
+ iq /= 127.5
+ iq -= 1 + 1j
+
+ return iq
+
+
+# really, user shouldn't have to call this function at all
+result = libhackrf.hackrf_init()
+if result != 0:
+ print("error initializing the hackrf library")
diff --git a/src/ria_toolkit_oss/sdr/blade.py b/src/ria_toolkit_oss/sdr/blade.py
new file mode 100644
index 0000000..02fa8ca
--- /dev/null
+++ b/src/ria_toolkit_oss/sdr/blade.py
@@ -0,0 +1,383 @@
+from typing import Optional
+
+import numpy as np
+from bladerf import _bladerf
+
+from ria_toolkit_oss.datatypes import Recording
+from ria_toolkit_oss.sdr import SDR
+
+
+class Blade(SDR):
+
+ def __init__(self, identifier=""):
+ """
+ Initialize a BladeRF device object and connect to the SDR hardware.
+
+ :param identifier: Not used for BladeRF.
+
+ BladeRF devices cannot currently be selected with and identifier value.
+ If there are multiple connected devices, the device in use may be selected randomly.
+ """
+
+ if identifier != "":
+ print(f"Warning, radio identifier {identifier} provided for Blade but will not be used.")
+
+ uut = self._probe_bladerf()
+
+ if uut is None:
+ print("No bladeRFs detected. Exiting.")
+ self._shutdown(error=-1, board=None)
+
+ print(uut)
+
+ self.device = _bladerf.BladeRF(uut)
+ self._print_versions(device=self.device)
+
+ super().__init__()
+
+ def _shutdown(self, error=0, board=None):
+ print("Shutting down with error code: " + str(error))
+ if board is not None:
+ board.close()
+
+ # TODO why does this create an error under any conditions?
+ raise OSError("Shutdown initiated with error code: {}".format(error))
+
+ def _probe_bladerf(self):
+ device = None
+ print("Searching for bladeRF devices...")
+ try:
+ devinfos = _bladerf.get_device_list()
+ if len(devinfos) == 1:
+ device = "{backend}:device={usb_bus}:{usb_addr}".format(**devinfos[0]._asdict())
+ print("Found bladeRF device: " + str(device))
+ if len(devinfos) > 1:
+ print("Unsupported feature: more than one bladeRFs detected.")
+ print("\n".join([str(devinfo) for devinfo in devinfos]))
+ self._shutdown(error=-1, board=None)
+ except _bladerf.BladeRFError:
+ print("No bladeRF devices found.")
+ pass
+ return device
+
+ def _print_versions(self, device=None):
+ print("libbladeRF version:\t" + str(_bladerf.version()))
+ if device is not None:
+ print("Firmware version:\t" + str(device.get_fw_version()))
+ print("FPGA version:\t\t" + str(device.get_fpga_version()))
+ return 0
+
+ def close(self):
+ self.device.close()
+
+ def init_rx(
+ self,
+ sample_rate: int | float,
+ center_frequency: int | float,
+ gain: int,
+ channel: int,
+ buffer_size: Optional[int] = 8192,
+ gain_mode: Optional[str] = "absolute",
+ ):
+ """
+ Initializes the BladeRF for receiving.
+
+ :param sample_rate: The sample rate for receiving.
+ :type sample_rate: int or float
+ :param center_frequency: The center frequency of the recording.
+ :type center_frequency: int or float
+ :param gain: The gain set for receiving on the BladeRF
+ :type gain: int
+ :param channel: The channel the BladeRF is set to.
+ :type channel: int
+ :param buffer_size: The buffer size during receive. Defaults to 8192.
+ :type buffer_size: int
+ """
+ print("Initializing RX")
+
+ # Configure BladeRF
+ self._set_rx_channel(channel)
+ self._set_rx_sample_rate(sample_rate)
+ self._set_rx_center_frequency(center_frequency)
+ self._set_rx_gain(channel, gain, gain_mode)
+ self._set_rx_buffer_size(buffer_size)
+
+ bw = self.rx_sample_rate
+ if bw < 200000:
+ bw = 200000
+ elif bw > 56000000:
+ bw = 56000000
+ self.rx_ch.bandwidth = bw
+
+ self._rx_initialized = True
+ self._tx_initialized = False
+
+ def init_tx(
+ self,
+ sample_rate: int | float,
+ center_frequency: int | float,
+ gain: int,
+ channel: int,
+ buffer_size: Optional[int] = 8192,
+ gain_mode: Optional[str] = "absolute",
+ ):
+ """
+ Initializes the BladeRF for transmitting.
+
+ :param sample_rate: The sample rate for transmitting.
+ :type sample_rate: int or float
+ :param center_frequency: The center frequency of the recording.
+ :type center_frequency: int or float
+ :param gain: The gain set for transmitting on the BladeRF
+ :type gain: int
+ :param channel: The channel the BladeRF is set to.
+ :type channel: int
+ :param buffer_size: The buffer size during transmission. Defaults to 8192.
+ :type buffer_size: int
+ """
+
+ # Configure BladeRF
+ self._set_tx_channel(channel)
+ self._set_tx_sample_rate(sample_rate)
+ self._set_tx_center_frequency(center_frequency)
+ self._set_tx_gain(channel=channel, gain=gain, gain_mode=gain_mode)
+ self._set_tx_buffer_size(buffer_size)
+
+ bw = self.tx_sample_rate
+ if bw < 200000:
+ bw = 200000
+ elif bw > 56000000:
+ bw = 56000000
+ self.tx_ch.bandwidth = bw
+
+ if self.device is None:
+ print("TX: Invalid device handle.")
+ return -1
+
+ if self.tx_channel is None:
+ print("TX: Invalid channel.")
+ return -1
+
+ self._tx_initialized = True
+ self._rx_initialized = False
+ return 0
+
+ def _stream_rx(self, callback):
+ if not self._rx_initialized:
+ raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
+
+ # Setup synchronous stream
+ self.device.sync_config(
+ layout=_bladerf.ChannelLayout.RX_X1,
+ fmt=_bladerf.Format.SC16_Q11,
+ num_buffers=16,
+ buffer_size=self.rx_buffer_size,
+ num_transfers=8,
+ stream_timeout=3500000000,
+ )
+
+ self.rx_ch.enable = True
+ self.bytes_per_sample = 4
+
+ print("Blade Starting RX...")
+ self._enable_rx = True
+
+ while self._enable_rx:
+ # Create receive buffer and read in samples to buffer
+ # Add them to a list to convert and save after stream is finished
+ buffer = bytearray(self.rx_buffer_size * self.bytes_per_sample)
+ self.device.sync_rx(buffer, self.rx_buffer_size)
+ signal = self._convert_rx_samples(buffer)
+ # samples = convert_to_2xn(signal)
+ self.buffer = buffer
+ # send callback complex signal
+ callback(buffer=signal, metadata=None)
+
+ # Disable module
+ print("Blade RX Completed.")
+ self.rx_ch.enable = False
+
+ def record(self, num_samples):
+ if not self._rx_initialized:
+ raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
+
+ # Setup synchronous stream
+ self.device.sync_config(
+ layout=_bladerf.ChannelLayout.RX_X1,
+ fmt=_bladerf.Format.SC16_Q11,
+ num_buffers=16,
+ buffer_size=self.rx_buffer_size,
+ num_transfers=8,
+ stream_timeout=3500000000,
+ )
+
+ self.rx_ch.enable = True
+ self.bytes_per_sample = 4
+
+ print("Blade Starting RX...")
+ self._enable_rx = True
+
+ store_array = np.zeros((1, (num_samples // self.rx_buffer_size + 1) * self.rx_buffer_size), dtype=np.complex64)
+
+ for i in range(num_samples // self.rx_buffer_size + 1):
+ # Create receive buffer and read in samples to buffer
+ # Add them to a list to convert and save after stream is finished
+ buffer = bytearray(self.rx_buffer_size * self.bytes_per_sample)
+ self.device.sync_rx(buffer, self.rx_buffer_size)
+ signal = self._convert_rx_samples(buffer)
+ # samples = convert_to_2xn(signal)
+ store_array[:, i * self.rx_buffer_size : (i + 1) * self.rx_buffer_size] = signal
+
+ # Disable module
+ print("Blade RX Completed.")
+ self.rx_ch.enable = False
+ metadata = {
+ "source": self.__class__.__name__,
+ "sample_rate": self.rx_sample_rate,
+ "center_frequency": self.rx_center_frequency,
+ "gain": self.rx_gain,
+ }
+
+ return Recording(data=store_array[:, :num_samples], metadata=metadata)
+
+ def _stream_tx(self, callback):
+
+ # Setup stream
+ self.device.sync_config(
+ layout=_bladerf.ChannelLayout.TX_X1,
+ fmt=_bladerf.Format.SC16_Q11,
+ num_buffers=16,
+ buffer_size=8192,
+ num_transfers=8,
+ stream_timeout=3500,
+ )
+
+ # Enable module
+ self.tx_ch.enable = True
+ self._enable_tx = True
+
+ print("Blade Starting TX...")
+
+ while self._enable_tx:
+ buffer = callback(self.tx_buffer_size) # [0]
+ byte_array = self._convert_tx_samples(buffer)
+ self.device.sync_tx(byte_array, len(buffer))
+
+ # Disable module
+ print("Blade TX Completed.")
+ self.tx_ch.enable = False
+
+ def _convert_rx_samples(self, samples):
+ samples = np.frombuffer(samples, dtype=np.int16).astype(np.float32)
+ samples /= 2048
+ samples = samples[::2] + 1j * samples[1::2]
+ return samples
+
+ def _convert_tx_samples(self, samples):
+ tx_samples = np.empty(samples.size * 2, dtype=np.float32)
+ tx_samples[::2] = np.real(samples) # Real part
+ tx_samples[1::2] = np.imag(samples) # Imaginary part
+
+ tx_samples *= 2048
+ tx_samples = tx_samples.astype(np.int16)
+ byte_array = tx_samples.tobytes()
+
+ return byte_array
+
+ def _set_rx_channel(self, channel):
+ self.rx_channel = channel
+ self.rx_ch = self.device.Channel(_bladerf.CHANNEL_RX(channel))
+ print(f"\nBlade channel = {self.rx_ch}")
+
+ def _set_rx_sample_rate(self, sample_rate):
+ self.rx_sample_rate = sample_rate
+ self.rx_ch.sample_rate = self.rx_sample_rate
+ print(f"Blade sample rate = {self.rx_ch.sample_rate}")
+
+ def _set_rx_center_frequency(self, center_frequency):
+ self.rx_center_frequency = center_frequency
+ self.rx_ch.frequency = center_frequency
+ print(f"Blade center frequency = {self.rx_ch.frequency}")
+
+ def _set_rx_gain(self, channel, gain, gain_mode):
+
+ rx_gain_min = self.device.get_gain_range(channel)[0]
+ rx_gain_max = self.device.get_gain_range(channel)[1]
+
+ if gain_mode == "relative":
+ if gain > 0:
+ raise ValueError(
+ "When gain_mode = 'relative', gain must be < 0. This sets \
+ the gain relative to the maximum possible gain."
+ )
+ else:
+ abs_gain = rx_gain_max + gain
+ else:
+ abs_gain = gain
+
+ if abs_gain < rx_gain_min or abs_gain > rx_gain_max:
+ abs_gain = min(max(gain, rx_gain_min), rx_gain_max)
+ print(f"Gain {abs_gain} out of range for Blade.")
+ print(f"Gain range: {rx_gain_min} to {rx_gain_max} dB")
+
+ self.rx_gain = abs_gain
+ self.rx_ch.gain = abs_gain
+
+ print(f"Blade gain = {self.rx_ch.gain}")
+
+ def _set_rx_buffer_size(self, buffer_size):
+ self.rx_buffer_size = buffer_size
+
+ def _set_tx_channel(self, channel):
+ self.tx_channel = channel
+ self.tx_ch = self.device.Channel(_bladerf.CHANNEL_TX(self.tx_channel))
+ print(f"\nBlade channel = {self.tx_ch}")
+
+ def _set_tx_sample_rate(self, sample_rate):
+ self.tx_sample_rate = sample_rate
+ self.tx_ch.sample_rate = self.tx_sample_rate
+ print(f"Blade sample rate = {self.tx_ch.sample_rate}")
+
+ def _set_tx_center_frequency(self, center_frequency):
+ self.tx_center_frequency = center_frequency
+ self.tx_ch.frequency = center_frequency
+ print(f"Blade center frequency = {self.tx_ch.frequency}")
+
+ def _set_tx_gain(self, channel, gain, gain_mode):
+
+ tx_gain_min = self.device.get_gain_range(channel)[0]
+ tx_gain_max = self.device.get_gain_range(channel)[1]
+
+ if gain_mode == "relative":
+ if gain > 0:
+ raise ValueError(
+ "When gain_mode = 'relative', gain must be < 0. This sets\
+ the gain relative to the maximum possible gain."
+ )
+ else:
+ abs_gain = tx_gain_max + gain
+ else:
+ abs_gain = gain
+
+ if abs_gain < tx_gain_min or abs_gain > tx_gain_max:
+ abs_gain = min(max(gain, tx_gain_min), tx_gain_max)
+ print(f"Gain {abs_gain} out of range for Blade.")
+ print(f"Gain range: {tx_gain_min} to {tx_gain_max} dB")
+
+ self.tx_gain = abs_gain
+ self.tx_ch.gain = abs_gain
+
+ print(f"Blade gain = {self.tx_ch.gain}")
+
+ def _set_tx_buffer_size(self, buffer_size):
+ self.tx_buffer_size = buffer_size
+
+ def set_clock_source(self, source):
+ if source.lower() == "external":
+ self.device.set_pll_enable(True)
+ elif source.lower() == "internal":
+ print("Disabling PLL")
+ self.device.set_pll_enable(False)
+
+ print(f"Clock source set to {self.device.get_clock_select()}")
+ print(f"PLL Reference set to {self.device.get_pll_refclk()}")
diff --git a/src/ria_toolkit_oss/sdr/hackrf.py b/src/ria_toolkit_oss/sdr/hackrf.py
new file mode 100644
index 0000000..91a84ef
--- /dev/null
+++ b/src/ria_toolkit_oss/sdr/hackrf.py
@@ -0,0 +1,160 @@
+import time
+import warnings
+from typing import Optional
+
+import numpy as np
+
+from ria_toolkit_oss.datatypes.recording import Recording
+from ria_toolkit_oss.sdr._external.libhackrf import HackRF as hrf
+from ria_toolkit_oss.sdr.sdr import SDR
+
+
+class HackRF(SDR):
+ def __init__(self, identifier=""):
+ """
+ Initialize a HackRF device object and connect to the SDR hardware.
+
+ :param identifier: Not used for HackRF.
+
+ HackRF devices cannot currently be selected with and identifier value.
+ If there are multiple connected devices, the device in use may be selected randomly.
+ """
+
+ if identifier != "":
+ print(f"Warning, radio identifier {identifier} provided for HackRF but will not be used.")
+
+ print("Initializing HackRF radio.")
+ try:
+ super().__init__()
+
+ self.radio = hrf()
+ print("Successfully found HackRF radio.")
+ except Exception as e:
+ print("Failed to find HackRF radio.")
+ raise e
+
+ super().__init__()
+
+ def init_rx(self, sample_rate, center_frequency, gain, channel, gain_mode):
+ self._tx_initialized = False
+ self._rx_initialized = True
+ return NotImplementedError("RX not yet implemented for HackRF")
+
+ def init_tx(
+ self,
+ sample_rate: int | float,
+ center_frequency: int | float,
+ gain: int,
+ channel: int,
+ gain_mode: Optional[str] = "absolute",
+ ):
+ """
+ Initializes the HackRF for transmitting.
+
+ :param sample_rate: The sample rate for transmitting.
+ :type sample_rate: int or float
+ :param center_frequency: The center frequency of the recording.
+ :type center_frequency: int or float
+ :param gain: The gain set for transmitting on the HackRF
+ :type gain: int
+ :param channel: The channel the HackRF is set to. (Not actually used)
+ :type channel: int
+ :param buffer_size: The buffer size during transmit. Defaults to 10000.
+ :type buffer_size: int
+ """
+
+ print("Initializing TX")
+ self.tx_sample_rate = sample_rate
+ self.radio.sample_rate = int(sample_rate)
+ print(f"HackRF sample rate = {self.radio.sample_rate}")
+
+ self.tx_center_frequency = center_frequency
+ self.radio.center_freq = int(center_frequency)
+ print(f"HackRF center frequency = {self.radio.center_freq}")
+
+ self.radio.enable_amp()
+
+ tx_gain_min = 0
+ tx_gain_max = 47
+ if gain_mode == "relative":
+ if gain > 0:
+ raise ValueError(
+ "When gain_mode = 'relative', gain must be < 0. This \
+ sets the gain relative to the maximum possible gain."
+ )
+ else:
+ abs_gain = tx_gain_max + gain
+ else:
+ abs_gain = gain
+
+ if abs_gain < tx_gain_min or abs_gain > tx_gain_max:
+ abs_gain = min(max(gain, tx_gain_min), tx_gain_max)
+ print(f"Gain {gain} out of range for Pluto.")
+ print(f"Gain range: {tx_gain_min} to {tx_gain_max} dB")
+
+ self.radio.txvga_gain = abs_gain
+ print(f"HackRF gain = {self.radio.txvga_gain}")
+
+ self._tx_initialized = True
+ self._rx_initialized = False
+
+ def tx_recording(
+ self,
+ recording: Recording | np.ndarray,
+ num_samples: Optional[int] = None,
+ tx_time: Optional[int | float] = None,
+ ):
+ """
+ Transmit the given iq samples from the provided recording.
+ init_tx() must be called before this function.
+
+ :param recording: The recording to transmit.
+ :type recording: Recording or np.ndarray
+ :param num_samples: The number of samples to transmit, will repeat or
+ truncate the recording to this length. Defaults to None.
+ :type num_samples: int, optional
+ :param tx_time: The time to transmit, will repeat or truncate the
+ recording to this length. Defaults to None.
+ :type tx_time: int or float, optional
+ """
+ if num_samples is not None and tx_time is not None:
+ raise ValueError("Only input one of num_samples or tx_time")
+ elif num_samples is not None:
+ tx_time = num_samples / self.tx_sample_rate
+ elif tx_time is not None:
+ pass
+ else:
+ tx_time = len(recording) / self.tx_sample_rate
+
+ if isinstance(recording, np.ndarray):
+ samples = recording
+ elif isinstance(recording, Recording):
+ if len(recording.data) > 1:
+ warnings.warn("Recording object is multichannel, only channel 0 data was used for transmission")
+
+ samples = recording.data[0]
+
+ samples = samples.astype(np.complex64, copy=False)
+ if np.max(np.abs(samples)) >= 1:
+ samples = samples / (np.max(np.abs(samples)) + 1e-12)
+
+ print("HackRF Starting TX...")
+ self.radio.start_tx(samples=samples, repeat=True)
+ time.sleep(tx_time)
+ self.radio.stop_tx()
+ print("HackRF Tx Completed.")
+
+ def set_clock_source(self, source):
+
+ self.radio.set_clock_source(source)
+
+ def close(self):
+ self.radio.close()
+
+ def _stream_rx(self, callback):
+ if not self._rx_initialized:
+ raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
+ return NotImplementedError("RX not yet implemented for HackRF")
+
+ def _stream_tx(self, callback):
+ return super()._stream_tx(callback)
diff --git a/src/ria_toolkit_oss/sdr/pluto.py b/src/ria_toolkit_oss/sdr/pluto.py
new file mode 100644
index 0000000..449505e
--- /dev/null
+++ b/src/ria_toolkit_oss/sdr/pluto.py
@@ -0,0 +1,502 @@
+import threading
+import time
+import traceback
+import warnings
+from typing import Optional
+
+import adi
+import numpy as np
+
+from ria_toolkit_oss.datatypes.recording import Recording
+from ria_toolkit_oss.sdr.sdr import SDR
+
+
+class Pluto(SDR):
+
+ def __init__(self, identifier=None):
+ """
+ Initialize a Pluto SDR device object and connect to the SDR hardware.
+
+ This software supports the ADALAM Pluto SDR created by Analog Devices.
+
+ :param identifier: The value of the parameter that identifies the device.
+ :type identifier: str = "192.168.3.1", "pluto.local", etc
+
+ If no identifier is provided, it will select the first device found, with a warning.
+ If more than one device is found with the identifier, it will select the first of those devices.
+ """
+ print(f"Initializing Pluto radio with identifier [{identifier}].")
+ try:
+ super().__init__()
+
+ if identifier is None:
+ uri = "ip:pluto.local"
+ else:
+ uri = f"ip:{identifier}"
+
+ self.radio = adi.ad9361(uri)
+ print(f"Successfully found Pluto radio with identifier [{identifier}].")
+ except Exception as e:
+ print(f"Failed to find Pluto radio with identifier [{identifier}].")
+ raise e
+
+ def init_rx(
+ self,
+ sample_rate: int | float,
+ center_frequency: int | float,
+ gain: int,
+ channel: int,
+ gain_mode: Optional[str] = "absolute",
+ ):
+ """
+ Initializes the Pluto for receiving.
+
+ :param sample_rate: The sample rate for receiving.
+ :type sample_rate: int or float
+ :param center_frequency: The center frequency of the recording.
+ :type center_frequency: int or float
+ :param gain: The gain set for receiving on the Pluto
+ :type gain: int
+ :param channel: The channel the Pluto is set to. Must be 0 or 1. 0 enables channel 1, 1 enables both channels.
+ :type channel: int
+ :param buffer_size: The buffer size during receive. Defaults to 10000.
+ :type buffer_size: int
+ """
+ print("Initializing RX")
+
+ self.set_rx_sample_rate(sample_rate=int(sample_rate))
+ print(f"Pluto sample rate = {self.radio.sample_rate}")
+
+ self.set_rx_center_frequency(center_frequency=int(center_frequency))
+ print(f"Pluto center frequency = {self.radio.rx_lo}")
+
+ if channel == 0:
+ self.radio.rx_enabled_channels = [0]
+ print(f"Pluto channel(s) = {self.radio.rx_enabled_channels}")
+ elif channel == 1:
+ self.radio.rx_enabled_channels = [0, 1]
+ print(f"Pluto channel(s) = {self.radio.rx_enabled_channels}")
+ else:
+ raise ValueError("Channel must be either 0 or 1.")
+
+ rx_gain_min = 0
+ rx_gain_max = 74
+
+ if gain_mode == "relative":
+ if gain > 0:
+ raise ValueError(
+ "When gain_mode = 'relative', gain must be < 0. This sets \
+ the gain relative to the maximum possible gain."
+ )
+ else:
+ abs_gain = rx_gain_max + gain
+ else:
+ abs_gain = gain
+
+ if abs_gain < rx_gain_min or abs_gain > rx_gain_max:
+ abs_gain = min(max(gain, rx_gain_min), rx_gain_max)
+ print(f"Gain {gain} out of range for Pluto.")
+ print(f"Gain range: {rx_gain_min} to {rx_gain_max} dB")
+
+ self.set_rx_gain(gain=abs_gain, channel=channel)
+ if channel == 0:
+ print(f"Pluto gain = {self.radio.rx_hardwaregain_chan0}")
+ elif channel == 1:
+ self.set_rx_gain(gain=abs_gain, channel=0)
+ print(f"Pluto gain = {self.radio.rx_hardwaregain_chan0}, {self.radio.rx_hardwaregain_chan1}")
+
+ self.radio.rx_buffer_size = 1024 # TODO deal with this for zmq
+ self._rx_initialized = True
+ self._tx_initialized = False
+
+ def init_tx(
+ self,
+ sample_rate: int | float,
+ center_frequency: int | float,
+ gain: int,
+ channel: int,
+ gain_mode: Optional[str] = "absolute",
+ ):
+ """
+ Initializes the Pluto for transmitting. Will transmit garbage during
+ center frequency tuning and setting the sample rate.
+
+ :param sample_rate: The sample rate for transmitting.
+ :type sample_rate: int or float
+ :param center_frequency: The center frequency of the recording.
+ :type center_frequency: int or float
+ :param gain: The gain set for transmitting on the Pluto
+ :type gain: int
+ :param channel: The channel the Pluto is set to. Must be 0 or 1. 0 enables channel 1, 1 enables both channels.
+ :type channel: int
+ :param buffer_size: The buffer size during transmit. Defaults to 10000.
+ :type buffer_size: int
+ """
+
+ print("Initializing TX")
+
+ self.set_tx_sample_rate(sample_rate=int(sample_rate))
+ print(f"Pluto sample rate = {self.radio.sample_rate}")
+
+ self.set_tx_center_frequency(center_frequency=int(center_frequency))
+ print(f"Pluto center frequency = {self.radio.tx_lo}")
+
+ if channel == 1:
+ self.radio.tx_enabled_channels = [0, 1]
+ print(f"Pluto channel(s) = {self.radio.tx_enabled_channels}")
+ elif channel == 0:
+ self.radio.tx_enabled_channels = [0]
+ print(f"Pluto channel(s) = {self.radio.tx_enabled_channels}")
+ else:
+ raise ValueError("Channel must be either 0 or 1.")
+
+ tx_gain_min = -89
+ tx_gain_max = 0
+
+ if gain_mode == "relative":
+ if gain > 0:
+ raise ValueError(
+ "When gain_mode = 'relative', gain must be < 0. This sets\
+ the gain relative to the maximum possible gain."
+ )
+ else:
+ abs_gain = tx_gain_max + gain
+ else:
+ abs_gain = gain
+
+ if abs_gain < tx_gain_min or abs_gain > tx_gain_max:
+ abs_gain = min(max(gain, tx_gain_min), tx_gain_max)
+ print(f"Gain {gain} out of range for Pluto.")
+ print(f"Gain range: {tx_gain_min} to {tx_gain_max} dB")
+
+ self.set_tx_gain(gain=abs_gain, channel=channel)
+ if channel == 0:
+ print(f"Pluto gain = {self.radio.tx_hardwaregain_chan0}")
+ elif channel == 1:
+ self.set_tx_gain(gain=abs_gain, channel=0)
+ print(f"Pluto gain = {self.radio.tx_hardwaregain_chan0}, {self.radio.tx_hardwaregain_chan1}")
+
+ self._tx_initialized = True
+ self._rx_initialized = False
+
+ def _stream_rx(self, callback):
+ if not self._rx_initialized:
+ raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
+
+ # print("Starting rx...")
+
+ self._enable_rx = True
+ while self._enable_rx is True:
+ signal = self.radio.rx()
+ signal = self._convert_rx_samples(signal)
+ # send callback complex signal
+ callback(buffer=signal, metadata=None)
+
+ def record(self, num_samples: Optional[int] = None, rx_time: Optional[int | float] = None):
+ """
+ Create a radio recording (iq samples and metadata) of a given length from the SDR.
+ Either num_samples or rx_time must be provided.
+ init_rx() must be called before record()
+
+ :param num_samples: The number of samples to record. Pluto max = 16M.
+ :type num_samples: int, optional
+ :param rx_time: The time to record.
+ :type rx_time: int or float, optional
+
+ returns: Recording object (iq samples and metadata)
+ """
+ if not self._rx_initialized:
+ raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
+
+ if num_samples is not None and rx_time is not None:
+ raise ValueError("Only input one of num_samples or rx_time")
+ elif num_samples is not None:
+ self._num_samples_to_record = num_samples
+ elif rx_time is not None:
+ self._num_samples_to_record = int(rx_time * self.rx_sample_rate)
+ else:
+ raise ValueError("Must provide input of one of num_samples or rx_time")
+
+ if self._num_samples_to_record > 16000000:
+ raise NotImplementedError("Pluto record for num_samples>16M not implemented yet.")
+ self.radio.rx_buffer_size = self._num_samples_to_record
+
+ print("Pluto Starting RX...")
+ samples = self.radio.rx()
+ if self.radio.rx_enabled_channels == [0]:
+ samples = self._convert_rx_samples(samples)
+ samples = [samples]
+ else:
+ channel1 = self._convert_rx_samples(samples[0])
+ channel2 = self._convert_rx_samples(samples[1])
+ samples = [channel1, channel2]
+ print("Pluto RX Completed.")
+
+ metadata = {
+ "source": self.__class__.__name__,
+ "sample_rate": self.rx_sample_rate,
+ "center_frequency": self.rx_center_frequency,
+ "gain": self.rx_gain,
+ }
+
+ recording = Recording(data=samples, metadata=metadata)
+ return recording
+
+ def _format_tx_data(self, recording: Recording | np.ndarray | list):
+ if isinstance(recording, np.ndarray):
+ data = self._convert_tx_samples(samples=recording)
+ elif isinstance(recording, Recording):
+ if self.radio.tx_enabled_channels == [0]:
+ samples = recording.data[0]
+ data = self._convert_tx_samples(samples=samples)
+
+ if len(recording.data) > 1:
+ warnings.warn("Recording object is multichannel, only channel 0 data was used for transmission")
+
+ else:
+ if len(recording.data) == 1:
+ warnings.warn(
+ "Recording has only 1 channel, the same data will be transmitted over both Pluto channels"
+ )
+ samples = recording.data[0]
+ data = [self._convert_tx_samples(samples), self._convert_tx_samples(samples)]
+ else:
+ if len(recording) > 2:
+ warnings.warn(
+ "More recordings were provided than channels in the Pluto. \
+ Only the first two recordings will be used"
+ )
+ sample0 = self._convert_tx_samples(recording.data[0])
+ sample1 = self._convert_tx_samples(recording.data[1])
+ data = [sample0, sample1]
+
+ elif isinstance(recording, list):
+ if len(recording) > 2:
+ warnings.warn(
+ "More recordings were provided than channels in the Pluto. \
+ Only the first two recordings will be used"
+ )
+
+ if isinstance(recording[0], np.ndarray):
+ data = [self._convert_tx_samples(recording[0]), self._convert_tx_samples(recording[1])]
+ elif isinstance(recording[0], Recording):
+ sample0 = self._convert_tx_samples(recording[0].data[0])
+ sample1 = self._convert_tx_samples(recording[1].data[0])
+ data = [sample0, sample1]
+
+ return data
+
+ def _timeout_cyclic_buffer(self, timeout):
+ time.sleep(timeout)
+ self.radio.tx_destroy_buffer()
+ self.radio.tx_cyclic_buffer = False
+ print("Pluto TX Completed.")
+
+ def interrupt_transmit(self):
+ self.radio.tx_destroy_buffer()
+ self.radio.tx_cyclic_buffer = False
+ print("Pluto TX Completed.")
+
+ def close(self):
+ if self.radio.tx_cyclic_buffer:
+ self.radio.tx_destroy_buffer()
+ del self.radio
+
+ def tx_recording(self, recording: Recording | np.ndarray | list, num_samples=None, tx_time=None, mode="timed"):
+ """
+ Transmit the given iq samples from the provided recording.
+ init_tx() must be called before this function.
+
+ :param recording: The recording(s) to transmit.
+ :type recording: Recording, np.ndarray, list[Recording, np.ndarray]
+ :param num_samples: The number of samples to transmit, will repeat or
+ truncate the recording to this length. Defaults to None.
+ :type num_samples: int, optional
+ :param tx_time: The time to transmit, will repeat or truncate the
+ recording to this length. Defaults to None.
+ :type tx_time: int or float, optional
+ :param mode: The mode of transmission, either timed or continuous. Defaults to timed.
+ :type mode: str, optional
+ """
+ if num_samples is not None and tx_time is not None:
+ raise ValueError("Only input one of num_samples or tx_time")
+ elif num_samples is not None:
+ tx_time = num_samples / self.tx_sample_rate
+ elif tx_time is not None:
+ pass
+ else:
+ tx_time = len(recording) / self.tx_sample_rate
+
+ data = self._format_tx_data(recording=recording)
+
+ try:
+ if self.radio.tx_cyclic_buffer:
+ print("Destroying existing TX buffer...")
+ self.radio.tx_destroy_buffer()
+ self.radio.tx_cyclic_buffer = False
+ except Exception as e:
+ print(f"Error while destroying TX buffer: {e}")
+
+ self.radio.tx_cyclic_buffer = True
+ print("Pluto Starting TX...")
+ self.radio.tx(data_np=data)
+ if mode == "timed":
+ timeout_thread = threading.Thread(target=self._timeout_cyclic_buffer, args=([tx_time]))
+ timeout_thread.start()
+ timeout_thread.join()
+
+ def _stream_tx(self, callback):
+ if self._tx_initialized is False:
+ raise RuntimeError("TX was not initialized, init_tx must be called before _stream_tx")
+
+ num_samples = 10000
+ # TODO remove hardcode
+
+ self._enable_tx = True
+ while self._enable_tx is True:
+ buffer = self._convert_tx_samples(callback(num_samples))
+ self.radio.tx(buffer[0])
+
+ def set_rx_center_frequency(self, center_frequency):
+ try:
+ self.radio.rx_lo = int(center_frequency)
+ self.rx_center_frequency = center_frequency
+ except OSError as e:
+ _handle_OSError(e)
+ except ValueError as e:
+ _handle_OSError(e)
+
+ def set_rx_sample_rate(self, sample_rate):
+ self.rx_sample_rate = sample_rate
+
+ # TODO add logic for limiting sample rate
+
+ try:
+ self.radio.sample_rate = int(sample_rate)
+
+ # set the front end filter width
+ self.radio.rx_rf_bandwidth = int(sample_rate)
+ except OSError as e:
+ _handle_OSError(e)
+ except ValueError as e:
+ _handle_OSError(e)
+
+ def set_rx_gain(self, gain, channel=0):
+ self.rx_gain = gain
+ try:
+ if channel == 0:
+
+ if gain is None:
+ self.radio.gain_control_mode_chan0 = "automatic"
+ print("Using Pluto Automatic Gain Control.")
+
+ else:
+ self.radio.gain_control_mode_chan0 = "manual"
+ self.radio.rx_hardwaregain_chan0 = gain # dB
+
+ elif channel == 1:
+ try:
+ if gain is None:
+ self.radio.gain_control_mode_chan1 = "automatic"
+ print("Using Pluto Automatic Gain Control.")
+
+ else:
+ self.radio.gain_control_mode_chan1 = "manual"
+ self.radio.rx_hardwaregain_chan1 = gain # dB
+
+ except Exception as e:
+ print("Failed to use channel 1 on the PlutoSDR. \nThis is only available for revC versions.")
+ raise e
+
+ else:
+ raise ValueError(f"Pluto channel must be 0 or 1 but was {channel}.")
+
+ except OSError as e:
+ _handle_OSError(e)
+ except ValueError as e:
+ _handle_OSError(e)
+
+ def set_rx_channel(self, channel):
+ self.rx_channel = channel
+
+ def set_rx_buffer_size(self, buffer_size):
+ raise NotImplementedError
+
+ def set_tx_center_frequency(self, center_frequency):
+ try:
+ self.radio.tx_lo = int(center_frequency)
+ self.tx_center_frequency = center_frequency
+
+ except OSError as e:
+ _handle_OSError(e)
+ except ValueError as e:
+ _handle_OSError(e)
+
+ def set_tx_sample_rate(self, sample_rate):
+ try:
+ self.radio.sample_rate = sample_rate
+ self.tx_sample_rate = sample_rate
+
+ except OSError as e:
+ _handle_OSError(e)
+ except ValueError as e:
+ _handle_OSError(e)
+
+ def set_tx_gain(self, gain, channel=0):
+ try:
+ self.tx_gain = gain
+
+ if channel == 0:
+ self.radio.tx_hardwaregain_chan0 = int(gain)
+ elif channel == 1:
+ self.radio.tx_hardwaregain_chan1 = int(gain)
+ else:
+ raise ValueError(f"Pluto channel must be 0 or 1 but was {channel}.")
+
+ except OSError as e:
+ _handle_OSError(e)
+ except ValueError as e:
+ _handle_OSError(e)
+
+ def set_tx_channel(self, channel):
+ raise NotImplementedError
+
+ def set_tx_buffer_size(self, buffer_size):
+ raise NotImplementedError
+
+ def shutdown(self):
+ del self.radio
+
+ def _convert_rx_samples(self, samples):
+ return samples / (2**11)
+
+ def _convert_tx_samples(self, samples):
+ return samples.astype(np.complex64) * (2**14)
+
+ def set_clock_source(self, source):
+ raise NotImplementedError
+
+
+def _handle_OSError(e):
+
+ # process a common difficult to read error message into a more intuitive format
+
+ print("PlutoSDR valid arguments:")
+
+ print("Standard: ")
+ print("\tCenter frequency: 325-3800Mhz")
+ print("\tSample rate: 521kHz-20Mhz")
+ print("\tGain: -90-0")
+ print("Hacked:")
+ print("\tCenter frequency: 70-6000Mhz")
+ print("\tSample rate: 521kHz-56Mhz")
+ print("\tGain: -90-0")
+
+ stack_trace = traceback.format_exc()
+ print(stack_trace)
+ if "sampling_frequency" in stack_trace or "sample rates" in stack_trace:
+ raise ValueError("The sample rate was out of range for the Pluto SDR.\n")
+ if "tx_lo" in stack_trace or "rx_lo" in stack_trace:
+ raise ValueError("The center frequency was out of range for the Pluto SDR.\n")
+ if "hardwaregain" in stack_trace:
+ raise ValueError("The gain was out of range for the Pluto SDR.\n")
diff --git a/src/ria_toolkit_oss/sdr/sdr.py b/src/ria_toolkit_oss/sdr/sdr.py
new file mode 100644
index 0000000..b69920a
--- /dev/null
+++ b/src/ria_toolkit_oss/sdr/sdr.py
@@ -0,0 +1,375 @@
+import math
+import pickle
+import warnings
+from abc import ABC, abstractmethod
+from typing import Optional
+
+import numpy as np
+import zmq
+
+from ria_toolkit_oss.datatypes.recording import Recording
+
+
+class SDR(ABC):
+ """
+ This class defines a common interface (a template) for all SDR devices.
+ Each specific SDR implementation should subclass SDR and provide concrete implementations
+ for the abstract methods.
+
+ To add support for a new radio, subclass this interface and implement all abstract methods.
+ If you experience difficulties, please `contact us `_, we are happy to
+ provide additional direction and/or help with the implementation details.
+ """
+
+ def __init__(self):
+
+ self._rx_initialized = False
+ self._tx_initialized = False
+ self._enable_rx = False
+ self._enable_tx = False
+ self._accumulated_buffer = None
+ self._max_num_buffers = None
+ self._num_buffers_processed = 0
+ self._accumulated_buffer = None
+ self._last_buffer = None
+
+ def record(self, num_samples: Optional[int] = None, rx_time: Optional[int | float] = None) -> Recording:
+ """
+ Create a radio recording of a given length. Either ``num_samples`` or ``rx_time`` must be provided.
+
+ Note that ``init_rx()`` must be called before ``record()``.
+
+ :param num_samples: The number of samples to record.
+ :type num_samples: int, optional
+ :param rx_time: The time to record.
+ :type rx_time: int or float, optional
+
+ :return: The Recording object
+ :rtype: Recording
+ """
+
+ if not self._rx_initialized:
+ raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
+
+ if num_samples is not None and rx_time is not None:
+ raise ValueError("Only input one of num_samples or rx_time")
+ elif num_samples is not None:
+ self._num_samples_to_record = num_samples
+ elif rx_time is not None:
+ self._num_samples_to_record = int(rx_time * self.rx_sample_rate)
+ else:
+ raise ValueError("Must provide input of one of num_samples or rx_time")
+
+ self.buffer_size = self.rx_buffer_size
+ num_buffers = self._num_samples_to_record // self.buffer_size + 1
+
+ self._max_num_buffers = num_buffers
+ self._num_buffers_processed = 0
+ self._num_buffers_processed = 0
+ self._last_buffer = None
+ self._accumulated_buffer = None
+ print("Starting stream")
+
+ self._stream_rx(
+ callback=self._accumulate_buffers_callback,
+ )
+
+ print("Finished stream")
+ metadata = {
+ "source": self.__class__.__name__,
+ "sample_rate": self.rx_sample_rate,
+ "center_frequency": self.rx_center_frequency,
+ "gain": self.rx_gain,
+ }
+
+ print("Creating recording")
+ # build recording, truncate to self._num_samples_to_record
+ recording = Recording(data=self._accumulated_buffer[:, : self._num_samples_to_record], metadata=metadata)
+
+ # reset to record again
+ self._accumulated_buffer = None
+ return recording
+
+ def stream_to_zmq(self, zmq_address, n_samples: int, buffer_size: Optional[int] = 10000):
+ """
+ Stream iq samples as interleaved bytes via zmq.
+
+ :param zmq_address: The zmq address.
+ :type zmq_address:
+ :param n_samples: The number of samples to stream.
+ :type n_samples: int
+ :param buffer_size: The buffer size during streaming. Defaults to 10000.
+ :type buffer_size: int, optional
+
+ :return: The trimmed Recording.
+ :rtype: Recording
+ """
+
+ self._previous_buffer = None
+ self._max_num_buffers = np.inf if n_samples == np.inf else math.ceil(n_samples / buffer_size)
+ self._num_buffers_processed = 0
+ self.zmq_address = _generate_full_zmq_address(str(zmq_address))
+ self.context = zmq.Context()
+ self.socket = self.context.socket(zmq.PUB)
+ self.socket.bind(self.zmq_address)
+
+ self._stream_rx(
+ self._zmq_bytestream_callback,
+ )
+
+ self.context.destroy()
+ self.socket.close()
+
+ def _accumulate_buffers_callback(self, buffer, metadata=None):
+ """
+ Receives a buffer and saves it to self.accumulated_buffer.
+ """
+ # expected buffer is complex samples range -1 to 1
+ # save the buffer until max reached
+ # return a recording
+
+ buffer = np.array(buffer) # make it 1d
+ if len(buffer.shape) == 1:
+ buffer = np.array([buffer])
+
+ # it runs these checks each time, is that an efficiency issue?
+
+ if self._max_num_buffers is None:
+ # default then
+ # this should probably print, but that would happen every buffer...
+ raise ValueError("Number of buffers for block capture not set.")
+
+ # add the given buffer to the pre-allocated buffer
+
+ if metadata is not None:
+ self.received_metadata = metadata
+
+ # TODO optimize, pre-allocate
+ if self._accumulated_buffer is not None:
+ self._accumulated_buffer = np.concatenate((self._accumulated_buffer, buffer), axis=1)
+ else:
+ # the first time
+ self._accumulated_buffer = buffer.copy()
+
+ self._num_buffers_processed = self._num_buffers_processed + 1
+ if self._num_buffers_processed >= self._max_num_buffers:
+ self.stop()
+
+ if self._last_buffer is not None:
+ if (buffer == self._last_buffer).all():
+ print("\033[93mWarning: Buffer Overflow Detected\033[0m")
+ self._last_buffer = buffer.copy()
+ else:
+ self._last_buffer = buffer.copy()
+
+ # print("Number of buffers received: " + str(self._num_buffers_processed))
+
+ def _zmq_bytestream_callback(self, buffer, metadata=None):
+ # push to ZMQ port
+ data = np.array(buffer).tobytes() # convert to bytes for transport
+ self.socket.send(data)
+
+ # print(f"Sent {self._num_buffers_processed} ZMQ buffers to {self.zmq_address}")
+
+ self._num_buffers_processed = self._num_buffers_processed + 1
+ if self._max_num_buffers is not None:
+ if self._num_buffers_processed >= self._max_num_buffers:
+ self.pause_rx()
+
+ if self._previous_buffer is not None:
+ if (buffer == self._previous_buffer).all():
+ print("\033[93mWarning: Buffer Overflow Detected\033[0m")
+ # TODO: I suggest we think about moving this part to the top of this function
+ # and skip the rest of the function in case of overflow.
+ # like, it's not necessary to stream repeated IQ data anyways!
+ self._previous_buffer = buffer.copy()
+
+ def pickle_buffer_to_zmq(self, zmq_address, buffer_size, num_buffers):
+ """
+ Stream samples to a zmq address, packaged in binary buffers using numpy.pickle.
+ Useful for inference applications with a known input size.
+ May reduce transfer rates, but individual buffers will not have discontinuities.
+
+ :param zmq_address: The tcp address to stream to.
+ :type zmq_address: str
+ :param buffer_size: The number of iq samples in a buffer.
+ :type buffer_size: int
+ :param num_buffers: The number of buffers to stream before stopping.
+ :type num_buffers: int
+ """
+ self._max_num_buffers = num_buffers
+ self.buffer_size = buffer_size
+ self._num_buffers_processed = 0
+ self.zmq_address = _generate_full_zmq_address(str(zmq_address))
+ self.context = zmq.Context()
+ self.socket = self.context.socket(zmq.PUB)
+ self.socket.bind(self.zmq_address)
+ self.set_rx_buffer_size(buffer_size)
+
+ self._stream_rx(self._zmq_pickle_buffer_callback)
+
+ def _zmq_pickle_buffer_callback(self, buffer, metadata=None):
+ # push to ZMQ port
+ # data = np.array(buffer).tobytes() # convert to bytes for transport
+ # self.socket.send(data)
+
+ self.socket.send(pickle.dumps(buffer))
+
+ # print(f"Sent {self._num_buffers_processed} ZMQ buffers to {self.zmq_address}")
+
+ self._num_buffers_processed = self._num_buffers_processed + 1
+ if self._max_num_buffers is not None:
+ if self._num_buffers_processed >= self._max_num_buffers:
+ self.stop()
+
+ if self._last_buffer is not None:
+ if (buffer == self._last_buffer).all():
+ print("\033[93mWarning: Buffer Overflow Detected\033[0m")
+ self._last_buffer = buffer.copy()
+ else:
+ self._last_buffer = buffer.copy()
+
+ def tx_recording(
+ self,
+ recording: Recording | np.ndarray,
+ num_samples: Optional[int] = None,
+ tx_time: Optional[int | float] = None,
+ ):
+ """
+ Transmit the given iq samples from the provided recording.
+ init_tx() must be called before this function.
+
+ :param recording: The recording to transmit.
+ :type recording: Recording or np.ndarray
+ :param num_samples: The number of samples to transmit, will repeat or
+ truncate the recording to this length. Defaults to None.
+ :type num_samples: int, optional
+ :param tx_time: The time to transmit, will repeat or truncate the
+ recording to this length. Defaults to None.
+ :type tx_time: int or float, optional
+ """
+
+ if not self._tx_initialized:
+ raise RuntimeError(
+ "TX was not initialized. init_tx() must be called before _stream_tx() or transmit_recording()"
+ )
+
+ if num_samples is not None and tx_time is not None:
+ raise ValueError("Only input one of num_samples or tx_time")
+ elif num_samples is not None:
+ self._num_samples_to_transmit = num_samples
+ elif tx_time is not None:
+ self._num_samples_to_transmit = tx_time * self.tx_sample_rate
+ else:
+ self._num_samples_to_transmit = len(recording)
+
+ if isinstance(recording, np.ndarray):
+ self._samples_to_transmit = recording
+ elif isinstance(recording, Recording):
+ if len(recording.data) > 1:
+ warnings.warn("Recording object is multichannel, only channel 0 data was used for transmission")
+
+ self._samples_to_transmit = recording.data[0]
+
+ self._num_samples_transmitted = 0
+
+ self._stream_tx(self._loop_recording_callback)
+
+ def _loop_recording_callback(self, num_samples):
+
+ samples_left = self._num_samples_to_transmit - self._num_samples_transmitted
+ # find where to start based on num_samples_transmitted
+ start_index = self._num_samples_transmitted % len(self._samples_to_transmit)
+
+ # generates an array of indices that wrap around as many times as necessary.
+ indices = np.arange(start_index, start_index + num_samples) % len(self._samples_to_transmit)
+ samples = self._samples_to_transmit[indices]
+
+ # zero pad at the end so we are still giving the requested buffer size
+ # while also giving the exact number of non zero samples
+ if len(samples) > samples_left:
+ samples[int(samples_left) :] = 0
+ self.pause_tx()
+
+ self._num_samples_transmitted = self._num_samples_transmitted + num_samples
+
+ return samples
+
+ def pause_rx(self):
+ self._enable_rx = False
+
+ def pause_tx(self):
+ self._enable_tx = False
+
+ def stop(self):
+ self.pause_rx()
+
+ @abstractmethod
+ def close(self):
+ pass
+
+ @abstractmethod
+ def init_rx(self, sample_rate, center_frequency, gain, channel, gain_mode):
+ pass
+
+ @abstractmethod
+ def init_tx(self, sample_rate, center_frequency, gain, channel, gain_mode):
+ pass
+
+ @abstractmethod
+ def _stream_rx(self, callback):
+ pass
+
+ @abstractmethod
+ def _stream_tx(self, callback):
+ pass
+
+ @abstractmethod
+ def set_clock_source(self, source):
+ """
+ Sets the clock source to external or internal.
+
+ :param source: The clock source
+ :type source: str
+ """
+ pass
+
+
+def _generate_full_zmq_address(input_address):
+ """
+ Helper function for zmq streaming.
+ If given a port number like 5556,
+ return tcp localhost address at that port.
+ Otherwise, return the address untouched.
+ """
+
+ if ("://" not in str(input_address)) and _is_valid_port(input_address):
+ # If no transport protocol specified, assume TCP
+ return "tcp://*:" + str(input_address)
+ else:
+ # Otherwise, return the input unchanged
+ return input_address
+
+
+def _is_valid_port(port):
+ """
+ Helper function for zmq address.
+ """
+ try:
+ port_num = int(port)
+ return 0 <= port_num <= 65535
+ except ValueError:
+ return False
+
+
+def _verify_sample_format(samples):
+ """
+ Verify that the sample data is in the range -1 to 1.
+
+ :param buffer: An array of samples.
+
+ :Return: True if the buffer is in the correct format, false if not.
+ :rtype: bool
+ """
+
+ return np.max(np.abs(samples)) <= 1
diff --git a/src/ria_toolkit_oss/sdr/usrp.py b/src/ria_toolkit_oss/sdr/usrp.py
new file mode 100644
index 0000000..29fa94a
--- /dev/null
+++ b/src/ria_toolkit_oss/sdr/usrp.py
@@ -0,0 +1,552 @@
+import subprocess
+import time
+import warnings
+from typing import Optional
+
+import numpy as np
+import uhd
+
+from ria_toolkit_oss.datatypes.recording import Recording
+from ria_toolkit_oss.sdr.sdr import SDR
+
+
+class USRP(SDR):
+ def __init__(self, identifier: str = None):
+ """
+ Initialize a USRP device object and connect to the SDR hardware.
+
+ This software supports all USRP SDRs created by Ettus Research.
+
+ :param identifier: Identifier of the device. Can be an IP address (e.g. "192.168.0.0"),
+ a device name (e.g. "MyB210"), or any name/address found via ``uhd_find_devices``.
+ If not provided, the first available device is selected with a warning.
+ If multiple devices match the identifier, the first one is selected.
+ :type identifier: str, optional
+ """
+ super().__init__()
+
+ self.default_buffer_size = 8000
+
+ # get all the info from only one of the parameters
+ self.device_dict = _create_device_dict(identifier)
+
+ self._rx_initialized = False
+ self._tx_initialized = False
+
+ def init_rx(
+ self,
+ sample_rate: int | float,
+ center_frequency: int | float,
+ channel: int,
+ gain: int,
+ gain_mode: Optional[str] = "absolute",
+ rx_buffer_size: int = 960000,
+ ):
+ """
+ Initialize the USRP for receiving.
+
+ :param sample_rate: The sample rate for receiving.
+ :type sample_rate: int or float
+
+ :param center_frequency: The center frequency of the recording.
+ :type center_frequency: int or float
+
+ :param channel: The channel the USRP is set to.
+ :type channel: int
+
+ :param gain: The gain set for receiving on the USRP.
+ :type gain: int
+
+ :param gain_mode: Gain mode setting. ``"absolute"`` passes gain directly to the SDR.
+ ``"relative"`` means gain should be a negative value, which will be subtracted
+ from the maximum gain.
+ :type gain_mode: str
+
+ :param rx_buffer_size: Internal buffer size for receiving samples. Defaults to 960000.
+ :type rx_buffer_size: int
+
+ :return: Dictionary with the actual RX parameters after configuration.
+ :rtype: dict
+ """
+
+ self.rx_buffer_size = rx_buffer_size
+
+ # build USRP object
+ usrp_args = _generate_usrp_config_string(sample_rate=sample_rate, device_dict=self.device_dict)
+ self.usrp = uhd.usrp.MultiUSRP(usrp_args)
+
+ # check if channel arg is valid
+ max_num_channels = self.usrp.get_rx_num_channels()
+ if channel + 1 > max_num_channels:
+ raise IOError(f"Channel {channel} not valid for device with {max_num_channels} channels.")
+
+ # check if gain arg is valid
+ gain_range = self.usrp.get_rx_gain_range()
+ if gain_mode == "relative":
+ if gain > 0:
+ raise ValueError(
+ "When gain_mode = 'relative', gain must be < 0. This sets\
+ the gain relative to the maximum possible gain."
+ )
+ else:
+ # set gain relative to max
+ abs_gain = gain_range.stop() + gain
+ else:
+ abs_gain = gain
+ if abs_gain < gain_range.start() or abs_gain > gain_range.stop():
+ print(f"Gain {abs_gain} out of range for this USRP.")
+ print(f"Gain range: {gain_range.start()} to {gain_range.stop()} dB")
+ abs_gain = min(max(abs_gain, gain_range.start()), gain_range.stop())
+ self.usrp.set_rx_gain(abs_gain, channel)
+
+ # check if sample rate arg is valid
+ sample_rate_range = self.usrp.get_rx_rates()
+ if sample_rate < sample_rate_range.start() or sample_rate > sample_rate_range.stop():
+ raise IOError(
+ f"Sample rate {sample_rate} not valid for this USRP.\nValid\
+ range is {sample_rate_range.start()}\
+ to {sample_rate_range.stop()}."
+ )
+ self.usrp.set_rx_rate(sample_rate, channel)
+
+ center_frequency_range = self.usrp.get_rx_freq_range()
+ if center_frequency < center_frequency_range.start() or center_frequency > center_frequency_range.stop():
+ raise IOError(
+ f"Center frequency {center_frequency} out of range for USRP.\
+ \nValid range is {center_frequency_range.start()} \
+ to {center_frequency_range.stop()}."
+ )
+ self.usrp.set_rx_freq(uhd.libpyuhd.types.tune_request(center_frequency), channel)
+
+ # set internal variables for metadata
+ self.rx_sample_rate = self.usrp.get_rx_rate(channel)
+ self.rx_gain = self.usrp.get_rx_gain(channel)
+ self.rx_center_frequency = self.usrp.get_rx_freq(channel)
+ self.rx_channel = channel
+
+ print(f"USRP RX Sample Rate = {self.rx_sample_rate}")
+ print(f"USRP RX Center Frequency = {self.rx_center_frequency}")
+ print(f"USRP RX Channel = {self.rx_channel}")
+ print(f"USRP RX Gain = {self.rx_gain}")
+
+ # flag to prevent user from calling certain functions before this one.
+ self._rx_initialized = True
+ self._tx_initialized = False
+
+ return {"sample_rate": self.rx_sample_rate, "center_frequency": self.rx_center_frequency, "gain": self.rx_gain}
+
+ def get_rx_sample_rate(self):
+ """
+ Retrieve the current sample rate of the receiver.
+
+ Returns:
+ float: The receiver's sample rate in samples per second (Hz).
+ """
+ return self.rx_sample_rate
+
+ def get_rx_center_frequency(self):
+ """
+ Retrieve the current center frequency of the receiver.
+
+ Returns:
+ float: The receiver's center frequency in Hertz (Hz).
+ """
+ return self.rx_center_frequency
+
+ def get_rx_gain(self):
+ """
+ Retrieve the current gain setting of the receiver.
+
+ Returns:
+ float: The receiver's gain in decibels (dB).
+ """
+ return self.rx_gain
+
+ def _stream_rx(self, callback):
+
+ if not self._rx_initialized:
+ raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
+
+ stream_args = uhd.usrp.StreamArgs("fc32", "sc16")
+ stream_args.channels = [self.rx_channel]
+
+ self.metadata = uhd.types.RXMetadata()
+ self.rx_stream = self.usrp.get_rx_stream(stream_args)
+
+ stream_command = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont)
+ stream_command.stream_now = True
+ self.rx_stream.issue_stream_cmd(stream_command)
+
+ # receive loop
+ self._enable_rx = True
+ print("USRP Starting RX...")
+ receive_buffer = np.zeros((1, self.rx_buffer_size), dtype=np.complex64)
+
+ while self._enable_rx:
+
+ # 1 is the timeout #TODO maybe set this intelligently based on the desired sample rate
+ self.rx_stream.recv(receive_buffer, self.metadata, 1)
+
+ # TODO set metadata correctly, sending real sample rate plus any error codes
+ # sending complex signal
+ callback(buffer=receive_buffer, metadata=self.metadata)
+
+ if self.metadata.error_code != uhd.types.RXMetadataErrorCode.none:
+ print(f"Error while receiving samples: {self.metadata.strerror()}")
+ if self.metadata.error_code == uhd.types.RXMetadataErrorCode.timeout:
+ print("Stopping receive due to timeout error.")
+ self.stop()
+ wait_time = 0.1
+ stop_time = self.usrp.get_time_now() + wait_time
+ stop_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont)
+ stop_cmd.stream_now = False
+ stop_cmd.time_spec = stop_time
+ self.rx_stream.issue_stream_cmd(stop_cmd)
+ time.sleep(wait_time) # TODO figure out what a realistic wait time is here.
+ del self.rx_stream
+ print("USRP RX Completed.")
+
+ def record(self, num_samples):
+ if not self._rx_initialized:
+ raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
+
+ stream_args = uhd.usrp.StreamArgs("fc32", "sc16")
+ stream_args.channels = [self.rx_channel]
+
+ self.metadata = uhd.types.RXMetadata()
+ self.rx_stream = self.usrp.get_rx_stream(stream_args)
+
+ stream_command = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont)
+ stream_command.stream_now = True
+ self.rx_stream.issue_stream_cmd(stream_command)
+
+ # receive loop
+ self._enable_rx = True
+ print("USRP Starting RX...")
+ store_array = np.zeros((1, (num_samples // self.rx_buffer_size + 1) * self.rx_buffer_size), dtype=np.complex64)
+ receive_buffer = np.zeros((1, self.rx_buffer_size), dtype=np.complex64)
+ for i in range(num_samples // self.rx_buffer_size + 1):
+
+ # write samples to receive buffer
+ # they should already be complex
+
+ # 1 is the timeout #TODO maybe set this intelligently based on the desired sample rate
+ self.rx_stream.recv(receive_buffer, self.metadata, 1)
+
+ # TODO set metadata correctly, sending real sample rate plus any error codes
+ # sending complex signal
+ store_array[:, i * self.rx_buffer_size : (i + 1) * self.rx_buffer_size] = receive_buffer
+
+ wait_time = 0.1
+ stop_time = self.usrp.get_time_now() + wait_time
+ stop_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont)
+ stop_cmd.stream_now = False
+ stop_cmd.time_spec = stop_time
+ self.rx_stream.issue_stream_cmd(stop_cmd)
+ time.sleep(wait_time) # TODO figure out what a realistic wait time is here.
+ del self.rx_stream
+ print("USRP RX Completed.")
+ metadata = {
+ "source": self.__class__.__name__,
+ "sample_rate": self.rx_sample_rate,
+ "center_frequency": self.rx_center_frequency,
+ "gain": self.rx_gain,
+ }
+
+ return Recording(data=store_array[:, :num_samples], metadata=metadata)
+
+ def init_tx(
+ self,
+ sample_rate: int | float,
+ center_frequency: int | float,
+ gain: int,
+ channel: int,
+ gain_mode: Optional[str] = "absolute",
+ ):
+ """
+ Initialize the USRP for transmitting.
+
+ :param sample_rate: The sample rate for transmitting.
+ :type sample_rate: int or float
+
+ :param center_frequency: The center frequency of the recording.
+ :type center_frequency: int or float
+
+ :param gain: The gain set for transmitting on the USRP.
+ :type gain: int
+
+ :param channel: The channel the USRP is set to.
+ :type channel: int
+
+ :param gain_mode: Gain mode setting. ``"absolute"`` passes gain directly to the SDR.
+ ``"relative"`` means gain should be a negative value, which will be subtracted
+ from the maximum gain.
+ :type gain_mode: str
+ """
+
+ self.tx_buffer_size = 2000
+
+ print(f"USRP TX Gain Mode = '{gain_mode}'")
+
+ config_str = _generate_usrp_config_string(sample_rate=sample_rate, device_dict=self.device_dict)
+ self.usrp = uhd.usrp.MultiUSRP(config_str)
+
+ # check if channel arg is valid
+ max_num_channels = self.usrp.get_rx_num_channels()
+ if channel + 1 > max_num_channels:
+ raise IOError(f"Channel {channel} not valid for device with {max_num_channels} channels.")
+
+ # Ensure gain is within valid range
+ gain_range = self.usrp.get_tx_gain_range()
+ if gain_mode == "relative":
+ if gain > 0:
+ raise ValueError(
+ "When gain_mode = 'relative', gain must be < 0. This sets\
+ the gain relative to the maximum possible gain."
+ )
+ else:
+ # set gain relative to max
+ abs_gain = gain_range.stop() + gain
+ else:
+ abs_gain = gain
+ if abs_gain < gain_range.start() or abs_gain > gain_range.stop():
+ print(f"Gain {abs_gain} out of range for this USRP.")
+ print(f"Gain range: {gain_range.start()} to {gain_range.stop()} dB")
+ abs_gain = min(max(abs_gain, gain_range.start()), gain_range.stop())
+
+ self.usrp.set_tx_gain(abs_gain, channel)
+
+ # check if sample rate arg is valid
+ sample_rate_range = self.usrp.get_tx_rates()
+ if sample_rate < sample_rate_range.start() or sample_rate > sample_rate_range.stop():
+ raise IOError(
+ f"Sample rate {sample_rate} not valid for this USRP.\nValid\
+ range is {sample_rate_range.start()} to {sample_rate_range.stop()}."
+ )
+ self.usrp.set_tx_rate(sample_rate, channel)
+
+ center_frequency_range = self.usrp.get_tx_freq_range()
+ if center_frequency < center_frequency_range.start() or center_frequency > center_frequency_range.stop():
+ raise IOError(
+ f"Center frequency {center_frequency} out of range for USRP.\
+ \nValid range is {center_frequency_range.start()}\
+ to {center_frequency_range.stop()}."
+ )
+ self.usrp.set_tx_freq(uhd.libpyuhd.types.tune_request(center_frequency), channel)
+
+ self.usrp.set_clock_source("internal")
+ self.usrp.set_time_source("internal")
+ self.usrp.set_tx_rate(sample_rate)
+ self.usrp.set_tx_freq(uhd.types.TuneRequest(center_frequency), channel)
+ self.usrp.set_tx_antenna("TX/RX", channel)
+
+ # set internal variables for metadata
+ self.tx_sample_rate = self.usrp.get_tx_rate(channel)
+ self.tx_gain = self.usrp.get_tx_gain(channel)
+ self.tx_center_frequency = self.usrp.get_tx_freq(channel)
+ self.tx_channel = channel
+
+ print(f"USRP TX Sample Rate = {self.tx_sample_rate}")
+ print(f"USRP TX Center Frequency = {self.tx_center_frequency}")
+ print(f"USRP TX Channel = {self.tx_channel}")
+ print(f"USRP TX Gain = {self.tx_gain}")
+
+ self._tx_initialized = True
+ self._rx_initialized = False
+
+ def close(self):
+ pass
+
+ def _stream_tx(self, callback):
+
+ stream_args = uhd.usrp.StreamArgs("fc32", "sc16") # wire and cpu data formats
+ stream_args.channels = [self.tx_channel]
+ tx_stream = self.usrp.get_tx_stream(stream_args)
+
+ metadata = uhd.types.TXMetadata()
+
+ metadata.start_of_burst = True
+ metadata.end_of_burst = False
+ self._enable_tx = True
+ print("USRP Starting TX...")
+
+ while self._enable_tx:
+ buffer = callback(self.tx_buffer_size)
+ tx_stream.send(buffer, metadata)
+ metadata.start_of_burst = False
+
+ print("USRP TX Completed.")
+
+ def tx_recording(
+ self,
+ recording: Recording | np.ndarray,
+ num_samples: Optional[int] = None,
+ tx_time: Optional[int | float] = None,
+ ):
+ """
+ Transmit the given iq samples from the provided recording.
+ init_tx() must be called before this function.
+
+ :param recording: The recording to transmit.
+ :type recording: Recording or np.ndarray
+ :param num_samples: The number of samples to transmit, will repeat or
+ truncate the recording to this length. Defaults to None.
+ :type num_samples: int, optional
+ :param tx_time: The time to transmit, will repeat or truncate the
+ recording to this length. Defaults to None.
+ :type tx_time: int or float, optional
+ """
+
+ if num_samples is not None and tx_time is not None:
+ raise ValueError("Only input one of num_samples or tx_time")
+ elif num_samples is not None:
+ self._num_samples_to_transmit = num_samples
+ elif tx_time is not None:
+ self._num_samples_to_transmit = int(tx_time * self.tx_sample_rate)
+ else:
+ self._num_samples_to_transmit = len(recording)
+
+ if isinstance(recording, np.ndarray):
+ samples = recording
+ elif isinstance(recording, Recording):
+ if len(recording.data) > 1:
+ warnings.warn("Recording object is multichannel, only channel 0 data was used for transmission")
+
+ samples = recording.data[0]
+
+ samples = samples.astype(np.complex64, copy=False)
+
+ # This is extremely important
+ # Ensure array is contiguous
+ samples = np.ascontiguousarray(samples)
+
+ # Ensure correct byte order
+ if samples.dtype.byteorder == ">":
+ samples = samples.byteswap().newbyteorder()
+
+ self._samples_to_transmit = samples
+ self._num_samples_transmitted = 0
+
+ self._stream_tx(self._loop_recording_callback)
+
+ def set_clock_source(self, source):
+ source = source.lower()
+ if source == "external":
+ self.usrp.set_clock_source(source)
+
+ print(f"USRP clock source set to {self.usrp.get_clock_source(0)}")
+
+
+def _create_device_dict(identifier_value=None):
+ """
+ Get the dictionary of information corresponding to any unique identifier,
+ using uhd_find_devices.
+ """
+
+ available_devices = _parse_uhd_find_devices()
+ print(available_devices)
+ if identifier_value is None:
+ print("\033[93mWarning: No USRP device identifier provided. Defaulting to the first USRP device found.\033[0m")
+ if len(available_devices) > 0:
+ formatted_dict_str = "\n".join([f"\t{key}: {value}" for key, value in available_devices[0].items()])
+ else:
+ raise IOError("\033[91mError: No USRP devices found.\033[0m")
+ print(f"Device information: \n{formatted_dict_str}")
+ return available_devices[0]
+
+ identified_devices = []
+ for device_dict in available_devices:
+ for key, value in device_dict.items():
+ if identifier_value is not None and str(value).lower() == str(identifier_value).lower():
+ identified_devices.append(device_dict)
+ break
+
+ if len(identified_devices) > 1:
+ print(f"\033[93mWarning: Found multiple USRP devices with identifier '{identifier_value}'.\033[0m")
+ print("\033[93mDefaulting to the first USRP device found with this identifier.\033[0m")
+ formatted_dict_str = "\n".join([f"\t{key}: {value}" for key, value in identified_devices[0].items()])
+ print(f"Device information: \n{formatted_dict_str}")
+ return identified_devices[0]
+
+ elif len(identified_devices) == 1:
+ print(f"\033[92mSuccessfully found USRP device with identifier '{identifier_value}'\033[0m")
+ formatted_dict_str = "\n".join([f"\t{key}: {value}" for key, value in identified_devices[0].items()])
+ print(f"Device information: \n{formatted_dict_str}")
+ return identified_devices[0]
+
+ elif len(identified_devices) == 0:
+ raise IOError(f"\033[31mError: No USRP device found for identifier '{identifier_value}'.\033[0m")
+
+
+def _generate_usrp_config_string(sample_rate, device_dict):
+ """
+ Create a correctly formatted string as expected by
+ uhd.usrp.MultiUSRP constructor
+
+ If it is a x300 there are two options for internal master clock settings
+ master_clock_rate_string = self.force_srate_xseries(sample_rate)
+ """
+
+ if "type" in device_dict and device_dict["type"] == "x300":
+ master_clock_rate_string = _force_srate_xseries(sample_rate)
+ else:
+ master_clock_rate_string = ""
+
+ if "addr" in device_dict:
+ ip_address_string = f"addr={device_dict['addr']},"
+ else:
+ ip_address_string = ""
+
+ if "name" in device_dict:
+ name_string = f"name={device_dict['name']},"
+ else:
+ name_string = ""
+
+ config_string = ip_address_string + master_clock_rate_string + name_string
+
+ return config_string
+
+
+def _force_srate_xseries(sample_rate):
+ two_hundred_rates = [200.0e6 / i for i in range(1, 201)] # down to 1MHz wide
+ one_eighty_four_rates = [184.32e6 / i for i in range(1, 185)] # down to ~ 1MHz wide
+
+ diff_two_hundred = min([abs(x - sample_rate) for x in two_hundred_rates])
+ diff_one_eighty_four = min([abs(x - sample_rate) for x in one_eighty_four_rates])
+
+ closest_list = "two_hundred_rates" if diff_two_hundred < diff_one_eighty_four else "one_eighty_four_rates"
+ if closest_list == "one_eighty_four_rates":
+ mcr_str = "master_clock_rate=184.32e6,"
+ # print("MCR set to 184.32 MHz")
+ else:
+ mcr_str = ""
+ return mcr_str
+
+
+def _parse_uhd_find_devices():
+ """
+ Parse the uhd_find_devices subprocess command output into usable data.
+ Returns: an array length = num_devices of dicts containing the data.
+ """
+ p = subprocess.Popen("uhd_find_devices", stdout=subprocess.PIPE)
+ output, err = p.communicate()
+ separate_devices = output.rsplit(b"--")
+ cleaned_separate_devices = [device for device in separate_devices if len(device) >= 20]
+ list_of_dicts = []
+ for device_string in cleaned_separate_devices:
+ device_as_list = device_string.split(b"\n")
+ device_as_list = [device for device in device_as_list if len(device) >= 2]
+ for i in range(len(device_as_list)):
+ device_as_list[i] = device_as_list[i].strip(b" ")
+
+ device_dict = {}
+ for i in range(len(device_as_list)):
+ [key, value] = device_as_list[i].split(b":")
+ key = key.strip()
+ value = value.strip()
+ key = key.decode("utf-8") # cast to string
+ value = value.decode("utf-8")
+ device_dict.update({key: value})
+
+ list_of_dicts.append(device_dict)
+ return list_of_dicts
diff --git a/tox.ini b/tox.ini
index b61f05e..107b46b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,7 +24,7 @@ commands =
[flake8]
max-line-length = 119
extend-ignore = W503, E203, E701
-exclude = .git, .github, build, dist, docs, venv, .venv, env, .env, .idea, .vscode, .tox
+exclude = .git, .riahub, build, dist, docs, venv, .venv, env, .env, .idea, .vscode, .tox, _external
max-complexity = 15
per-file-ignores = __init__.py:F401