updates_and_fixes #12

Merged
madrigal merged 9 commits from updates_and_fixes into main 2025-11-18 15:01:25 -05:00
Showing only changes of commit 96d864aa0b - Show all commits

View File

@ -1,4 +1,4 @@
import time import gc
import warnings import warnings
from typing import Optional from typing import Optional
@ -6,7 +6,7 @@ import numpy as np
from bladerf import _bladerf from bladerf import _bladerf
from ria_toolkit_oss.datatypes import Recording from ria_toolkit_oss.datatypes import Recording
from ria_toolkit_oss.sdr import SDR from ria_toolkit_oss.sdr import SDR, SDRError, SDRParameterError
class Blade(SDR): class Blade(SDR):
@ -22,7 +22,7 @@ class Blade(SDR):
""" """
if identifier != "": if identifier != "":
print(f"Warning, radio identifier {identifier} provided for Blade but will not be used.") warnings.warn(f"Blade: Identifier '{identifier}' will be ignored", UserWarning)
uut = self._probe_bladerf() uut = self._probe_bladerf()
@ -34,6 +34,7 @@ class Blade(SDR):
self.device = _bladerf.BladeRF(uut) self.device = _bladerf.BladeRF(uut)
self._print_versions(device=self.device) self._print_versions(device=self.device)
self.bytes_per_sample = 4
super().__init__() super().__init__()
@ -42,8 +43,10 @@ class Blade(SDR):
if board is not None: if board is not None:
board.close() board.close()
# TODO why does this create an error under any conditions? if error != 0:
raise OSError("Shutdown initiated with error code: {}".format(error)) raise OSError(f"BladeRF shutdown with error code: {error}")
else:
print("BladeRF shutdown successfully")
def _probe_bladerf(self): def _probe_bladerf(self):
device = None device = None
@ -85,24 +88,25 @@ class Blade(SDR):
:type sample_rate: int or float :type sample_rate: int or float
:param center_frequency: The center frequency of the recording. :param center_frequency: The center frequency of the recording.
:type center_frequency: int or float :type center_frequency: int or float
:param gain: The gain set for receiving on the BladeRF :param gain: The gain set for receiving on the BladeRF.
:type gain: int :type gain: int
:param channel: The channel the BladeRF is set to. :param channel: The channel the BladeRF is set to.
:type channel: int :type channel: int
:param buffer_size: The buffer size during receive. Defaults to 8192. :param buffer_size: The buffer size during receive. Defaults to 8192.
:type buffer_size: int :type buffer_size: int
:param gain_mode: 'absolute' passes gain directly to the sdr, :param gain_mode: 'absolute' passes gain directly to the SDR;
'relative' means that gain should be a negative value, and it will be subtracted from the max gain (60). 'relative' means that gain should be a negative value, and it will be subtracted from the max gain (60).
:type gain_mode: str :type gain_mode: str
""" """
print("Initializing RX") print("Initializing RX")
# Configure BladeRF # Configure BladeRF
self._set_rx_channel(channel) self.set_rx_channel(channel)
self._set_rx_sample_rate(sample_rate) self.set_rx_sample_rate(sample_rate)
self._set_rx_center_frequency(center_frequency) self.set_rx_center_frequency(center_frequency)
self._set_rx_gain(channel, gain, gain_mode) self.set_rx_gain(channel, gain, gain_mode)
self._set_rx_buffer_size(buffer_size) self.set_rx_buffer_size(buffer_size)
bw = self.rx_sample_rate bw = self.rx_sample_rate
if bw < 200000: if bw < 200000:
@ -128,10 +132,8 @@ class Blade(SDR):
stream_timeout=3500000000, stream_timeout=3500000000,
) )
self.rx_ch.enable = True
self.bytes_per_sample = 4
print("Blade Starting RX...") print("Blade Starting RX...")
self.rx_ch.enable = True
self._enable_rx = True self._enable_rx = True
while self._enable_rx: while self._enable_rx:
@ -148,18 +150,34 @@ class Blade(SDR):
print("Blade RX Completed.") print("Blade RX Completed.")
self.rx_ch.enable = False self.rx_ch.enable = False
def record(self, num_samples: Optional[int] = None, rx_time: Optional[int | float] = None): def record(
self,
num_samples: Optional[int] = None,
rx_time: Optional[int | float] = None,
) -> Recording:
"""
Create a radio recording (iq samples and metadata) of a given length from the Blade.
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.
: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: if not self._rx_initialized:
raise RuntimeError("RX was not initialized. init_rx() must be called before _stream_rx() or record()") 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: if num_samples is not None and rx_time is not None:
raise ValueError("Only input one of num_samples or rx_time") raise SDRParameterError("Only input one of num_samples or rx_time")
elif num_samples is not None: elif num_samples is not None:
self._num_samples_to_record = num_samples self._num_samples_to_record = num_samples
elif rx_time is not None: elif rx_time is not None:
self._num_samples_to_record = int(rx_time * self.rx_sample_rate) self._num_samples_to_record = int(rx_time * self.rx_sample_rate)
else: else:
raise ValueError("Must provide input of one of num_samples or rx_time") raise SDRParameterError("Must provide input of one of num_samples or rx_time")
# Setup synchronous stream # Setup synchronous stream
self.device.sync_config( self.device.sync_config(
@ -171,11 +189,10 @@ class Blade(SDR):
stream_timeout=3500000000, stream_timeout=3500000000,
) )
self.rx_ch.enable = True
self.bytes_per_sample = 4
print("Blade Starting RX...") print("Blade Starting RX...")
self._enable_rx = True with self._param_lock:
self._enable_rx = True
self.rx_ch.enable = True
store_array = np.zeros( store_array = np.zeros(
(1, (self._num_samples_to_record // self.rx_buffer_size + 1) * self.rx_buffer_size), dtype=np.complex64 (1, (self._num_samples_to_record // self.rx_buffer_size + 1) * self.rx_buffer_size), dtype=np.complex64
@ -191,7 +208,8 @@ class Blade(SDR):
# Disable module # Disable module
print("Blade RX Completed.") print("Blade RX Completed.")
self.rx_ch.enable = False with self._param_lock:
self.rx_ch.enable = False
metadata = { metadata = {
"source": self.__class__.__name__, "source": self.__class__.__name__,
"sample_rate": self.rx_sample_rate, "sample_rate": self.rx_sample_rate,
@ -207,7 +225,7 @@ class Blade(SDR):
center_frequency: int | float, center_frequency: int | float,
gain: int, gain: int,
channel: int, channel: int,
buffer_size: Optional[int] = 8192, buffer_size: Optional[int] = 32768,
gain_mode: Optional[str] = "absolute", gain_mode: Optional[str] = "absolute",
): ):
""" """
@ -224,16 +242,24 @@ class Blade(SDR):
:param buffer_size: The buffer size during transmission. Defaults to 8192. :param buffer_size: The buffer size during transmission. Defaults to 8192.
:type buffer_size: int :type buffer_size: int
:param gain_mode: 'absolute' passes gain directly to the sdr, :param gain_mode: 'absolute' passes gain directly to the sdr,
'relative' means that gain should be a negative value, and it will be subtracted from the max gain (60). 'relative' means that gain should be a negative value, and it will be subtracted from the max gain (60).
:type gain_mode: str :type gain_mode: str
:return: 0 if successful, -1 if there's an error.
:rtype: int
""" """
# Configure BladeRF # Configure BladeRF
self._set_tx_channel(channel) self.set_tx_channel(channel)
self._set_tx_sample_rate(sample_rate) self.set_tx_sample_rate(sample_rate)
self._set_tx_center_frequency(center_frequency) self.set_tx_center_frequency(center_frequency)
self._set_tx_gain(channel=channel, gain=gain, gain_mode=gain_mode) self.set_tx_gain(channel=channel, gain=gain, gain_mode=gain_mode)
self._set_tx_buffer_size(buffer_size) self.set_tx_buffer_size(buffer_size)
if self.tx_sample_rate >= 7.5e6 and self.tx_buffer_size < 65536:
warnings.warn(
"Blade: For high sample rates, a buffer size of 65536, 131072, or 262144 is recommended", UserWarning
)
bw = self.tx_sample_rate bw = self.tx_sample_rate
if bw < 200000: if bw < 200000:
@ -302,13 +328,13 @@ class Blade(SDR):
""" """
if num_samples is not None and tx_time is not None: if num_samples is not None and tx_time is not None:
raise ValueError("Only input one of num_samples or tx_time") raise SDRParameterError("Only input one of num_samples or tx_time")
elif num_samples is not None: elif num_samples is not None:
tx_time = num_samples / self.tx_sample_rate
elif tx_time is not None:
pass pass
elif tx_time is not None:
num_samples = int(tx_time * self.tx_sample_rate)
else: else:
tx_time = len(recording) / self.tx_sample_rate num_samples = len(recording)
if isinstance(recording, np.ndarray): if isinstance(recording, np.ndarray):
samples = recording samples = recording
@ -317,9 +343,15 @@ class Blade(SDR):
warnings.warn("Recording object is multichannel, only channel 0 data was used for transmission") warnings.warn("Recording object is multichannel, only channel 0 data was used for transmission")
samples = recording.data[0] samples = recording.data[0]
else: else:
raise TypeError("recording must be np.ndarray or Recording") raise SDRParameterError("recording must be np.ndarray or Recording")
samples = samples.astype(np.complex64, copy=False) samples = samples.astype(np.complex64, copy=False)
tx_bytes = self._convert_tx_samples(samples)
# Transmit in chunks
samples_sent = 0
len_samples = len(samples)
chunk_size = self.tx_buffer_size
# Setup stream # Setup stream
self.device.sync_config( self.device.sync_config(
@ -335,26 +367,21 @@ class Blade(SDR):
self.tx_ch.enable = True self.tx_ch.enable = True
print("Blade Starting TX...") print("Blade Starting TX...")
# Transmit samples - repeat as needed for the duration
start_time = time.time()
sample_index = 0
try: try:
while time.time() - start_time < tx_time: while samples_sent < num_samples:
# Get next chunk this_chunk_size = min(chunk_size, num_samples - samples_sent)
chunk_size = min(self.tx_buffer_size, len(samples) - sample_index)
if chunk_size == 0:
# Reached end, loop back
sample_index = 0
chunk_size = min(self.tx_buffer_size, len(samples))
chunk = samples[sample_index : sample_index + chunk_size] start_idx = (samples_sent % len_samples) * self.bytes_per_sample
sample_index += chunk_size end_idx = start_idx + this_chunk_size * self.bytes_per_sample
end_idx %= len_samples * self.bytes_per_sample
# Convert and transmit if end_idx > start_idx:
byte_array = self._convert_tx_samples(chunk) chunk_bytes_arr = tx_bytes[start_idx:end_idx]
self.device.sync_tx(byte_array, len(chunk)) else:
chunk_bytes_arr = tx_bytes[start_idx:] + tx_bytes[:end_idx]
self.device.sync_tx(chunk_bytes_arr, this_chunk_size)
samples_sent += this_chunk_size
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nTransmission interrupted by user") print("\nTransmission interrupted by user")
@ -384,73 +411,146 @@ class Blade(SDR):
byte_array = tx_samples.tobytes() byte_array = tx_samples.tobytes()
return byte_array return byte_array
def _set_rx_channel(self, channel): def set_rx_channel(self, channel):
if channel != 0 and channel != 1:
raise SDRParameterError("Channel must be either 0 or 1.")
self.rx_channel = channel self.rx_channel = channel
self.rx_ch = self.device.Channel(_bladerf.CHANNEL_RX(channel)) self.rx_ch = self.device.Channel(_bladerf.CHANNEL_RX(channel))
print(f"\nBlade channel = {self.rx_ch}") print(f"\nBlade channel = {self.rx_ch}")
def _set_rx_sample_rate(self, sample_rate): def set_rx_sample_rate(self, sample_rate):
self.rx_sample_rate = sample_rate """
self.rx_ch.sample_rate = self.rx_sample_rate Set the sample rate of the receiver.
print(f"Blade sample rate = {self.rx_ch.sample_rate}") Not callable during recording; Blade requires stream stop/restart to change sample rate.
"""
def _set_rx_center_frequency(self, center_frequency): with self._param_lock:
self.rx_center_frequency = center_frequency if hasattr(self, "rx_channel"):
self.rx_ch.frequency = center_frequency range_list = self.device.get_sample_rate_range(self.rx_channel)
print(f"Blade center frequency = {self.rx_ch.frequency}") min_rate, max_rate = range_list[0], range_list[1]
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: else:
abs_gain = rx_gain_max + gain raise SDRError("Must set channel before setting center frequency")
else:
abs_gain = gain
if abs_gain < rx_gain_min or abs_gain > rx_gain_max: if sample_rate < min_rate or sample_rate > max_rate:
abs_gain = min(max(gain, rx_gain_min), rx_gain_max) raise SDRParameterError(
print(f"Gain {abs_gain} out of range for Blade.") f"{self.__class__.__name__}: Sample rate {sample_rate/1e6:.3f} Msps "
print(f"Gain range: {rx_gain_min} to {rx_gain_max} dB") f"out of range: [{min_rate/1e6:.3f} - {max_rate/1e6:.3f} Msps]"
)
self.rx_gain = abs_gain self.rx_sample_rate = sample_rate
self.rx_ch.gain = abs_gain self.rx_ch.sample_rate = self.rx_sample_rate
print(f"Blade sample rate = {self.rx_ch.sample_rate}")
print(f"Blade gain = {self.rx_ch.gain}") def set_rx_center_frequency(self, center_frequency):
"""
Set the center frequency of the receiver.
Not callable during recording; Blade requires stream stop/restart to change center frequency.
"""
with self._param_lock:
if hasattr(self, "rx_channel"):
range_list = self.device.get_frequency_range(self.rx_channel)
min_rate, max_rate = range_list[0], range_list[1]
else:
raise SDRError("Must set channel before setting center frequency")
def _set_rx_buffer_size(self, buffer_size): if center_frequency < min_rate or center_frequency > max_rate:
raise SDRParameterError(
f"{self.__class__.__name__}: Center frequency {center_frequency/1e9:.3f} GHz "
f"out of range: [{min_rate/1e9:.3f} - {max_rate/1e9:.3f} GHz]"
)
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):
"""
Set the gain of the receiver.
Not callable during recording; Blade requires stream stop/restart to change gain.
"""
with self._param_lock:
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 SDRParameterError(
"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 self.rx_buffer_size = buffer_size
def _set_tx_channel(self, channel): def set_tx_channel(self, channel):
if channel != 0 and channel != 1:
raise SDRParameterError("Channel must be either 0 or 1.")
self.tx_channel = channel self.tx_channel = channel
self.tx_ch = self.device.Channel(_bladerf.CHANNEL_TX(self.tx_channel)) self.tx_ch = self.device.Channel(_bladerf.CHANNEL_TX(self.tx_channel))
print(f"\nBlade channel = {self.tx_ch}") print(f"\nBlade channel = {self.tx_ch}")
def _set_tx_sample_rate(self, sample_rate): def set_tx_sample_rate(self, sample_rate):
if hasattr(self, "tx_channel"):
range_list = self.device.get_sample_rate_range(self.tx_channel)
min_rate, max_rate = range_list[0], range_list[1]
else:
raise SDRError("Must set channel before setting center frequency")
if sample_rate < min_rate or sample_rate > max_rate:
raise SDRParameterError(
f"{self.__class__.__name__}: Sample rate {sample_rate/1e6:.3f} Msps "
f"out of range: [{min_rate/1e6:.3f} - {max_rate/1e6:.3f} Msps]"
)
if sample_rate < min_rate or sample_rate > max_rate:
raise SDRParameterError(
f"{self.__class__.__name__}: Sample rate {sample_rate/1e6:.3f} Msps "
f"out of range: [{min_rate/1e6:.3f} - {max_rate/1e6:.3f} Msps]"
)
self.tx_sample_rate = sample_rate self.tx_sample_rate = sample_rate
self.tx_ch.sample_rate = self.tx_sample_rate self.tx_ch.sample_rate = self.tx_sample_rate
print(f"Blade sample rate = {self.tx_ch.sample_rate}") print(f"Blade sample rate = {self.tx_ch.sample_rate}")
def _set_tx_center_frequency(self, center_frequency): def set_tx_center_frequency(self, center_frequency):
if hasattr(self, "tx_channel"):
range_list = self.device.get_frequency_range(self.tx_channel)
min_rate, max_rate = range_list[0], range_list[1]
else:
raise SDRError("Must set channel before setting center frequency")
if center_frequency < min_rate or center_frequency > max_rate:
raise SDRParameterError(
f"{self.__class__.__name__}: Center frequency {center_frequency/1e9:.3f} GHz "
f"out of range: [{min_rate/1e9:.3f} - {max_rate/1e9:.3f} GHz]"
)
self.tx_center_frequency = center_frequency self.tx_center_frequency = center_frequency
self.tx_ch.frequency = center_frequency self.tx_ch.frequency = center_frequency
print(f"Blade center frequency = {self.tx_ch.frequency}") print(f"Blade center frequency = {self.tx_ch.frequency}")
def _set_tx_gain(self, channel, gain, gain_mode): def set_tx_gain(self, channel, gain, gain_mode):
tx_gain_min = self.device.get_gain_range(channel)[0] tx_gain_min = self.device.get_gain_range(channel)[0]
tx_gain_max = self.device.get_gain_range(channel)[1] tx_gain_max = self.device.get_gain_range(channel)[1]
if gain_mode == "relative": if gain_mode == "relative":
if gain > 0: if gain > 0:
raise ValueError( raise SDRParameterError(
"When gain_mode = 'relative', gain must be < 0. This sets\ "When gain_mode = 'relative', gain must be < 0. This sets\
the gain relative to the maximum possible gain." the gain relative to the maximum possible gain."
) )
@ -469,7 +569,7 @@ class Blade(SDR):
print(f"Blade gain = {self.tx_ch.gain}") print(f"Blade gain = {self.tx_ch.gain}")
def _set_tx_buffer_size(self, buffer_size): def set_tx_buffer_size(self, buffer_size):
self.tx_buffer_size = buffer_size self.tx_buffer_size = buffer_size
def set_clock_source(self, source): def set_clock_source(self, source):
@ -499,4 +599,20 @@ class Blade(SDR):
print(f"BladeRF bias tee {state} on channel {channel}.") print(f"BladeRF bias tee {state} on channel {channel}.")
def close(self): def close(self):
self.device.close() if hasattr(self, "device") and self.device is not None:
try:
if hasattr(self, "tx_ch"):
self.tx_ch.enable = False
if hasattr(self, "rx_ch"):
self.rx_ch.enable = False
self.device.close()
except Exception as e:
print(f"Warning: error closing bladeRF: {e}")
finally:
del self.device
self.device = None
gc.collect()
def supports_dynamic_updates(self) -> dict:
return {"center_frequency": False, "sample_rate": False, "gain": False}