From 96d864aa0bb4468495a314298365e4cad50bba5d Mon Sep 17 00:00:00 2001 From: madrigal Date: Mon, 17 Nov 2025 11:24:54 -0500 Subject: [PATCH] Fixed shutdown and cleanup, standardized setters, and improved TX --- src/ria_toolkit_oss/sdr/blade.py | 304 +++++++++++++++++++++---------- 1 file changed, 210 insertions(+), 94 deletions(-) diff --git a/src/ria_toolkit_oss/sdr/blade.py b/src/ria_toolkit_oss/sdr/blade.py index 6bb0d03..c8e6f3f 100644 --- a/src/ria_toolkit_oss/sdr/blade.py +++ b/src/ria_toolkit_oss/sdr/blade.py @@ -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, - 'relative' means that gain should be a negative value, and it will be subtracted from the max gain (60). + :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...") - self._enable_rx = True + 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,7 +208,8 @@ class Blade(SDR): # Disable module print("Blade RX Completed.") - self.rx_ch.enable = False + with self._param_lock: + self.rx_ch.enable = False metadata = { "source": self.__class__.__name__, "sample_rate": self.rx_sample_rate, @@ -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", ): """ @@ -224,16 +242,24 @@ class Blade(SDR): :param buffer_size: The buffer size during transmission. Defaults to 8192. :type buffer_size: int :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 + + :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,73 +411,146 @@ 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): - 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." - ) + 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: - abs_gain = rx_gain_max + gain - else: - abs_gain = gain + raise SDRError("Must set channel before setting center frequency") - 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") + 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_gain = abs_gain - self.rx_ch.gain = abs_gain + 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}") - 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 - 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): - 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}