Compare commits
No commits in common. "sdr" and "main" have entirely different histories.
12
README.md
12
README.md
|
@ -9,25 +9,17 @@
|
|||
|
||||
<p align="center">
|
||||
<!-- PyPI -->
|
||||
<a href="https://pypi.org/project/ria-toolkit-oss">
|
||||
<a href="https://pypi.org/project/ria-toolkit-oss/">
|
||||
<img src="https://img.shields.io/pypi/v/ria-toolkit-oss"/>
|
||||
</a>
|
||||
<!-- Conda (RIA Hub) -->
|
||||
<a href="https://riahub.ai/qoherent/-/packages/conda/ria-toolkit-oss">
|
||||
<img src="https://img.shields.io/badge/conda-ria--toolkit--oss-green.svg" alt="Conda on RIA Hub">
|
||||
</a>
|
||||
<!-- License -->
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0">
|
||||
<img src="https://img.shields.io/badge/License-AGPLv3-blue.svg" />
|
||||
</a>
|
||||
<!-- Docs -->
|
||||
<a href="https://ria-toolkit-oss.readthedocs.io">
|
||||
<a href="https://ria-toolkit-oss.readthedocs.io/">
|
||||
<img src="https://img.shields.io/badge/docs-ria--toolkit--oss-blue"/>
|
||||
</a>
|
||||
<!-- Python Version -->
|
||||
<a href="https://www.python.org/downloads">
|
||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python Version">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# RIA Toolkit OSS
|
||||
|
|
|
@ -36,12 +36,7 @@ todo_include_todos = True
|
|||
templates_path = ['.templates']
|
||||
exclude_patterns = []
|
||||
|
||||
# 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_mock_imports = ['uhd', 'adi', 'iio', 'rtlsdr']
|
||||
|
||||
autodoc_default_options = {
|
||||
'members': True,
|
||||
|
@ -52,9 +47,7 @@ version_link = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|||
intersphinx_mapping = {'python': (f'https://docs.python.org/{version_link}', None),
|
||||
'numpy': ('https://numpy.org/doc/stable', None),
|
||||
'scipy': ('https://docs.scipy.org/doc/scipy', None),
|
||||
'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)}
|
||||
'matplotlib': ('https://matplotlib.org/stable', None)}
|
||||
|
||||
|
||||
def autodoc_process_docstring(app, what, name, obj, options, lines):
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
.. _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>
|
|
@ -1,17 +0,0 @@
|
|||
.. _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>
|
|
@ -1,58 +0,0 @@
|
|||
.. _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.
|
|
@ -1,69 +0,0 @@
|
|||
.. _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.
|
|
@ -5,9 +5,11 @@ RIA Toolkit OSS Documentation
|
|||
:maxdepth: 2
|
||||
|
||||
Introduction <intro/index>
|
||||
SDR Guides <sdr_guides/index>
|
||||
Examples <examples/index>
|
||||
RIA Toolkit OSS <ria_toolkit_oss/index>
|
||||
Datatypes Package <ria_toolkit_oss/datatypes/ria_toolkit_oss.datatypes>
|
||||
IO Package <ria_toolkit_oss/ria_toolkit_oss.io>
|
||||
Transforms Package <ria_toolkit_oss/ria_toolkit_oss.transforms>
|
||||
Utils Package <ria_toolkit_oss/ria_toolkit_oss.utils>
|
||||
Viz Package <ria_toolkit_oss/ria_toolkit_oss.viz>
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
.. _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>
|
|
@ -1,16 +0,0 @@
|
|||
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
|
|
@ -1,85 +0,0 @@
|
|||
.. _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>`_
|
|
@ -1,56 +0,0 @@
|
|||
.. _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>`_
|
|
@ -1,17 +0,0 @@
|
|||
.. _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>
|
|
@ -1,96 +0,0 @@
|
|||
.. _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>`_
|
|
@ -1,40 +0,0 @@
|
|||
.. _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/>`_
|
|
@ -1,80 +0,0 @@
|
|||
.. _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
233
poetry.lock
generated
|
@ -154,104 +154,6 @@ files = [
|
|||
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "implementation_name == \"pypy\""
|
||||
files = [
|
||||
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
|
||||
{file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "5.2.0"
|
||||
|
@ -975,19 +877,6 @@ files = [
|
|||
{file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "implementation_name == \"pypy\""
|
||||
files = [
|
||||
{file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"},
|
||||
{file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyflakes"
|
||||
version = "3.4.0"
|
||||
|
@ -1116,111 +1005,6 @@ files = [
|
|||
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyzmq"
|
||||
version = "27.1.0"
|
||||
description = "Python bindings for 0MQ"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"},
|
||||
{file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"},
|
||||
{file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"},
|
||||
{file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"},
|
||||
{file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"},
|
||||
{file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"},
|
||||
{file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"},
|
||||
{file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"},
|
||||
{file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"},
|
||||
{file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"},
|
||||
{file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"},
|
||||
{file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"},
|
||||
{file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"},
|
||||
{file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"},
|
||||
{file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"},
|
||||
{file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"},
|
||||
{file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"},
|
||||
{file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"},
|
||||
{file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"},
|
||||
{file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"},
|
||||
{file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"},
|
||||
{file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"},
|
||||
{file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"},
|
||||
{file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"},
|
||||
{file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"},
|
||||
{file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"},
|
||||
{file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"},
|
||||
{file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"},
|
||||
{file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = "*", markers = "implementation_name == \"pypy\""}
|
||||
|
||||
[[package]]
|
||||
name = "quantiphy"
|
||||
version = "2.20"
|
||||
|
@ -2133,22 +1917,7 @@ files = [
|
|||
{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]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10"
|
||||
content-hash = "c351530d2d67ae2302b24199aaf6c78fa2bdbaaa39b40e172540f0b97b381b8f"
|
||||
content-hash = "8fafbb6cdc3f1490399a4cea9520376c1743cf30b3430d9c3ad35ff5754bb850"
|
||||
|
|
|
@ -44,8 +44,7 @@ dependencies = [
|
|||
"quantiphy (>=2.20,<3.0)",
|
||||
"plotly (>=6.3.0,<7.0.0)",
|
||||
"h5py (>=3.14.0,<4.0.0)",
|
||||
"pandas (>=2.3.2,<3.0.0)",
|
||||
"zmq (>=0.0.0,<0.0.1)"
|
||||
"pandas (>=2.3.2,<3.0.0)"
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
|
@ -83,7 +82,7 @@ target-version = ["py310"]
|
|||
exclude = '''
|
||||
/(
|
||||
\.git
|
||||
| \.riahub
|
||||
| \.github
|
||||
| \.tox
|
||||
| build
|
||||
| dist
|
||||
|
@ -94,7 +93,6 @@ exclude = '''
|
|||
| \.env
|
||||
| \.idea
|
||||
| \.vscode
|
||||
| _external
|
||||
)/
|
||||
'''
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class Annotation:
|
|||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
Verify ``sample_count > 0`` and the ``freq_lower_edge < freq_upper_edge``.
|
||||
Check that the annotation sample count is > 0 and the freq_lower_edge<freq_upper_edge.
|
||||
|
||||
:returns: True if valid, False if not.
|
||||
"""
|
||||
|
@ -96,9 +96,9 @@ class Annotation:
|
|||
def __eq__(self, other: Annotation) -> bool:
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
def to_sigmf_format(self) -> dict:
|
||||
def to_sigmf_format(self):
|
||||
"""
|
||||
Returns a JSON dictionary representation, formatted for saving in a ``.sigmf-meta`` file.
|
||||
Returns a JSON dictionary representing this annotation formatted to be saved in a .sigmf-meta file.
|
||||
"""
|
||||
|
||||
annotation_dict = {SigMFFile.START_INDEX_KEY: self.sample_start, SigMFFile.LENGTH_INDEX_KEY: self.sample_count}
|
||||
|
@ -119,8 +119,7 @@ class Annotation:
|
|||
|
||||
def _is_jsonable(x: Any) -> bool:
|
||||
"""
|
||||
:return: True if ``x`` is JSON serializable, False otherwise.
|
||||
:rtype: bool
|
||||
:return: True if x is JSON serializable, False otherwise.
|
||||
"""
|
||||
try:
|
||||
json.dumps(x)
|
||||
|
|
|
@ -276,13 +276,7 @@ class Recording:
|
|||
|
||||
:return: A new recording with the same metadata and data, with dtype.
|
||||
|
||||
|
||||
**Examples:**
|
||||
|
||||
.. todo::
|
||||
|
||||
Usage examples coming soon!
|
||||
|
||||
TODO: Add example usage.
|
||||
"""
|
||||
# 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.
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
"""
|
||||
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
|
|
@ -1,714 +0,0 @@
|
|||
# Original work by Dressel, from the pyhackrf project: https://github.com/dressel/pyhackrf
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from ctypes import *
|
||||
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from itertools import izip
|
||||
except ImportError:
|
||||
izip = zip
|
||||
|
||||
path = os.path.dirname(__file__)
|
||||
logging.basicConfig()
|
||||
logger = logging.getLogger("HackRf Core")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# libhackrf = CDLL('/usr/local/lib/libhackrf.so')
|
||||
libhackrf = CDLL("libhackrf.so.0")
|
||||
|
||||
|
||||
def enum(*sequential, **named):
|
||||
enums = dict(zip(sequential, range(len(sequential))), **named)
|
||||
return type("Enum", (), enums)
|
||||
|
||||
|
||||
HackRfVendorRequest = enum(
|
||||
HACKRF_VENDOR_REQUEST_SET_TRANSCEIVER_MODE=1,
|
||||
HACKRF_VENDOR_REQUEST_MAX2837_WRITE=2,
|
||||
HACKRF_VENDOR_REQUEST_MAX2837_READ=3,
|
||||
HACKRF_VENDOR_REQUEST_SI5351C_WRITE=4,
|
||||
HACKRF_VENDOR_REQUEST_SI5351C_READ=5,
|
||||
HACKRF_VENDOR_REQUEST_SAMPLE_RATE_SET=6,
|
||||
HACKRF_VENDOR_REQUEST_BASEBAND_FILTER_BANDWIDTH_SET=7,
|
||||
HACKRF_VENDOR_REQUEST_RFFC5071_WRITE=8,
|
||||
HACKRF_VENDOR_REQUEST_RFFC5071_READ=9,
|
||||
HACKRF_VENDOR_REQUEST_SPIFLASH_ERASE=10,
|
||||
HACKRF_VENDOR_REQUEST_SPIFLASH_WRITE=11,
|
||||
HACKRF_VENDOR_REQUEST_SPIFLASH_READ=12,
|
||||
HACKRF_VENDOR_REQUEST_CPLD_WRITE=13,
|
||||
HACKRF_VENDOR_REQUEST_BOARD_ID_READ=14,
|
||||
HACKRF_VENDOR_REQUEST_VERSION_STRING_READ=15,
|
||||
HACKRF_VENDOR_REQUEST_SET_FREQ=16,
|
||||
HACKRF_VENDOR_REQUEST_AMP_ENABLE=17,
|
||||
HACKRF_VENDOR_REQUEST_BOARD_PARTID_SERIALNO_READ=18,
|
||||
HACKRF_VENDOR_REQUEST_SET_LNA_GAIN=19,
|
||||
HACKRF_VENDOR_REQUEST_SET_VGA_GAIN=20,
|
||||
HACKRF_VENDOR_REQUEST_SET_TXVGA_GAIN=21,
|
||||
)
|
||||
|
||||
HackRfConstants = enum(
|
||||
LIBUSB_ENDPOINT_IN=0x80,
|
||||
LIBUSB_ENDPOINT_OUT=0x00,
|
||||
HACKRF_DEVICE_OUT=0x40,
|
||||
HACKRF_DEVICE_IN=0xC0,
|
||||
HACKRF_USB_VID=0x1D50,
|
||||
HACKRF_USB_PID=0x6089,
|
||||
)
|
||||
|
||||
HackRfError = enum(
|
||||
HACKRF_SUCCESS=0,
|
||||
HACKRF_TRUE=1,
|
||||
HACKRF_ERROR_INVALID_PARAM=-2,
|
||||
HACKRF_ERROR_NOT_FOUND=-5,
|
||||
HACKRF_ERROR_BUSY=-6,
|
||||
HACKRF_ERROR_NO_MEM=-11,
|
||||
HACKRF_ERROR_LIBUSB=-1000,
|
||||
HACKRF_ERROR_THREAD=-1001,
|
||||
HACKRF_ERROR_STREAMING_THREAD_ERR=-1002,
|
||||
HACKRF_ERROR_STREAMING_STOPPED=-1003,
|
||||
HACKRF_ERROR_STREAMING_EXIT_CALLED=-1004,
|
||||
HACKRF_ERROR_OTHER=-9999,
|
||||
# Python defaults to returning none
|
||||
HACKRF_ERROR=None,
|
||||
)
|
||||
|
||||
HackRfTranscieverMode = enum(
|
||||
HACKRF_TRANSCEIVER_MODE_OFF=0, HACKRF_TRANSCEIVER_MODE_RECEIVE=1, HACKRF_TRANSCEIVER_MODE_TRANSMIT=2
|
||||
)
|
||||
|
||||
# Data structures
|
||||
_libusb_device_handle = c_void_p
|
||||
_pthread_t = c_ulong
|
||||
|
||||
p_hackrf_device = c_void_p
|
||||
|
||||
|
||||
class hackrf_transfer(Structure):
|
||||
_fields_ = [
|
||||
("device", p_hackrf_device),
|
||||
("buffer", POINTER(c_byte)),
|
||||
("buffer_length", c_int),
|
||||
("valid_length", c_int),
|
||||
("rx_ctx", c_void_p),
|
||||
("tx_ctx", c_void_p),
|
||||
]
|
||||
|
||||
|
||||
class read_partid_serialno_t(Structure):
|
||||
_fields_ = [("part_id", c_uint32 * 2), ("serial_no", c_uint32 * 4)]
|
||||
|
||||
|
||||
class hackrf_device_list_t(Structure):
|
||||
_fields_ = [
|
||||
("serial_numbers", POINTER(c_char_p)),
|
||||
("usb_board_ids", c_void_p),
|
||||
("usb_device_index", POINTER(c_int)),
|
||||
("devicecount", c_int),
|
||||
("usb_devices", POINTER(c_void_p)),
|
||||
("usb_devicecount", c_int),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# _callback = CFUNCTYPE(c_int, POINTER(hackrf_transfer))
|
||||
_callback = CFUNCTYPE(c_int, POINTER(hackrf_transfer))
|
||||
|
||||
|
||||
# extern ADDAPI int ADDCALL hackrf_init();
|
||||
libhackrf.hackrf_init.restype = c_int
|
||||
libhackrf.hackrf_init.argtypes = []
|
||||
# extern ADDAPI int ADDCALL hackrf_exit();
|
||||
libhackrf.hackrf_exit.restype = c_int
|
||||
libhackrf.hackrf_exit.argtypes = []
|
||||
# extern ADDAPI int ADDCALL hackrf_open(hackrf_device** device);
|
||||
libhackrf.hackrf_open.restype = c_int
|
||||
libhackrf.hackrf_open.argtypes = [POINTER(p_hackrf_device)]
|
||||
# extern ADDAPI int ADDCALL hackrf_open_by_serial
|
||||
# (const char* const desired_serial_number, hackrf_device** device);
|
||||
# TODO: check that this one works
|
||||
f = libhackrf.hackrf_open_by_serial
|
||||
f.restype = c_int
|
||||
f.argtypes = [POINTER(p_hackrf_device)]
|
||||
|
||||
# extern ADDAPI int ADDCALL hackrf_device_list_open
|
||||
# (hackrf_device_list_t *list, int idx, hackrf_device** device);
|
||||
f = libhackrf.hackrf_device_list_open
|
||||
f.restype = c_int
|
||||
f.arg_types = [POINTER(hackrf_device_list_t), c_int, POINTER(p_hackrf_device)]
|
||||
# f.arg_types = [hackrf_device_list_t, c_int, POINTER(p_hackrf_device)]
|
||||
|
||||
# extern ADDAPI int ADDCALL hackrf_close(hackrf_device* device);
|
||||
libhackrf.hackrf_close.restype = c_int
|
||||
libhackrf.hackrf_close.argtypes = [p_hackrf_device]
|
||||
|
||||
|
||||
# extern ADDAPI int ADDCALL hackrf_set_sample_rate(hackrf_device*
|
||||
# device, const double freq_hz);
|
||||
libhackrf.hackrf_set_sample_rate.restype = c_int
|
||||
libhackrf.hackrf_set_sample_rate.argtypes = [p_hackrf_device, c_double]
|
||||
|
||||
# GAIN SETTINGS
|
||||
# extern ADDAPI int ADDCALL hackrf_set_amp_enable(hackrf_device*
|
||||
# device, const uint8_t value);
|
||||
libhackrf.hackrf_set_amp_enable.restype = c_int
|
||||
libhackrf.hackrf_set_amp_enable.argtypes = [p_hackrf_device, c_uint8]
|
||||
# extern ADDAPI int ADDCALL hackrf_set_lna_gain(hackrf_device* device,
|
||||
# uint32_t value);
|
||||
libhackrf.hackrf_set_lna_gain.restype = c_int
|
||||
libhackrf.hackrf_set_lna_gain.argtypes = [p_hackrf_device, c_uint32]
|
||||
# extern ADDAPI int ADDCALL hackrf_set_vga_gain(hackrf_device* device,
|
||||
# uint32_t value);
|
||||
libhackrf.hackrf_set_vga_gain.restype = c_int
|
||||
libhackrf.hackrf_set_vga_gain.argtypes = [p_hackrf_device, c_uint32]
|
||||
|
||||
# START AND STOP RX
|
||||
# extern ADDAPI int ADDCALL hackrf_start_rx(hackrf_device* device,
|
||||
# hackrf_sample_block_cb_fn callback, void* rx_ctx);
|
||||
libhackrf.hackrf_start_rx.restype = c_int
|
||||
libhackrf.hackrf_start_rx.argtypes = [p_hackrf_device, _callback, c_void_p]
|
||||
# extern ADDAPI int ADDCALL hackrf_stop_rx(hackrf_device* device);
|
||||
libhackrf.hackrf_stop_rx.restype = c_int
|
||||
libhackrf.hackrf_stop_rx.argtypes = [p_hackrf_device]
|
||||
|
||||
# extern ADDAPI hackrf_device_list_t* ADDCALL hackrf_device_list();
|
||||
f = libhackrf.hackrf_device_list
|
||||
f.restype = POINTER(hackrf_device_list_t)
|
||||
f.argtypes = []
|
||||
|
||||
|
||||
def hackrf_device_list():
|
||||
return libhackrf.hackrf_device_list()
|
||||
|
||||
|
||||
# dictionary containing all hackrf_devices in use
|
||||
_hackrf_dict = dict()
|
||||
|
||||
|
||||
def get_dict():
|
||||
return _hackrf_dict
|
||||
|
||||
|
||||
def read_samples_cb(hackrf_transfer):
|
||||
|
||||
# let's access the contents
|
||||
c = hackrf_transfer.contents
|
||||
|
||||
# c.device is an int representing the pointer to the hackrf device
|
||||
# we can get the pointer with p_hackrf_device(c.device)
|
||||
this_hackrf = _hackrf_dict[c.device]
|
||||
|
||||
if len(this_hackrf.buffer) == this_hackrf.num_bytes:
|
||||
this_hackrf.still_sampling = False
|
||||
return 0
|
||||
|
||||
# like == case, but cut down the buffer to size
|
||||
if len(this_hackrf.buffer) > this_hackrf.num_bytes:
|
||||
this_hackrf.still_sampling = False
|
||||
this_hackrf.buffer = this_hackrf.buffer[0 : this_hackrf.num_bytes]
|
||||
return 0
|
||||
|
||||
# grab the buffer data and concatenate it
|
||||
values = cast(c.buffer, POINTER(c_byte * c.buffer_length)).contents
|
||||
this_hackrf.buffer = this_hackrf.buffer + bytearray(values)
|
||||
|
||||
# print("len(bd) = ",len(this_hackrf.buffer))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
rs_callback = _callback(read_samples_cb)
|
||||
|
||||
|
||||
## extern ADDAPI int ADDCALL hackrf_start_tx(hackrf_device* device,
|
||||
## hackrf_sample_block_cb_fn callback, void* tx_ctx);
|
||||
# libhackrf.hackrf_start_tx.restype = c_int
|
||||
# libhackrf.hackrf_start_tx.argtypes = [POINTER(hackrf_device), _callback, c_void_p]
|
||||
## extern ADDAPI int ADDCALL hackrf_stop_tx(hackrf_device* device);
|
||||
# libhackrf.hackrf_stop_tx.restype = c_int
|
||||
# libhackrf.hackrf_stop_tx.argtypes = [POINTER(hackrf_device)]
|
||||
# extern ADDAPI int ADDCALL hackrf_is_streaming(hackrf_device* device);
|
||||
libhackrf.hackrf_is_streaming.restype = c_int
|
||||
libhackrf.hackrf_is_streaming.argtypes = [p_hackrf_device]
|
||||
## extern ADDAPI int ADDCALL hackrf_max2837_read(hackrf_device* device,
|
||||
## uint8_t register_number, uint16_t* value);
|
||||
# libhackrf.hackrf_max2837_read.restype = c_int
|
||||
# libhackrf.hackrf_max2837_read.argtypes = [
|
||||
# POINTER(hackrf_device), c_uint8, POINTER(c_uint16)]
|
||||
## extern ADDAPI int ADDCALL hackrf_max2837_write(hackrf_device* device,
|
||||
## uint8_t register_number, uint16_t value);
|
||||
# libhackrf.hackrf_max2837_write.restype = c_int
|
||||
# libhackrf.hackrf_max2837_write.argtypes = [POINTER(hackrf_device), c_uint8, c_uint16]
|
||||
## extern ADDAPI int ADDCALL hackrf_si5351c_read(hackrf_device* device,
|
||||
## uint16_t register_number, uint16_t* value);
|
||||
# libhackrf.hackrf_si5351c_read.restype = c_int
|
||||
# libhackrf.hackrf_si5351c_read.argtypes = [
|
||||
# POINTER(hackrf_device), c_uint16, POINTER(c_uint16)]
|
||||
## extern ADDAPI int ADDCALL hackrf_si5351c_write(hackrf_device* device,
|
||||
## uint16_t register_number, uint16_t value);
|
||||
# libhackrf.hackrf_si5351c_write.restype = c_int
|
||||
# libhackrf.hackrf_si5351c_write.argtypes = [POINTER(hackrf_device), c_uint16, c_uint16]
|
||||
## extern ADDAPI int ADDCALL
|
||||
## hackrf_set_baseband_filter_bandwidth(hackrf_device* device, const
|
||||
## uint32_t bandwidth_hz);
|
||||
# libhackrf.hackrf_set_baseband_filter_bandwidth.restype = c_int
|
||||
# libhackrf.hackrf_set_baseband_filter_bandwidth.argtypes = [
|
||||
# POINTER(hackrf_device), c_uint32]
|
||||
## extern ADDAPI int ADDCALL hackrf_rffc5071_read(hackrf_device* device,
|
||||
## uint8_t register_number, uint16_t* value);
|
||||
# libhackrf.hackrf_rffc5071_read.restype = c_int
|
||||
# libhackrf.hackrf_rffc5071_read.argtypes = [
|
||||
# POINTER(hackrf_device), c_uint8, POINTER(c_uint16)]
|
||||
## extern ADDAPI int ADDCALL hackrf_rffc5071_write(hackrf_device*
|
||||
## device, uint8_t register_number, uint16_t value);
|
||||
# libhackrf.hackrf_rffc5071_write.restype = c_int
|
||||
# libhackrf.hackrf_rffc5071_write.argtypes = [POINTER(hackrf_device), c_uint8, c_uint16]
|
||||
## extern ADDAPI int ADDCALL hackrf_spiflash_erase(hackrf_device*
|
||||
## device);
|
||||
# libhackrf.hackrf_spiflash_erase.restype = c_int
|
||||
# libhackrf.hackrf_spiflash_erase.argtypes = [POINTER(hackrf_device)]
|
||||
## extern ADDAPI int ADDCALL hackrf_spiflash_write(hackrf_device*
|
||||
## device, const uint32_t address, const uint16_t length, unsigned char*
|
||||
## const data);
|
||||
# libhackrf.hackrf_spiflash_write.restype = c_int
|
||||
# libhackrf.hackrf_spiflash_write.argtypes = [
|
||||
# POINTER(hackrf_device), c_uint32, c_uint16, POINTER(c_ubyte)]
|
||||
## extern ADDAPI int ADDCALL hackrf_spiflash_read(hackrf_device* device,
|
||||
## const uint32_t address, const uint16_t length, unsigned char* data);
|
||||
# libhackrf.hackrf_spiflash_read.restype = c_int
|
||||
# libhackrf.hackrf_spiflash_read.argtypes = [
|
||||
# POINTER(hackrf_device), c_uint32, c_uint16, POINTER(c_ubyte)]
|
||||
## extern ADDAPI int ADDCALL hackrf_cpld_write(hackrf_device* device,
|
||||
## unsigned char* const data, const unsigned int total_length);
|
||||
# libhackrf.hackrf_cpld_write.restype = c_int
|
||||
# libhackrf.hackrf_cpld_write.argtypes = [POINTER(hackrf_device), POINTER(c_ubyte), c_uint]
|
||||
## extern ADDAPI int ADDCALL hackrf_board_id_read(hackrf_device* device,
|
||||
## uint8_t* value);
|
||||
# libhackrf.hackrf_board_id_read.restype = c_int
|
||||
# libhackrf.hackrf_board_id_read.argtypes = [POINTER(hackrf_device), POINTER(c_uint8)]
|
||||
## extern ADDAPI int ADDCALL hackrf_version_string_read(hackrf_device*
|
||||
## device, char* version, uint8_t length);
|
||||
# libhackrf.hackrf_version_string_read.restype = c_int
|
||||
# libhackrf.hackrf_version_string_read.argtypes = [POINTER(hackrf_device), POINTER(c_char), c_uint8]
|
||||
# extern ADDAPI int ADDCALL hackrf_set_freq(hackrf_device* device,
|
||||
# const uint64_t freq_hz);
|
||||
libhackrf.hackrf_set_freq.restype = c_int
|
||||
libhackrf.hackrf_set_freq.argtypes = [p_hackrf_device, c_uint64]
|
||||
#
|
||||
## extern ADDAPI int ADDCALL hackrf_set_freq_explicit(hackrf_device* device,
|
||||
## const uint64_t if_freq_hz, const uint64_t lo_freq_hz,
|
||||
## const enum rf_path_filter path);,
|
||||
## libhackrf.hackrf_set_freq_explicit.restype = c_int
|
||||
## libhackrf.hackrf_set_freq_explicit.argtypes = [c_uint64,
|
||||
## c_uint64, ]
|
||||
#
|
||||
## extern ADDAPI int ADDCALL
|
||||
## hackrf_set_sample_rate_manual(hackrf_device* device, const uint32_t
|
||||
## freq_hz, const uint32_t divider);
|
||||
# libhackrf.hackrf_set_sample_rate_manual.restype = c_int
|
||||
# libhackrf.hackrf_set_sample_rate_manual.argtypes = [
|
||||
# POINTER(hackrf_device), c_uint32, c_uint32]
|
||||
#
|
||||
# extern ADDAPI int ADDCALL
|
||||
# hackrf_board_partid_serialno_read(hackrf_device* device,
|
||||
# read_partid_serialno_t* read_partid_serialno);
|
||||
f = libhackrf.hackrf_board_partid_serialno_read
|
||||
f.restype = c_int
|
||||
f.argtypes = [p_hackrf_device, POINTER(read_partid_serialno_t)]
|
||||
|
||||
## extern ADDAPI int ADDCALL hackrf_set_txvga_gain(hackrf_device*
|
||||
## device, uint32_t value);
|
||||
# libhackrf.hackrf_set_txvga_gain.restype = c_int
|
||||
# libhackrf.hackrf_set_txvga_gain.argtypes = [POINTER(hackrf_device), c_uint32]
|
||||
## extern ADDAPI int ADDCALL hackrf_set_antenna_enable(hackrf_device*
|
||||
## device, const uint8_t value);
|
||||
# libhackrf.hackrf_set_antenna_enable.restype = c_int
|
||||
# libhackrf.hackrf_set_antenna_enable.argtypes = [POINTER(hackrf_device), c_uint8]
|
||||
#
|
||||
## extern ADDAPI const char* ADDCALL hackrf_error_name(enum hackrf_error errcode);
|
||||
## libhackrf.hackrf_error_name.restype = POINTER(c_char)
|
||||
## libhackrf.hackrf_error_name.argtypes = []
|
||||
#
|
||||
## extern ADDAPI const char* ADDCALL hackrf_board_id_name(enum hackrf_board_id board_id);
|
||||
## libhackrf.hackrf_board_id_name.restype = POINTER(c_char)
|
||||
## libhackrf.hackrf_board_id_name.argtypes = []
|
||||
#
|
||||
## extern ADDAPI const char* ADDCALL hackrf_filter_path_name(const enum rf_path_filter path);
|
||||
## libhackrf.hackrf_filter_path_name.restype = POINTER(c_char)
|
||||
## libhackrf.hackrf_filter_path_name.argtypes = []
|
||||
#
|
||||
|
||||
libhackrf.hackrf_si5351c_write.restype = c_int
|
||||
libhackrf.hackrf_si5351c_write.argtypes = [POINTER(p_hackrf_device), c_uint16, c_uint16]
|
||||
|
||||
PLL_SOURCE_CLKIN = 0x01
|
||||
PLL_SOURCE_INTERNAL = 0x00
|
||||
|
||||
# Callback type for transmit
|
||||
_tx_callback = CFUNCTYPE(c_int, POINTER(hackrf_transfer))
|
||||
|
||||
# extern ADDAPI int ADDCALL hackrf_start_tx(hackrf_device* device, hackrf_sample_block_cb_fn callback, void* tx_ctx);
|
||||
libhackrf.hackrf_start_tx.restype = c_int
|
||||
libhackrf.hackrf_start_tx.argtypes = [p_hackrf_device, _tx_callback, c_void_p]
|
||||
|
||||
# extern ADDAPI int ADDCALL hackrf_stop_tx(hackrf_device* device);
|
||||
libhackrf.hackrf_stop_tx.restype = c_int
|
||||
libhackrf.hackrf_stop_tx.argtypes = [p_hackrf_device]
|
||||
|
||||
# Gain settings for transmit
|
||||
# extern ADDAPI int ADDCALL hackrf_set_txvga_gain(hackrf_device* device, uint32_t value);
|
||||
libhackrf.hackrf_set_txvga_gain.restype = c_int
|
||||
libhackrf.hackrf_set_txvga_gain.argtypes = [p_hackrf_device, c_uint32]
|
||||
|
||||
|
||||
# Helper function to convert complex64 samples to bytes
|
||||
def iq2bytes(samples):
|
||||
"""
|
||||
Convert complex64 samples to interleaved int8 bytes for HackRF transmission.
|
||||
|
||||
:param samples: NumPy array of complex64 samples.
|
||||
:return: Bytes object containing interleaved I/Q samples as int8.
|
||||
"""
|
||||
# Normalize samples to the range -1 to +1
|
||||
samples = samples / np.max(np.abs(samples))
|
||||
|
||||
# Scale to the range -127 to +127
|
||||
samples_scaled = samples * 127.0
|
||||
|
||||
# Ensure the values are within the int8 range
|
||||
samples_scaled = np.clip(samples_scaled, -127, 127)
|
||||
|
||||
# Separate real and imaginary parts and convert to int8
|
||||
i_samples = np.real(samples_scaled).astype(np.int8)
|
||||
q_samples = np.imag(samples_scaled).astype(np.int8)
|
||||
|
||||
# Interleave I and Q samples
|
||||
interleaved = np.empty(i_samples.size + q_samples.size, dtype=np.int8)
|
||||
interleaved[0::2] = i_samples
|
||||
interleaved[1::2] = q_samples
|
||||
|
||||
return interleaved.tobytes()
|
||||
|
||||
|
||||
# Helper function to get error names
|
||||
def get_error_name(error_code):
|
||||
libhackrf.hackrf_error_name.restype = c_char_p
|
||||
libhackrf.hackrf_error_name.argtypes = [c_int]
|
||||
return libhackrf.hackrf_error_name(error_code).decode("utf-8")
|
||||
|
||||
|
||||
class HackRF(object):
|
||||
|
||||
_center_freq = 100e6
|
||||
_sample_rate = 20e6
|
||||
device_opened = False
|
||||
|
||||
def __init__(self, device_index=0):
|
||||
self.open(device_index)
|
||||
|
||||
# TODO: initialize defaults here
|
||||
self.disable_amp()
|
||||
self.set_lna_gain(16)
|
||||
self.set_vga_gain(16)
|
||||
self.set_txvga_gain(0)
|
||||
|
||||
self.active_clock_source = None
|
||||
|
||||
self.buffer = bytearray()
|
||||
self.num_bytes = 16 * 262144
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
def open(self, device_index=0):
|
||||
|
||||
# pointer to device structure
|
||||
self.dev_p = p_hackrf_device(None)
|
||||
|
||||
hdl = hackrf_device_list()
|
||||
result = libhackrf.hackrf_device_list_open(hdl, device_index, pointer(self.dev_p))
|
||||
if result != 0:
|
||||
raise IOError("Error code %d when opening HackRF" % (result))
|
||||
|
||||
# This is how I used to do it...
|
||||
# Note I only pass in the dev_p here, but it worked.
|
||||
# But above, I have to pass in a pointer(self.dev_p)
|
||||
# They should both take the same thing
|
||||
# result = libhackrf.hackrf_open(self.dev_p)
|
||||
# if result != 0:
|
||||
# raise IOError('Error code %d when opening HackRF' % (result))
|
||||
|
||||
# self.dev_p.value returns the integer value of the pointer
|
||||
|
||||
_hackrf_dict[self.dev_p.value] = self
|
||||
# print("self.dev_p.value = ", self.dev_p.value)
|
||||
|
||||
self.device_opened = True
|
||||
|
||||
def close(self):
|
||||
if not self.device_opened:
|
||||
return
|
||||
|
||||
libhackrf.hackrf_close(self.dev_p)
|
||||
self.device_opened = False
|
||||
|
||||
def __del__(self):
|
||||
print("del function is being called")
|
||||
self.close()
|
||||
|
||||
# sleep_time in seconds
|
||||
# I used to have just pass in the while loop
|
||||
def read_samples(self, num_samples=131072, sleep_time=0.05):
|
||||
|
||||
num_bytes = 2 * num_samples
|
||||
self.num_bytes = int(num_bytes)
|
||||
|
||||
self.buffer = bytearray()
|
||||
|
||||
# start receiving
|
||||
result = libhackrf.hackrf_start_rx(self.dev_p, rs_callback, None)
|
||||
if result != 0:
|
||||
raise IOError("Error in hackrf_start_rx")
|
||||
self.still_sampling = True # this does get called
|
||||
|
||||
while self.still_sampling:
|
||||
if sleep_time:
|
||||
time.sleep(sleep_time)
|
||||
|
||||
# stop receiving
|
||||
result = libhackrf.hackrf_stop_rx(self.dev_p)
|
||||
if result != 0:
|
||||
raise IOError("Error in hackrf_stop_rx")
|
||||
|
||||
# convert samples to iq
|
||||
iq = bytes2iq(self.buffer)
|
||||
|
||||
return iq
|
||||
|
||||
# setting the center frequency
|
||||
def set_freq(self, freq):
|
||||
freq = int(freq)
|
||||
result = libhackrf.hackrf_set_freq(self.dev_p, freq)
|
||||
if result != 0:
|
||||
raise IOError("Error code %d when setting frequency to %d Hz" % (result, freq))
|
||||
|
||||
self._center_freq = freq
|
||||
return
|
||||
|
||||
def get_freq(self):
|
||||
return self._center_freq
|
||||
|
||||
center_freq = property(get_freq, set_freq)
|
||||
|
||||
# sample rate
|
||||
def set_sample_rate(self, rate):
|
||||
result = libhackrf.hackrf_set_sample_rate(self.dev_p, rate)
|
||||
if result != 0:
|
||||
# TODO: make this error message better
|
||||
raise IOError("Sample rate set failure")
|
||||
self._sample_rate = rate
|
||||
return
|
||||
|
||||
def get_sample_rate(self):
|
||||
return self._sample_rate
|
||||
|
||||
sample_rate = property(get_sample_rate, set_sample_rate)
|
||||
|
||||
def get_serial_no(self):
|
||||
return get_serial_no(self.dev_p)
|
||||
|
||||
def enable_amp(self):
|
||||
result = libhackrf.hackrf_set_amp_enable(self.dev_p, 1)
|
||||
if result != 0:
|
||||
# TODO: make this a better message
|
||||
raise IOError("error enabling amp")
|
||||
return 0
|
||||
|
||||
def disable_amp(self):
|
||||
result = libhackrf.hackrf_set_amp_enable(self.dev_p, 0)
|
||||
if result != 0:
|
||||
# TODO: make this a better message
|
||||
raise IOError("error disabling amp")
|
||||
return 0
|
||||
|
||||
# rounds down to multiple of 8 (15 -> 8, 39 -> 32), etc.
|
||||
# internally, hackrf_set_lna_gain does the same thing
|
||||
# But we take care of it so we can keep track of the correct gain
|
||||
def set_lna_gain(self, gain):
|
||||
gain -= gain % 8 # round DOWN to multiple of 8
|
||||
result = libhackrf.hackrf_set_lna_gain(self.dev_p, gain)
|
||||
if result != 0:
|
||||
# TODO: make this a better message
|
||||
raise IOError("error setting lna gain")
|
||||
self._lna_gain = gain
|
||||
print("LNA gain set to", gain, "dB.")
|
||||
return 0
|
||||
|
||||
def get_lna_gain(self):
|
||||
return self._lna_gain
|
||||
|
||||
lna_gain = property(get_lna_gain, set_lna_gain)
|
||||
|
||||
def set_vga_gain(self, gain):
|
||||
gain -= gain % 2
|
||||
result = libhackrf.hackrf_set_vga_gain(self.dev_p, gain)
|
||||
if result != 0:
|
||||
# TODO: make this a better message
|
||||
raise IOError("error setting vga gain")
|
||||
self._vga_gain = gain
|
||||
print("VGA gain set to", gain, "dB.")
|
||||
return 0
|
||||
|
||||
def get_vga_gain(self):
|
||||
return self._vga_gain
|
||||
|
||||
vga_gain = property(get_vga_gain, set_vga_gain)
|
||||
|
||||
# rx_cb_fn is a callback function (in python)
|
||||
def start_rx(self, rx_cb_fn):
|
||||
rx_cb = _callback(rx_cb_fn)
|
||||
result = libhackrf.hackrf_start_rx(self.dev_p, rx_cb, None)
|
||||
if result != 0:
|
||||
raise IOError("start_rx failure")
|
||||
|
||||
def stop_rx(self):
|
||||
result = libhackrf.hackrf_stop_rx(self.dev_p)
|
||||
if result != 0:
|
||||
raise IOError("stop_rx failure")
|
||||
|
||||
# Add transmit gain property
|
||||
def set_txvga_gain(self, gain):
|
||||
if gain < 0 or gain > 47:
|
||||
raise ValueError("TXVGA gain must be between 0 and 47 dB")
|
||||
result = libhackrf.hackrf_set_txvga_gain(self.dev_p, gain)
|
||||
if result != 0:
|
||||
error_name = get_error_name(result)
|
||||
raise IOError(f"Error setting TXVGA gain: {error_name} (Code {result})")
|
||||
self._txvga_gain = gain
|
||||
print(f"TXVGA gain set to {gain} dB.")
|
||||
return 0
|
||||
|
||||
def get_txvga_gain(self):
|
||||
return self._txvga_gain
|
||||
|
||||
txvga_gain = property(get_txvga_gain, set_txvga_gain)
|
||||
|
||||
# Method to start transmission
|
||||
def start_tx(self, samples, repeat=False):
|
||||
"""
|
||||
Start transmitting samples.
|
||||
|
||||
:param samples: A numpy array of complex64 samples to transmit.
|
||||
:param repeat: If True, the samples will be transmitted in a loop.
|
||||
"""
|
||||
if not isinstance(samples, np.ndarray) or samples.dtype != np.complex64:
|
||||
raise ValueError("Samples must be a numpy array of complex64")
|
||||
|
||||
# Scale samples to int8 range
|
||||
if np.max(samples) > 1:
|
||||
raise ValueError
|
||||
samples_scaled = samples * 127.0
|
||||
samples_scaled = np.clip(samples_scaled, -127, 127) # Ensure values are within int8 range
|
||||
samples_scaled = samples_scaled.astype(np.complex64) # Ensure correct data type
|
||||
|
||||
self.tx_buffer = samples
|
||||
self.tx_repeat = repeat
|
||||
self.tx_index = 0 # Index to keep track of where we are in the buffer
|
||||
|
||||
# Convert the callback function to the required C callback
|
||||
self._tx_cb = _tx_callback(self._tx_callback)
|
||||
result = libhackrf.hackrf_start_tx(self.dev_p, self._tx_cb, None)
|
||||
if result != 0:
|
||||
error_name = get_error_name(result)
|
||||
raise IOError(f"Error starting transmission: {error_name} (Code {result})")
|
||||
|
||||
# Method to stop transmission
|
||||
def stop_tx(self):
|
||||
result = libhackrf.hackrf_stop_tx(self.dev_p)
|
||||
if result != 0:
|
||||
error_name = get_error_name(result)
|
||||
raise IOError(f"Error stopping transmission: {error_name} (Code {result})")
|
||||
|
||||
# The transmit callback function
|
||||
def _tx_callback(self, hackrf_transfer):
|
||||
c = hackrf_transfer.contents
|
||||
|
||||
# Determine how many bytes we need to send
|
||||
bytes_to_send = c.valid_length
|
||||
|
||||
# Prepare the data to send
|
||||
end_index = self.tx_index + bytes_to_send // 2 # Each sample is 2 bytes (I and Q)
|
||||
|
||||
if end_index > len(self.tx_buffer):
|
||||
if self.tx_repeat:
|
||||
# Loop back to the start
|
||||
end_index = end_index % len(self.tx_buffer)
|
||||
data = np.concatenate((self.tx_buffer[self.tx_index :], self.tx_buffer[:end_index]))
|
||||
self.tx_index = end_index
|
||||
else:
|
||||
# Fill the remaining buffer with zeros
|
||||
data = self.tx_buffer[self.tx_index :]
|
||||
padding = np.zeros(end_index - len(self.tx_buffer), dtype=np.complex64)
|
||||
data = np.concatenate((data, padding))
|
||||
self.tx_index = len(self.tx_buffer)
|
||||
else:
|
||||
data = self.tx_buffer[self.tx_index : end_index]
|
||||
self.tx_index = end_index
|
||||
|
||||
# Convert complex64 samples to bytes
|
||||
iq_bytes = iq2bytes(data)
|
||||
|
||||
# Copy data to the buffer
|
||||
memmove(c.buffer, iq_bytes, len(iq_bytes))
|
||||
|
||||
# If we've reached the end of the buffer and not repeating, stop transmission
|
||||
if self.tx_index >= len(self.tx_buffer) and not self.tx_repeat:
|
||||
return -1 # Returning -1 stops the transmission
|
||||
|
||||
return 0 # Continue transmission
|
||||
|
||||
def set_clock_source(self, source_str):
|
||||
pass
|
||||
|
||||
|
||||
# returns serial number as a string
|
||||
# it is too big to be a single number, so make it a string
|
||||
# the returned string matches the hackrf_info output
|
||||
def get_serial_no(dev_p):
|
||||
sn = read_partid_serialno_t()
|
||||
result = libhackrf.hackrf_board_partid_serialno_read(dev_p, sn)
|
||||
if result != 0:
|
||||
raise IOError("Error %d while getting serial number" % (result))
|
||||
|
||||
# convert the serial number to a string
|
||||
sn_str = ""
|
||||
for i in range(0, 4):
|
||||
sni = sn.serial_no[i]
|
||||
if sni == 0:
|
||||
sn_str += "00000000"
|
||||
else:
|
||||
sn_str += hex(sni)[2:-1]
|
||||
|
||||
return sn_str
|
||||
|
||||
|
||||
# converts byte array to iq values
|
||||
def bytes2iq(data):
|
||||
values = np.array(data).astype(np.int8)
|
||||
iq = values.astype(np.float64).view(np.complex128)
|
||||
iq /= 127.5
|
||||
iq -= 1 + 1j
|
||||
|
||||
return iq
|
||||
|
||||
|
||||
# really, user shouldn't have to call this function at all
|
||||
result = libhackrf.hackrf_init()
|
||||
if result != 0:
|
||||
print("error initializing the hackrf library")
|
|
@ -1,383 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from bladerf import _bladerf
|
||||
|
||||
from ria_toolkit_oss.datatypes import Recording
|
||||
from ria_toolkit_oss.sdr import SDR
|
||||
|
||||
|
||||
class Blade(SDR):
|
||||
|
||||
def __init__(self, identifier=""):
|
||||
"""
|
||||
Initialize a BladeRF device object and connect to the SDR hardware.
|
||||
|
||||
:param identifier: Not used for BladeRF.
|
||||
|
||||
BladeRF devices cannot currently be selected with and identifier value.
|
||||
If there are multiple connected devices, the device in use may be selected randomly.
|
||||
"""
|
||||
|
||||
if identifier != "":
|
||||
print(f"Warning, radio identifier {identifier} provided for Blade but will not be used.")
|
||||
|
||||
uut = self._probe_bladerf()
|
||||
|
||||
if uut is None:
|
||||
print("No bladeRFs detected. Exiting.")
|
||||
self._shutdown(error=-1, board=None)
|
||||
|
||||
print(uut)
|
||||
|
||||
self.device = _bladerf.BladeRF(uut)
|
||||
self._print_versions(device=self.device)
|
||||
|
||||
super().__init__()
|
||||
|
||||
def _shutdown(self, error=0, board=None):
|
||||
print("Shutting down with error code: " + str(error))
|
||||
if board is not None:
|
||||
board.close()
|
||||
|
||||
# TODO why does this create an error under any conditions?
|
||||
raise OSError("Shutdown initiated with error code: {}".format(error))
|
||||
|
||||
def _probe_bladerf(self):
|
||||
device = None
|
||||
print("Searching for bladeRF devices...")
|
||||
try:
|
||||
devinfos = _bladerf.get_device_list()
|
||||
if len(devinfos) == 1:
|
||||
device = "{backend}:device={usb_bus}:{usb_addr}".format(**devinfos[0]._asdict())
|
||||
print("Found bladeRF device: " + str(device))
|
||||
if len(devinfos) > 1:
|
||||
print("Unsupported feature: more than one bladeRFs detected.")
|
||||
print("\n".join([str(devinfo) for devinfo in devinfos]))
|
||||
self._shutdown(error=-1, board=None)
|
||||
except _bladerf.BladeRFError:
|
||||
print("No bladeRF devices found.")
|
||||
pass
|
||||
return device
|
||||
|
||||
def _print_versions(self, device=None):
|
||||
print("libbladeRF version:\t" + str(_bladerf.version()))
|
||||
if device is not None:
|
||||
print("Firmware version:\t" + str(device.get_fw_version()))
|
||||
print("FPGA version:\t\t" + str(device.get_fpga_version()))
|
||||
return 0
|
||||
|
||||
def close(self):
|
||||
self.device.close()
|
||||
|
||||
def init_rx(
|
||||
self,
|
||||
sample_rate: int | float,
|
||||
center_frequency: int | float,
|
||||
gain: int,
|
||||
channel: int,
|
||||
buffer_size: Optional[int] = 8192,
|
||||
gain_mode: Optional[str] = "absolute",
|
||||
):
|
||||
"""
|
||||
Initializes the BladeRF for receiving.
|
||||
|
||||
:param sample_rate: The sample rate for receiving.
|
||||
:type sample_rate: int or float
|
||||
:param center_frequency: The center frequency of the recording.
|
||||
:type center_frequency: int or float
|
||||
:param gain: The gain set for receiving on the BladeRF
|
||||
:type gain: int
|
||||
:param channel: The channel the BladeRF is set to.
|
||||
:type channel: int
|
||||
:param buffer_size: The buffer size during receive. Defaults to 8192.
|
||||
:type buffer_size: int
|
||||
"""
|
||||
print("Initializing RX")
|
||||
|
||||
# Configure BladeRF
|
||||
self._set_rx_channel(channel)
|
||||
self._set_rx_sample_rate(sample_rate)
|
||||
self._set_rx_center_frequency(center_frequency)
|
||||
self._set_rx_gain(channel, gain, gain_mode)
|
||||
self._set_rx_buffer_size(buffer_size)
|
||||
|
||||
bw = self.rx_sample_rate
|
||||
if bw < 200000:
|
||||
bw = 200000
|
||||
elif bw > 56000000:
|
||||
bw = 56000000
|
||||
self.rx_ch.bandwidth = bw
|
||||
|
||||
self._rx_initialized = True
|
||||
self._tx_initialized = False
|
||||
|
||||
def init_tx(
|
||||
self,
|
||||
sample_rate: int | float,
|
||||
center_frequency: int | float,
|
||||
gain: int,
|
||||
channel: int,
|
||||
buffer_size: Optional[int] = 8192,
|
||||
gain_mode: Optional[str] = "absolute",
|
||||
):
|
||||
"""
|
||||
Initializes the BladeRF for transmitting.
|
||||
|
||||
:param sample_rate: The sample rate for transmitting.
|
||||
:type sample_rate: int or float
|
||||
:param center_frequency: The center frequency of the recording.
|
||||
:type center_frequency: int or float
|
||||
:param gain: The gain set for transmitting on the BladeRF
|
||||
:type gain: int
|
||||
:param channel: The channel the BladeRF is set to.
|
||||
:type channel: int
|
||||
:param buffer_size: The buffer size during transmission. Defaults to 8192.
|
||||
:type buffer_size: int
|
||||
"""
|
||||
|
||||
# Configure BladeRF
|
||||
self._set_tx_channel(channel)
|
||||
self._set_tx_sample_rate(sample_rate)
|
||||
self._set_tx_center_frequency(center_frequency)
|
||||
self._set_tx_gain(channel=channel, gain=gain, gain_mode=gain_mode)
|
||||
self._set_tx_buffer_size(buffer_size)
|
||||
|
||||
bw = self.tx_sample_rate
|
||||
if bw < 200000:
|
||||
bw = 200000
|
||||
elif bw > 56000000:
|
||||
bw = 56000000
|
||||
self.tx_ch.bandwidth = bw
|
||||
|
||||
if self.device is None:
|
||||
print("TX: Invalid device handle.")
|
||||
return -1
|
||||
|
||||
if self.tx_channel is None:
|
||||
print("TX: Invalid channel.")
|
||||
return -1
|
||||
|
||||
self._tx_initialized = True
|
||||
self._rx_initialized = False
|
||||
return 0
|
||||
|
||||
def _stream_rx(self, callback):
|
||||
if not self._rx_initialized:
|
||||
raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
|
||||
|
||||
# Setup synchronous stream
|
||||
self.device.sync_config(
|
||||
layout=_bladerf.ChannelLayout.RX_X1,
|
||||
fmt=_bladerf.Format.SC16_Q11,
|
||||
num_buffers=16,
|
||||
buffer_size=self.rx_buffer_size,
|
||||
num_transfers=8,
|
||||
stream_timeout=3500000000,
|
||||
)
|
||||
|
||||
self.rx_ch.enable = True
|
||||
self.bytes_per_sample = 4
|
||||
|
||||
print("Blade Starting RX...")
|
||||
self._enable_rx = True
|
||||
|
||||
while self._enable_rx:
|
||||
# Create receive buffer and read in samples to buffer
|
||||
# Add them to a list to convert and save after stream is finished
|
||||
buffer = bytearray(self.rx_buffer_size * self.bytes_per_sample)
|
||||
self.device.sync_rx(buffer, self.rx_buffer_size)
|
||||
signal = self._convert_rx_samples(buffer)
|
||||
# samples = convert_to_2xn(signal)
|
||||
self.buffer = buffer
|
||||
# send callback complex signal
|
||||
callback(buffer=signal, metadata=None)
|
||||
|
||||
# Disable module
|
||||
print("Blade RX Completed.")
|
||||
self.rx_ch.enable = False
|
||||
|
||||
def record(self, num_samples):
|
||||
if not self._rx_initialized:
|
||||
raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()")
|
||||
|
||||
# Setup synchronous stream
|
||||
self.device.sync_config(
|
||||
layout=_bladerf.ChannelLayout.RX_X1,
|
||||
fmt=_bladerf.Format.SC16_Q11,
|
||||
num_buffers=16,
|
||||
buffer_size=self.rx_buffer_size,
|
||||
num_transfers=8,
|
||||
stream_timeout=3500000000,
|
||||
)
|
||||
|
||||
self.rx_ch.enable = True
|
||||
self.bytes_per_sample = 4
|
||||
|
||||
print("Blade Starting RX...")
|
||||
self._enable_rx = True
|
||||
|
||||
store_array = np.zeros((1, (num_samples // self.rx_buffer_size + 1) * self.rx_buffer_size), dtype=np.complex64)
|
||||
|
||||
for i in range(num_samples // self.rx_buffer_size + 1):
|
||||
# Create receive buffer and read in samples to buffer
|
||||
# Add them to a list to convert and save after stream is finished
|
||||
buffer = bytearray(self.rx_buffer_size * self.bytes_per_sample)
|
||||
self.device.sync_rx(buffer, self.rx_buffer_size)
|
||||
signal = self._convert_rx_samples(buffer)
|
||||
# samples = convert_to_2xn(signal)
|
||||
store_array[:, i * self.rx_buffer_size : (i + 1) * self.rx_buffer_size] = signal
|
||||
|
||||
# Disable module
|
||||
print("Blade RX Completed.")
|
||||
self.rx_ch.enable = False
|
||||
metadata = {
|
||||
"source": self.__class__.__name__,
|
||||
"sample_rate": self.rx_sample_rate,
|
||||
"center_frequency": self.rx_center_frequency,
|
||||
"gain": self.rx_gain,
|
||||
}
|
||||
|
||||
return Recording(data=store_array[:, :num_samples], metadata=metadata)
|
||||
|
||||
def _stream_tx(self, callback):
|
||||
|
||||
# Setup stream
|
||||
self.device.sync_config(
|
||||
layout=_bladerf.ChannelLayout.TX_X1,
|
||||
fmt=_bladerf.Format.SC16_Q11,
|
||||
num_buffers=16,
|
||||
buffer_size=8192,
|
||||
num_transfers=8,
|
||||
stream_timeout=3500,
|
||||
)
|
||||
|
||||
# Enable module
|
||||
self.tx_ch.enable = True
|
||||
self._enable_tx = True
|
||||
|
||||
print("Blade Starting TX...")
|
||||
|
||||
while self._enable_tx:
|
||||
buffer = callback(self.tx_buffer_size) # [0]
|
||||
byte_array = self._convert_tx_samples(buffer)
|
||||
self.device.sync_tx(byte_array, len(buffer))
|
||||
|
||||
# Disable module
|
||||
print("Blade TX Completed.")
|
||||
self.tx_ch.enable = False
|
||||
|
||||
def _convert_rx_samples(self, samples):
|
||||
samples = np.frombuffer(samples, dtype=np.int16).astype(np.float32)
|
||||
samples /= 2048
|
||||
samples = samples[::2] + 1j * samples[1::2]
|
||||
return samples
|
||||
|
||||
def _convert_tx_samples(self, samples):
|
||||
tx_samples = np.empty(samples.size * 2, dtype=np.float32)
|
||||
tx_samples[::2] = np.real(samples) # Real part
|
||||
tx_samples[1::2] = np.imag(samples) # Imaginary part
|
||||
|
||||
tx_samples *= 2048
|
||||
tx_samples = tx_samples.astype(np.int16)
|
||||
byte_array = tx_samples.tobytes()
|
||||
|
||||
return byte_array
|
||||
|
||||
def _set_rx_channel(self, channel):
|
||||
self.rx_channel = channel
|
||||
self.rx_ch = self.device.Channel(_bladerf.CHANNEL_RX(channel))
|
||||
print(f"\nBlade channel = {self.rx_ch}")
|
||||
|
||||
def _set_rx_sample_rate(self, sample_rate):
|
||||
self.rx_sample_rate = sample_rate
|
||||
self.rx_ch.sample_rate = self.rx_sample_rate
|
||||
print(f"Blade sample rate = {self.rx_ch.sample_rate}")
|
||||
|
||||
def _set_rx_center_frequency(self, center_frequency):
|
||||
self.rx_center_frequency = center_frequency
|
||||
self.rx_ch.frequency = center_frequency
|
||||
print(f"Blade center frequency = {self.rx_ch.frequency}")
|
||||
|
||||
def _set_rx_gain(self, channel, gain, gain_mode):
|
||||
|
||||
rx_gain_min = self.device.get_gain_range(channel)[0]
|
||||
rx_gain_max = self.device.get_gain_range(channel)[1]
|
||||
|
||||
if gain_mode == "relative":
|
||||
if gain > 0:
|
||||
raise ValueError(
|
||||
"When gain_mode = 'relative', gain must be < 0. This sets \
|
||||
the gain relative to the maximum possible gain."
|
||||
)
|
||||
else:
|
||||
abs_gain = rx_gain_max + gain
|
||||
else:
|
||||
abs_gain = gain
|
||||
|
||||
if abs_gain < rx_gain_min or abs_gain > rx_gain_max:
|
||||
abs_gain = min(max(gain, rx_gain_min), rx_gain_max)
|
||||
print(f"Gain {abs_gain} out of range for Blade.")
|
||||
print(f"Gain range: {rx_gain_min} to {rx_gain_max} dB")
|
||||
|
||||
self.rx_gain = abs_gain
|
||||
self.rx_ch.gain = abs_gain
|
||||
|
||||
print(f"Blade gain = {self.rx_ch.gain}")
|
||||
|
||||
def _set_rx_buffer_size(self, buffer_size):
|
||||
self.rx_buffer_size = buffer_size
|
||||
|
||||
def _set_tx_channel(self, channel):
|
||||
self.tx_channel = channel
|
||||
self.tx_ch = self.device.Channel(_bladerf.CHANNEL_TX(self.tx_channel))
|
||||
print(f"\nBlade channel = {self.tx_ch}")
|
||||
|
||||
def _set_tx_sample_rate(self, sample_rate):
|
||||
self.tx_sample_rate = sample_rate
|
||||
self.tx_ch.sample_rate = self.tx_sample_rate
|
||||
print(f"Blade sample rate = {self.tx_ch.sample_rate}")
|
||||
|
||||
def _set_tx_center_frequency(self, center_frequency):
|
||||
self.tx_center_frequency = center_frequency
|
||||
self.tx_ch.frequency = center_frequency
|
||||
print(f"Blade center frequency = {self.tx_ch.frequency}")
|
||||
|
||||
def _set_tx_gain(self, channel, gain, gain_mode):
|
||||
|
||||
tx_gain_min = self.device.get_gain_range(channel)[0]
|
||||
tx_gain_max = self.device.get_gain_range(channel)[1]
|
||||
|
||||
if gain_mode == "relative":
|
||||
if gain > 0:
|
||||
raise ValueError(
|
||||
"When gain_mode = 'relative', gain must be < 0. This sets\
|
||||
the gain relative to the maximum possible gain."
|
||||
)
|
||||
else:
|
||||
abs_gain = tx_gain_max + gain
|
||||
else:
|
||||
abs_gain = gain
|
||||
|
||||
if abs_gain < tx_gain_min or abs_gain > tx_gain_max:
|
||||
abs_gain = min(max(gain, tx_gain_min), tx_gain_max)
|
||||
print(f"Gain {abs_gain} out of range for Blade.")
|
||||
print(f"Gain range: {tx_gain_min} to {tx_gain_max} dB")
|
||||
|
||||
self.tx_gain = abs_gain
|
||||
self.tx_ch.gain = abs_gain
|
||||
|
||||
print(f"Blade gain = {self.tx_ch.gain}")
|
||||
|
||||
def _set_tx_buffer_size(self, buffer_size):
|
||||
self.tx_buffer_size = buffer_size
|
||||
|
||||
def set_clock_source(self, source):
|
||||
if source.lower() == "external":
|
||||
self.device.set_pll_enable(True)
|
||||
elif source.lower() == "internal":
|
||||
print("Disabling PLL")
|
||||
self.device.set_pll_enable(False)
|
||||
|
||||
print(f"Clock source set to {self.device.get_clock_select()}")
|
||||
print(f"PLL Reference set to {self.device.get_pll_refclk()}")
|
|
@ -1,158 +0,0 @@
|
|||
import time
|
||||
import warnings
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ria_toolkit_oss.datatypes.recording import Recording
|
||||
from ria_toolkit_oss.sdr._external.libhackrf import HackRF as hrf
|
||||
from ria_toolkit_oss.sdr.sdr import SDR
|
||||
|
||||
|
||||
class HackRF(SDR):
|
||||
def __init__(self, identifier=""):
|
||||
"""
|
||||
Initialize a HackRF device object and connect to the SDR hardware.
|
||||
|
||||
:param identifier: Not used for HackRF.
|
||||
|
||||
HackRF devices cannot currently be selected with and identifier value.
|
||||
If there are multiple connected devices, the device in use may be selected randomly.
|
||||
"""
|
||||
|
||||
if identifier != "":
|
||||
print(f"Warning, radio identifier {identifier} provided for HackRF but will not be used.")
|
||||
|
||||
print("Initializing HackRF radio.")
|
||||
try:
|
||||
super().__init__()
|
||||
|
||||
self.radio = hrf()
|
||||
print("Successfully found HackRF radio.")
|
||||
except Exception as e:
|
||||
print("Failed to find HackRF radio.")
|
||||
raise e
|
||||
|
||||
super().__init__()
|
||||
|
||||
def init_rx(self, sample_rate, center_frequency, gain, channel, gain_mode):
|
||||
self._tx_initialized = False
|
||||
self._rx_initialized = True
|
||||
return NotImplementedError("RX not yet implemented for HackRF")
|
||||
|
||||
def init_tx(
|
||||
self,
|
||||
sample_rate: int | float,
|
||||
center_frequency: int | float,
|
||||
gain: int,
|
||||
channel: int,
|
||||
gain_mode: Optional[str] = "absolute",
|
||||
):
|
||||
"""
|
||||
Initializes the HackRF for transmitting.
|
||||
|
||||
:param sample_rate: The sample rate for transmitting.
|
||||
:type sample_rate: int or float
|
||||
:param center_frequency: The center frequency of the recording.
|
||||
:type center_frequency: int or float
|
||||
:param gain: The gain set for transmitting on the HackRF
|
||||
:type gain: int
|
||||
:param channel: The channel the HackRF is set to. (Not actually used)
|
||||
:type channel: int
|
||||
:param buffer_size: The buffer size during transmit. Defaults to 10000.
|
||||
:type buffer_size: int
|
||||
"""
|
||||
|
||||
print("Initializing TX")
|
||||
self.tx_sample_rate = sample_rate
|
||||
self.radio.sample_rate = int(sample_rate)
|
||||
print(f"HackRF sample rate = {self.radio.sample_rate}")
|
||||
|
||||
self.tx_center_frequency = center_frequency
|
||||
self.radio.center_freq = int(center_frequency)
|
||||
print(f"HackRF center frequency = {self.radio.center_freq}")
|
||||
|
||||
self.radio.enable_amp()
|
||||
|
||||
tx_gain_min = 0
|
||||
tx_gain_max = 47
|
||||
if gain_mode == "relative":
|
||||
if gain > 0:
|
||||
raise ValueError(
|
||||
"When gain_mode = 'relative', gain must be < 0. This \
|
||||
sets the gain relative to the maximum possible gain."
|
||||
)
|
||||
else:
|
||||
abs_gain = tx_gain_max + gain
|
||||
else:
|
||||
abs_gain = gain
|
||||
|
||||
if abs_gain < tx_gain_min or abs_gain > tx_gain_max:
|
||||
abs_gain = min(max(gain, tx_gain_min), tx_gain_max)
|
||||
print(f"Gain {gain} out of range for Pluto.")
|
||||
print(f"Gain range: {tx_gain_min} to {tx_gain_max} dB")
|
||||
|
||||
self.radio.txvga_gain = abs_gain
|
||||
print(f"HackRF gain = {self.radio.txvga_gain}")
|
||||
|
||||
self._tx_initialized = True
|
||||
self._rx_initialized = False
|
||||
|
||||
def tx_recording(
|
||||
self,
|
||||
recording: Recording | np.ndarray,
|
||||
num_samples: Optional[int] = None,
|
||||
tx_time: Optional[int | float] = None,
|
||||
):
|
||||
"""
|
||||
Transmit the given iq samples from the provided recording.
|
||||
init_tx() must be called before this function.
|
||||
|
||||
:param recording: The recording to transmit.
|
||||
:type recording: Recording or np.ndarray
|
||||
:param num_samples: The number of samples to transmit, will repeat or
|
||||
truncate the recording to this length. Defaults to None.
|
||||
:type num_samples: int, optional
|
||||
:param tx_time: The time to transmit, will repeat or truncate the
|
||||
recording to this length. Defaults to None.
|
||||
:type tx_time: int or float, optional
|
||||
"""
|
||||
if num_samples is not None and tx_time is not None:
|
||||
raise ValueError("Only input one of num_samples or tx_time")
|
||||
elif num_samples is not None:
|
||||
tx_time = num_samples / self.tx_sample_rate
|
||||
elif tx_time is not None:
|
||||
pass
|
||||
else:
|
||||
tx_time = len(recording) / self.tx_sample_rate
|
||||
|
||||
if isinstance(recording, np.ndarray):
|
||||
samples = recording
|
||||
elif isinstance(recording, Recording):
|
||||
if len(recording.data) > 1:
|
||||
warnings.warn("Recording object is multichannel, only channel 0 data was used for transmission")
|
||||
|
||||
samples = recording.data[0]
|
||||
|
||||
samples = samples.astype(np.complex64, copy=False)
|
||||
|
||||
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)
|
|
@ -1,502 +0,0 @@
|
|||
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")
|
|
@ -1,375 +0,0 @@
|
|||
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
|
|
@ -1,552 +0,0 @@
|
|||
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
|
2
tox.ini
2
tox.ini
|
@ -24,7 +24,7 @@ commands =
|
|||
[flake8]
|
||||
max-line-length = 119
|
||||
extend-ignore = W503, E203, E701
|
||||
exclude = .git, .riahub, build, dist, docs, venv, .venv, env, .env, .idea, .vscode, .tox, _external
|
||||
exclude = .git, .github, build, dist, docs, venv, .venv, env, .env, .idea, .vscode, .tox
|
||||
max-complexity = 15
|
||||
per-file-ignores = __init__.py:F401
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user