Compare commits

..

4 Commits

25 changed files with 3520 additions and 14 deletions

View File

@ -36,7 +36,12 @@ todo_include_todos = True
templates_path = ['.templates'] templates_path = ['.templates']
exclude_patterns = [] 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', 'rtlsdr', 'bladerf']
autodoc_default_options = { autodoc_default_options = {
'members': True, '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), intersphinx_mapping = {'python': (f'https://docs.python.org/{version_link}', None),
'numpy': ('https://numpy.org/doc/stable', None), 'numpy': ('https://numpy.org/doc/stable', None),
'scipy': ('https://docs.scipy.org/doc/scipy', 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): def autodoc_process_docstring(app, what, name, obj, options, lines):

View File

@ -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 <sdr/index>

View File

@ -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 <rx>
Example 2: SDR Transmission <tx>

View File

@ -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.

View File

@ -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.

View File

@ -5,11 +5,9 @@ RIA Toolkit OSS Documentation
:maxdepth: 2 :maxdepth: 2
Introduction <intro/index> Introduction <intro/index>
Datatypes Package <ria_toolkit_oss/datatypes/ria_toolkit_oss.datatypes> SDR Guides <sdr_guides/index>
IO Package <ria_toolkit_oss/ria_toolkit_oss.io> Examples <examples/index>
Transforms Package <ria_toolkit_oss/ria_toolkit_oss.transforms> RIA Toolkit OSS <ria_toolkit_oss/index>
Utils Package <ria_toolkit_oss/ria_toolkit_oss.utils>
Viz Package <ria_toolkit_oss/ria_toolkit_oss.viz>
Indices and tables Indices and tables
================== ==================

View File

@ -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 <datatypes/ria_toolkit_oss.datatypes>
SDR Package <ria_toolkit_oss.sdr>
IO Package <ria_toolkit_oss.io>
Transforms Package <ria_toolkit_oss.transforms>
Utils Package <ria_toolkit_oss.utils>
Viz Package <ria_toolkit_oss.viz>

View File

@ -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

View File

@ -0,0 +1,85 @@
.. _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)
---------------------------
Step 1: 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.
Step 2: Create and/or activate your virtual environment
.. code-block:: bash
python3 -m venv venv
pip install -r requirements.txt # If relevant
source venv/bin/activate
Step 3: from within the virtual environment, clone the bladerf host repo, then build and install the wheel for
bladerf python bindings.
.. code-block:: bash
cd ~
mkdir workarea
cd workarea
git clone --depth 1 https://github.com/Nuand/bladeRF.git
cd bladeRF/host
cd libraries/libbladeRF_bindings/python
sudo python3 setup.py bdist_wheel
pip install dist/*.whl
Further Information
-------------------
- `Official Website <https://www.nuand.com/>`_
- `BladeRF Documentation <https://www.nuand.com/documentation/>`_
- `GitHub Repository <https://github.com/Nuand/bladeRF>`_

View File

@ -0,0 +1,56 @@
.. _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)
---------------------------
`HackRF Software Installation Guide <https://hackrf.readthedocs.io/en/latest/installing_hackrf_software.html>`_
.. todo::
Addition HackRF installation instructions
Further information
-------------------
- `Official Website <https://greatscottgadgets.com/hackrf/>`_
- `Project Documentation <https://hackrf.readthedocs.io/en/latest/>`_
- `GitHub Repository <https://github.com/greatscottgadgets/hackrf>`_

View File

@ -0,0 +1,17 @@
.. _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, and any additional information needed for setup and configuration.
.. toctree::
:maxdepth: 2
BladeRF <blade>
HackRF <hackrf>
PlutoSDR <pluto>
USRP <usrp>
RTL-SDR <rtl>

View File

@ -0,0 +1,96 @@
.. _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)
---------------------------
The PlutoSDR Python API can be installed via pip. To build and install the drivers from source, see the instructions below:
.. code-block:: bash
# Install required packages
sudo apt-get update
sudo apt-get install -y \
build-essential \
git \
libxml2-dev \
bison \
flex \
libcdk5-dev \
cmake \
python3-pip \
libusb-1.0-0-dev \
libavahi-client-dev \
libavahi-common-dev \
libaio-dev
# Clone and build libiio
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
# Clone and build libad9361-iio
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
# Install Python bindings
pip install pyadi-iio
Further information
-------------------
- `PlutoSDR Documentation <https://wiki.analog.com/university/tools/pluto>`_

View File

@ -0,0 +1,40 @@
.. _rtl:
RTL-SDR
=======
RTL-SDR (RTL2832U Software Defined Radio) is a low-cost USB dongle originally designed for digital TV reception
that has been repurposed as a wideband software-defined radio. RTL-SDR devices are popular for hobbyist use due to
their affordability and wide range of applications.
The RTL-SDR is based on the Realtek RTL2832U chipset, which features direct sampling and demodulation of RF
signals. These devices are commonly used for tasks such as listening to FM radio, monitoring aircraft traffic
(ADS-B), receiving weather satellite images, and more.
Supported Models
----------------
- **Generic RTL-SDR Dongle:** The most common variant, usually featuring an R820T or R820T2 tuner.
- **RTL-SDR Blog V3:** An enhanced version with additional features like direct sampling mode and a bias tee for
powering external devices.
Key Features
------------
- **Frequency Range:** Typically from 24 MHz to 1.7 GHz, depending on the tuner chip.
- **Bandwidth:** Limited to about 2.4 MHz, making it suitable for narrowband applications.
- **Connectivity:** USB 2.0 interface, plug-and-play on most platforms.
- **Software Support:** Compatible with SDR software like SDR#, GQRX, and GNU Radio.
Limitations
-----------
- Narrow bandwidth compared to more expensive SDRs, which may limit some applications.
- Sensitivity and performance can vary depending on the specific model and components.
- Requires external software for signal processing and analysis.
Further Information
-------------------
- `Official Website <https://www.rtl-sdr.com/>`_
- `RTL-SDR Quick Start Guide <https://www.rtl-sdr.com/rtl-sdr-quick-start-guide/>`_

View File

@ -0,0 +1,80 @@
.. _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)
---------------------------
1. Install the required system packages via APT:
.. code-block:: bash
sudo apt-get install libuhd-dev uhd-host python3-uhd
2. Build and install UHD from source:
.. code-block:: bash
sudo apt-get install git cmake libboost-all-dev libusb-1.0-0-dev python3-docutils python3-mako python3-numpy python3-requests python3-ruamel.yaml python3-setuptools build-essential
cd ~
git clone https://github.com/EttusResearch/uhd.git
cd uhd/host
mkdir build
cd build
cmake -DENABLE_TESTS=OFF -DENABLE_C_API=OFF -DENABLE_MANUAL=OFF ..
make -j8
sudo make install
sudo ldconfig
3. Find your dist packages and add to `PYTHONPATH`. Example:
.. code-block:: bash
export PYTHONPATH='/usr/local/lib/python3.10/site-packages/'
export PYTHONPATH='/usr/local/lib/python3.10/dist-packages/:$PYTHONPATH'
Further information
-------------------
- `Official Website <https://www.ettus.com/>`_
- `USRP Documentation <https://kb.ettus.com/USRP_Hardware_Driver_and_Interfaces>`_

233
poetry.lock generated
View File

@ -154,6 +154,104 @@ files = [
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, {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]] [[package]]
name = "chardet" name = "chardet"
version = "5.2.0" version = "5.2.0"
@ -877,6 +975,19 @@ files = [
{file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, {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]] [[package]]
name = "pyflakes" name = "pyflakes"
version = "3.4.0" version = "3.4.0"
@ -1005,6 +1116,111 @@ files = [
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, {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]] [[package]]
name = "quantiphy" name = "quantiphy"
version = "2.20" version = "2.20"
@ -1917,7 +2133,22 @@ files = [
{file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"},
] ]
[[package]]
name = "zmq"
version = "0.0.0"
description = "You are probably looking for pyzmq."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "zmq-0.0.0.tar.gz", hash = "sha256:6b1a1de53338646e8c8405803cffb659e8eb7bb02fff4c9be62a7acfac8370c9"},
{file = "zmq-0.0.0.zip", hash = "sha256:21cfc6be254c9bc25e4dabb8a3b2006a4227966b7b39a637426084c8dc6901f7"},
]
[package.dependencies]
pyzmq = "*"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10" python-versions = ">=3.10"
content-hash = "8fafbb6cdc3f1490399a4cea9520376c1743cf30b3430d9c3ad35ff5754bb850" content-hash = "c351530d2d67ae2302b24199aaf6c78fa2bdbaaa39b40e172540f0b97b381b8f"

View File

@ -44,7 +44,8 @@ dependencies = [
"quantiphy (>=2.20,<3.0)", "quantiphy (>=2.20,<3.0)",
"plotly (>=6.3.0,<7.0.0)", "plotly (>=6.3.0,<7.0.0)",
"h5py (>=3.14.0,<4.0.0)", "h5py (>=3.14.0,<4.0.0)",
"pandas (>=2.3.2,<3.0.0)" "pandas (>=2.3.2,<3.0.0)",
"zmq (>=0.0.0,<0.0.1)"
] ]
[tool.poetry] [tool.poetry]

View File

@ -57,7 +57,7 @@ class Annotation:
def is_valid(self) -> bool: def is_valid(self) -> bool:
""" """
Check that the annotation sample count is > 0 and the freq_lower_edge<freq_upper_edge. Verify ``sample_count > 0`` and the ``freq_lower_edge < freq_upper_edge``.
:returns: True if valid, False if not. :returns: True if valid, False if not.
""" """
@ -96,9 +96,9 @@ class Annotation:
def __eq__(self, other: Annotation) -> bool: def __eq__(self, other: Annotation) -> bool:
return self.__dict__ == other.__dict__ 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} 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: 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: try:
json.dumps(x) json.dumps(x)

View File

@ -276,7 +276,13 @@ class Recording:
:return: A new recording with the same metadata and data, with dtype. :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 # 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. # cross-platform support where the types are aliased across platforms.

View File

@ -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

View File

@ -0,0 +1,384 @@
from typing import Optional
import numpy as np
from ria_toolkit_oss.datatypes import Recording
from ria_toolkit_oss.sdr import SDR
from bladerf import _bladerf
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()}")

View File

@ -0,0 +1,159 @@
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.sdr import SDR
from .libhackrf import HackRF as hrf
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)
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)

View File

@ -0,0 +1,715 @@
# TODO: only use transfer->valid_length in callbacks
# TODO: make error messages more informative
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")

View File

@ -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_tx_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.tx_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")

View File

@ -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 <mailto:info@qoherent.ai>`_, 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

View File

@ -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