From fefcb0e010db07f0f5ca09cc5cce32721894e352 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 00:41:57 +0300 Subject: [PATCH 1/2] Restore sync stream after enable_module() re-enables a direction libbladeRF tears down the synchronous stream when a direction is disabled: rfic_host.c calls sync_deinit() on !dir_enable, and bladerf1.c does the same. That is documented ("this will shut down the underlying asynchronous stream when enable = false"), but re-enabling the module does not bring the stream back. Every later sync_tx()/sync_rx() then fails with sync tx invalid: not initialized which gives no hint that sync_config() must be repeated. From the caller's side the radio simply looks dead: measured on a TX1 -> 50 dB pad -> RX1 loopback, the received level stopped responding to TX gain (60 dB and -30 dB both gave -44.4 dB) and 65487 of 66033 transmit calls failed. Remember the last sync_config() arguments per direction and replay them when the module is enabled again. Direction is taken from the low bit: TX channels are 1 and 3, TX layouts are 1 and 3, RX are even. Verified on hardware: the disable -> enable -> sync_tx sequence went from ERR_INVAL to OK, and transmit errors dropped from 65487 to 0. --- python_bladerf/pylibbladerf/pybladerf.pxd | 4 +++ python_bladerf/pylibbladerf/pybladerf.pyx | 32 +++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/python_bladerf/pylibbladerf/pybladerf.pxd b/python_bladerf/pylibbladerf/pybladerf.pxd index 2dd00b0..b14958c 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pxd +++ b/python_bladerf/pylibbladerf/pybladerf.pxd @@ -104,6 +104,10 @@ cdef class pybladerf_stream: cdef class PyBladerfDevice: cdef cbladerf.bladerf *__bladerf_device cdef public str serialno + # Last sync_config() arguments per direction, so the stream can be + # restored after libbladeRF tears it down on enable_module(False). + cdef dict __sync_config + cdef set __sync_torn_down cdef cbladerf.bladerf *get_ptr(self) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index e815966..595185e 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1318,6 +1318,10 @@ cdef class PyBladerfDevice: def __cinit__(self): self.__bladerf_device = NULL + # Last sync_config() per direction (0 = RX, 1 = TX) and which + # directions libbladeRF has torn down via enable_module(False). + self.__sync_config = {} + self.__sync_torn_down = set() def __dealloc__(self): global global_callbacks @@ -1656,9 +1660,29 @@ cdef class PyBladerfDevice: raise_error('pybladerf_deinterleave_stream_buffer()', result) def pybladerf_enable_module(self, channel: int, enable: bool) -> None: + # libbladeRF tears the synchronous stream down when a direction is + # disabled (rfic_host.c calls sync_deinit() on !dir_enable, and + # bladerf1.c does the same). This is documented behaviour, but + # re-enabling the module does NOT bring the stream back: every + # later bladerf_sync_tx()/sync_rx() then fails with + # "sync tx invalid: not initialized" + # which gives no hint that sync_config() has to be repeated. + # + # Remember the last configuration per direction and restore it on + # re-enable, so a disable/enable cycle keeps working. result = cbladerf.bladerf_enable_module(self.__bladerf_device, channel, enable) raise_error('pybladerf_enable_module()', result) + direction = 1 if (channel & 1) else 0 # TX channels are odd + if not enable: + self.__sync_torn_down.add(direction) + return + if direction in self.__sync_torn_down: + self.__sync_torn_down.discard(direction) + cfg = self.__sync_config.get(direction) + if cfg is not None: + self.pybladerf_sync_config(*cfg) + def pybladerf_get_timestamp(self, direction: pybladerf_direction) -> int: cdef uint64_t timestamp result = cbladerf.bladerf_get_timestamp(self.__bladerf_device, direction, ×tamp) @@ -1669,6 +1693,14 @@ cdef class PyBladerfDevice: result = cbladerf.bladerf_sync_config(self.__bladerf_device, layout, data_format, num_buffers, buffer_size, num_transfers, stream_timeout) raise_error('pybladerf_sync_config()', result) + # Keep the settings so pybladerf_enable_module() can restore the + # stream after libbladeRF tears it down on disable. + direction = 1 if (int(layout) & 1) else 0 # TX layouts are odd + self.__sync_config[direction] = (layout, data_format, num_buffers, + buffer_size, num_transfers, + stream_timeout) + self.__sync_torn_down.discard(direction) + def pybladerf_sync_tx(self, samples: np.ndarray[Any, Any], num_samples: int, metadata: pybladerf_metadata | None = None, timeout_ms: int = 0) -> None: cdef cbladerf.bladerf_metadata *c_metadata_ptr = NULL cdef pybladerf_metadata metadata_link From 187a09309599b761fc18145209db4a58ab84da75 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 01:23:18 +0300 Subject: [PATCH 2/2] Refuse metadata-format transfers that were given no metadata A stream configured with a *_META format carries per-buffer timestamps and flags. Passing metadata=None leaves libbladeRF with nowhere to report them, so the caller silently loses the timestamp it needs and bladerf_get_timestamp() keeps returning 0. Nothing in the error path points at the cause, so this reads as dead hardware rather than a mismatched call. Measured on a TX1 -> 50 dB pad -> RX1 loopback at 15.36 MSps: with the stream in a metadata format but metadata=None, the frame timestamp stayed at 762229041 across 8 consecutive reads and get_timestamp() returned 0. Consecutive gain steps then analysed the same buffer, so the receive level repeated in pairs (-34.9/-34.9, -20.2/-20.2 dBFS) and a gain ladder that is in fact monotonic came out looking broken. The stream format is already remembered per direction for the enable_module restore path, so the check costs nothing extra: sync_rx()/sync_tx() now raise instead of losing timestamps quietly. After fixing the call sites the same ladder is monotonic, with deviations of +0.1 to +0.8 dB over a 40 dB span. --- python_bladerf/pylibbladerf/pybladerf.pyx | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index 595185e..9c783a1 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1708,6 +1708,8 @@ cdef class PyBladerfDevice: if isinstance(metadata, pybladerf_metadata): metadata_link = metadata c_metadata_ptr = metadata_link.get_ptr() + else: + self.__check_metadata_required(1, 'pybladerf_sync_tx') cdef unsigned int c_num_samples = num_samples cdef unsigned int c_timeout_ms = timeout_ms @@ -1725,6 +1727,8 @@ cdef class PyBladerfDevice: if isinstance(metadata, pybladerf_metadata): metadata_link = metadata c_metadata_ptr = metadata_link.get_ptr() + else: + self.__check_metadata_required(0, 'pybladerf_sync_rx') cdef unsigned int c_num_samples = num_samples cdef unsigned int c_timeout_ms = timeout_ms @@ -1735,6 +1739,33 @@ cdef class PyBladerfDevice: result = cbladerf.bladerf_sync_rx(self.__bladerf_device, c_samples_ptr, c_num_samples, c_metadata_ptr, c_timeout_ms) raise_error('pybladerf_sync_rx()', result) + def __check_metadata_required(self, direction: int, caller: str) -> None: + """Refuse a metadata-format transfer that was given no metadata. + + A stream configured with a *_META format carries per-buffer + timestamps and flags. Passing metadata=None leaves libbladeRF with + nowhere to report them, so the caller silently loses the timestamp + it needs and bladerf_get_timestamp() keeps returning 0. Nothing in + the error path points at the real cause, so this reads as dead + hardware rather than a mismatched call. + + Measured on a TX1 -> 50 dB pad -> RX1 loopback: with SC16_Q11 the + frame timestamp stayed at 762229041 across 8 consecutive reads and + get_timestamp() returned 0, so consecutive gain steps analysed the + same buffer and the gain ladder came out non-monotonic. + """ + cfg = self.__sync_config.get(direction) + if cfg is None: + return + fmt = int(cfg[1]) + if fmt in (int(pybladerf_format.PYBLADERF_FORMAT_SC16_Q11_META), + int(pybladerf_format.PYBLADERF_FORMAT_SC8_Q7_META)): + raise RuntimeError( + f'{caller}(): stream is configured with a metadata format ' + f'({pybladerf_format(fmt)}) but metadata=None was passed. ' + 'Timestamps and flags would be lost silently; pass a ' + 'pybladerf_metadata instance.') + def pybladerf_init_rx_stream(self, num_buffers: int, data_format: pybladerf_format, samples_per_buffer: int, num_transfers: int) -> pybladerf_stream: cdef pybladerf_stream pystream = pybladerf_stream() cdef pybladerf_async_data* async_data = malloc(sizeof(pybladerf_async_data))