Fixed shutdown and cleanup, standardized setters, and improved TX

This commit is contained in:
M madrigal 2025-11-17 11:24:54 -05:00
parent c673967a90
commit 96d864aa0b

View File

@ -1,4 +1,4 @@
import time
import gc
import warnings
from typing import Optional
@ -6,7 +6,7 @@ import numpy as np
from bladerf import _bladerf
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):
@ -22,7 +22,7 @@ class Blade(SDR):
"""
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()
@ -34,6 +34,7 @@ class Blade(SDR):
self.device = _bladerf.BladeRF(uut)
self._print_versions(device=self.device)
self.bytes_per_sample = 4
super().__init__()
@ -42,8 +43,10 @@ class Blade(SDR):
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))
if error != 0:
raise OSError(f"BladeRF shutdown with error code: {error}")
else:
print("BladeRF shutdown successfully")
def _probe_bladerf(self):
device = None
@ -85,24 +88,25 @@ class Blade(SDR):
: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
: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
: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).
:type gain_mode: str
"""
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)
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:
@ -128,10 +132,8 @@ class Blade(SDR):
stream_timeout=3500000000,
)
self.rx_ch.enable = True
self.bytes_per_sample = 4
print("Blade Starting RX...")
self.rx_ch.enable = True
self._enable_rx = True
while self._enable_rx:
@ -148,18 +150,34 @@ class Blade(SDR):
print("Blade RX Completed.")
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:
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")
raise SDRParameterError("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")
raise SDRParameterError("Must provide input of one of num_samples or rx_time")
# Setup synchronous stream
self.device.sync_config(
@ -171,11 +189,10 @@ class Blade(SDR):
stream_timeout=3500000000,
)
self.rx_ch.enable = True
self.bytes_per_sample = 4
print("Blade Starting RX...")
with self._param_lock:
self._enable_rx = True
self.rx_ch.enable = True
store_array = np.zeros(
(1, (self._num_samples_to_record // self.rx_buffer_size + 1) * self.rx_buffer_size), dtype=np.complex64
@ -191,6 +208,7 @@ class Blade(SDR):
# Disable module
print("Blade RX Completed.")
with self._param_lock:
self.rx_ch.enable = False
metadata = {
"source": self.__class__.__name__,
@ -207,7 +225,7 @@ class Blade(SDR):
center_frequency: int | float,
gain: int,
channel: int,
buffer_size: Optional[int] = 8192,
buffer_size: Optional[int] = 32768,
gain_mode: Optional[str] = "absolute",
):
"""
@ -226,14 +244,22 @@ class Blade(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).
:type gain_mode: str
:return: 0 if successful, -1 if there's an error.
:rtype: 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)
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)
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
if bw < 200000:
@ -302,13 +328,13 @@ class Blade(SDR):
"""
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:
tx_time = num_samples / self.tx_sample_rate
elif tx_time is not None:
pass
elif tx_time is not None:
num_samples = int(tx_time * self.tx_sample_rate)
else:
tx_time = len(recording) / self.tx_sample_rate
num_samples = len(recording)
if isinstance(recording, np.ndarray):
samples = recording
@ -317,9 +343,15 @@ class Blade(SDR):
warnings.warn("Recording object is multichannel, only channel 0 data was used for transmission")
samples = recording.data[0]
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)
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
self.device.sync_config(
@ -335,26 +367,21 @@ class Blade(SDR):
self.tx_ch.enable = True
print("Blade Starting TX...")
# Transmit samples - repeat as needed for the duration
start_time = time.time()
sample_index = 0
try:
while time.time() - start_time < tx_time:
# Get next chunk
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))
while samples_sent < num_samples:
this_chunk_size = min(chunk_size, num_samples - samples_sent)
chunk = samples[sample_index : sample_index + chunk_size]
sample_index += chunk_size
start_idx = (samples_sent % len_samples) * self.bytes_per_sample
end_idx = start_idx + this_chunk_size * self.bytes_per_sample
end_idx %= len_samples * self.bytes_per_sample
# Convert and transmit
byte_array = self._convert_tx_samples(chunk)
self.device.sync_tx(byte_array, len(chunk))
if end_idx > start_idx:
chunk_bytes_arr = tx_bytes[start_idx:end_idx]
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:
print("\nTransmission interrupted by user")
@ -384,29 +411,70 @@ class Blade(SDR):
byte_array = tx_samples.tobytes()
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_ch = self.device.Channel(_bladerf.CHANNEL_RX(channel))
print(f"\nBlade channel = {self.rx_ch}")
def _set_rx_sample_rate(self, sample_rate):
def set_rx_sample_rate(self, sample_rate):
"""
Set the sample rate of the receiver.
Not callable during recording; Blade requires stream stop/restart to change sample rate.
"""
with self._param_lock:
if hasattr(self, "rx_channel"):
range_list = self.device.get_sample_rate_range(self.rx_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]"
)
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):
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")
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):
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 ValueError(
raise SDRParameterError(
"When gain_mode = 'relative', gain must be < 0. This sets \
the gain relative to the maximum possible gain."
)
@ -425,32 +493,64 @@ class Blade(SDR):
print(f"Blade gain = {self.rx_ch.gain}")
def _set_rx_buffer_size(self, buffer_size):
def set_rx_buffer_size(self, 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_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):
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_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):
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_ch.frequency = center_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_max = self.device.get_gain_range(channel)[1]
if gain_mode == "relative":
if gain > 0:
raise ValueError(
raise SDRParameterError(
"When gain_mode = 'relative', gain must be < 0. This sets\
the gain relative to the maximum possible gain."
)
@ -469,7 +569,7 @@ class Blade(SDR):
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
def set_clock_source(self, source):
@ -499,4 +599,20 @@ class Blade(SDR):
print(f"BladeRF bias tee {state} on channel {channel}.")
def close(self):
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}