From fefcb0e010db07f0f5ca09cc5cce32721894e352 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 00:41:57 +0300 Subject: [PATCH 01/11] 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 02/11] 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)) From 92a3e6a2999a95592f228b16e294c22860ffb283 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 03:27:53 +0300 Subject: [PATCH 03/11] Expose bladerf_get_rffe_control() The RFFE control register lives in the FPGA and carries the RF front-end state that no RFIC register reflects: SPDT switch positions, per-channel enables, and the direction ENABLE/TXNRX bits. Needed to tell "an SPDT was left in its shutdown position" from "the RFIC is fine but the signal is routed nowhere". Both look identical from the RFIC side: every register reads back correct while the output is dead. Requires the matching accessor in libbladeRF. --- python_bladerf/pylibbladerf/cbladerf.pxd | 1 + python_bladerf/pylibbladerf/pybladerf.pyx | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/python_bladerf/pylibbladerf/cbladerf.pxd b/python_bladerf/pylibbladerf/cbladerf.pxd index bf8cc0b..948844c 100644 --- a/python_bladerf/pylibbladerf/cbladerf.pxd +++ b/python_bladerf/pylibbladerf/cbladerf.pxd @@ -552,6 +552,7 @@ cdef extern from 'bladeRF2.h' nogil: int bladerf_get_rfic_register(bladerf *dev, uint16_t address, uint8_t *val) int bladerf_set_rfic_register(bladerf *dev, uint16_t address, uint8_t val) + int bladerf_get_rffe_control(bladerf *dev, uint32_t *value) int bladerf_get_rfic_temperature(bladerf *dev, float *val) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index 9c783a1..1a07a0a 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1968,6 +1968,20 @@ cdef class PyBladerfDevice: result = cbladerf.bladerf_set_rfic_register(self.__bladerf_device, address, value) raise_error('pybladerf_set_rfic_register()', result) + def pybladerf_get_rffe_control(self) -> int: + """Read the RFFE control register (FPGA, not RFIC). + + Carries the RF front-end state that no RFIC register reflects: + SPDT switch positions, per-channel enables, and the direction + ENABLE/TXNRX bits. Needed to tell a switch left in its shutdown + position from an RFIC problem -- every RFIC register can read + back correct while the signal is routed nowhere. + """ + cdef uint32_t value + result = cbladerf.bladerf_get_rffe_control(self.__bladerf_device, &value) + raise_error('pybladerf_get_rffe_control()', result) + return value + def pybladerf_get_rfic_temperature(self) -> float: cdef float value result = cbladerf.bladerf_get_rfic_temperature(self.__bladerf_device, &value) From 4ee349eafb5d2865fdfc40f97fc5d8792b482d59 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 04:20:32 +0300 Subject: [PATCH 04/11] Expose bladerf_config_gpio_read() The FPGA configuration GPIO register carries the per-format mode bits (TIMESTAMP, PACKET, 8BIT_MODE, HIGHLY_PACKED) that both directions share. perform_format_config() writes the whole word from one direction's format while perform_format_deconfig() only forgets the format and leaves the register alone, so this is where a stream configuration on one direction could clobber the mode the other one needs. The declaration was already in cbladerf.pxd; only the method was missing. Used it to rule that out for a transmitter fault: the register reads 0x00010001 with TIMESTAMP set in the working state, in the disabled state, and in the broken one alike. --- python_bladerf/pylibbladerf/pybladerf.pyx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index 1a07a0a..cb5a1bb 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1968,6 +1968,20 @@ cdef class PyBladerfDevice: result = cbladerf.bladerf_set_rfic_register(self.__bladerf_device, address, value) raise_error('pybladerf_set_rfic_register()', result) + def pybladerf_config_gpio_read(self) -> int: + """Read the FPGA configuration GPIO register. + + Carries the per-format mode bits (TIMESTAMP, PACKET, 8BIT_MODE, + HIGHLY_PACKED) that both directions share. perform_format_config() + writes the whole word from one direction's format, so this is where + a stream configuration on one direction can clobber the mode the + other direction needs. + """ + cdef uint32_t value + result = cbladerf.bladerf_config_gpio_read(self.__bladerf_device, &value) + raise_error('pybladerf_config_gpio_read()', result) + return value + def pybladerf_get_rffe_control(self) -> int: """Read the RFFE control register (FPGA, not RFIC). From 7ddaa32899414187013376e8f78a2ed54a953449 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 05:31:53 +0300 Subject: [PATCH 05/11] Expose RFIC register read/write in the wrapper Without this the AD9361's internal state is unobservable from Python: the ENSM state, the digital datapath status and the filter enables live only in RFIC registers, while every FPGA-side register can read correct at the same time. libbladeRF already had bladerf_get_rfic_register and bladerf_set_rfic_register and cbladerf.pxd already declared both; only the binding was missing. Used to establish that RFIC state is bit-identical between a working and a dead transmit cycle (0x017, 0x05E, 0x002, 0x003, 0x001, 0x004, 0x073), and to drive the RFIC digital loopback via 0x3F5, which is what localized the fault to samples reaching the RFIC bus and then not advancing. --- python_bladerf/pylibbladerf/pybladerf.pyx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index cb5a1bb..4e00d25 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1996,6 +1996,25 @@ cdef class PyBladerfDevice: raise_error('pybladerf_get_rffe_control()', result) return value + def pybladerf_get_rfic_register(self, address: int) -> int: + """Read one RFIC (AD9361) register over SPI. + + Needed because no FPGA-side register reflects the RFIC's internal + state: the ENSM state, the digital datapath status and the filter + enables live only here. Without this an RFIC stuck with a gated + TX digital clock is indistinguishable from a healthy one, since + the FPGA enable pins read correct in both cases. + """ + cdef uint8_t value + result = cbladerf.bladerf_get_rfic_register(self.__bladerf_device, address, &value) + raise_error('pybladerf_get_rfic_register()', result) + return value + + def pybladerf_set_rfic_register(self, address: int, value: int) -> None: + """Write one RFIC (AD9361) register over SPI.""" + result = cbladerf.bladerf_set_rfic_register(self.__bladerf_device, address, value) + raise_error('pybladerf_set_rfic_register()', result) + def pybladerf_get_rfic_temperature(self) -> float: cdef float value result = cbladerf.bladerf_get_rfic_temperature(self.__bladerf_device, &value) From ac7ba719596b39175e5c6598cb5987f7598544d1 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 10:57:54 +0300 Subject: [PATCH 06/11] Add auto_sync_reconfig switch enable_module(dir, false) makes libbladeRF tear the sync stream down, so the wrapper repeats the last sync_config on re-enable. That is right for callers but hides the library's own behaviour, which makes it impossible to tell a wrapper problem from a library one. The switch turns the repeat off so both can be measured. --- python_bladerf/pylibbladerf/pybladerf.pxd | 1 + python_bladerf/pylibbladerf/pybladerf.pyx | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/python_bladerf/pylibbladerf/pybladerf.pxd b/python_bladerf/pylibbladerf/pybladerf.pxd index b14958c..fcad4e3 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pxd +++ b/python_bladerf/pylibbladerf/pybladerf.pxd @@ -108,6 +108,7 @@ cdef class PyBladerfDevice: # restored after libbladeRF tears it down on enable_module(False). cdef dict __sync_config cdef set __sync_torn_down + cdef bint __auto_reconfig cdef cbladerf.bladerf *get_ptr(self) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index 4e00d25..2165dbd 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1322,6 +1322,7 @@ cdef class PyBladerfDevice: # directions libbladeRF has torn down via enable_module(False). self.__sync_config = {} self.__sync_torn_down = set() + self.__auto_reconfig = True def __dealloc__(self): global global_callbacks @@ -1679,10 +1680,27 @@ cdef class PyBladerfDevice: return if direction in self.__sync_torn_down: self.__sync_torn_down.discard(direction) + if not self.__auto_reconfig: + return cfg = self.__sync_config.get(direction) if cfg is not None: self.pybladerf_sync_config(*cfg) + @property + def auto_sync_reconfig(self) -> bool: + """Whether re-enabling a direction repeats its last sync_config. + + On by default: without it every sync_tx/sync_rx after a + disable/enable cycle fails with "not initialized". Turn it off to + measure the library's own behaviour, or to drive sync_config by + hand. + """ + return self.__auto_reconfig + + @auto_sync_reconfig.setter + def auto_sync_reconfig(self, value: bool) -> None: + self.__auto_reconfig = bool(value) + def pybladerf_get_timestamp(self, direction: pybladerf_direction) -> int: cdef uint64_t timestamp result = cbladerf.bladerf_get_timestamp(self.__bladerf_device, direction, ×tamp) From 1b1e073c358c7e99192977dce1115f2bbc882c40 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 15:56:53 +0300 Subject: [PATCH 07/11] Keep an async stream running when no Python callback is installed Both __rx_callback_SC16_Q11 and __rx_callback_SC8_Q7 read after only assigning it inside 'if callback is not None'. With no callback installed the read raises UnboundLocalError inside the C callback, which is declared noexcept nogil, so the exception is swallowed: the stream stops after its very first buffer, ends up in STREAM_DONE, and bladerf_start_stream() returns with no error for the caller to see. Default result to 0 (keep streaming). An async RX stream with no Python callback is a legitimate configuration - it is the cheapest way to drain a direction without doing any per-buffer work, which is exactly what I needed to separate data-plane load from control-plane calls while chasing a TX wedge. --- python_bladerf/pylibbladerf/pybladerf.pyx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index 2165dbd..102c163 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1147,6 +1147,12 @@ cdef void *__rx_callback_SC16_Q11(cbladerf.bladerf *dev, cbladerf.bladerf_stream num_samples * async_data.bytes_per_sample, ) + # Keep streaming when no callback is installed. Without this default + # `result` is never assigned, the read below raises UnboundLocalError + # inside the callback, and the stream stops on its very first buffer + # with state STREAM_DONE and no error reported to the caller. + result = 0 + if global_callbacks[ dev]['__rx_callback'] is not None: result = global_callbacks[ dev]['__rx_callback'](global_callbacks[ dev]['device'], pystream, np_buffer, num_samples) @@ -1176,6 +1182,12 @@ cdef void *__rx_callback_SC8_Q7(cbladerf.bladerf *dev, cbladerf.bladerf_stream * num_samples * async_data.bytes_per_sample, ) + # Keep streaming when no callback is installed. Without this default + # `result` is never assigned, the read below raises UnboundLocalError + # inside the callback, and the stream stops on its very first buffer + # with state STREAM_DONE and no error reported to the caller. + result = 0 + if global_callbacks[ dev]['__rx_callback'] is not None: result = global_callbacks[ dev]['__rx_callback'](global_callbacks[ dev]['device'], pystream, np_buffer, num_samples) From b99d8e1532bc8715bd04986d95d93489488d94a9 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 17:41:58 +0300 Subject: [PATCH 08/11] add pybladerf_read_flash_bytes bladerf_read_flash_bytes was declared but not exposed. Needed it to test whether a firmware-side PIB/GPIF reset clears a wedged TX feed, since a flash read is the one safe host call that makes the firmware run NuandConfigureGpif without closing the device. --- python_bladerf/pylibbladerf/pybladerf.pyx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index 102c163..2bda967 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1903,6 +1903,13 @@ cdef class PyBladerfDevice: result = cbladerf.bladerf_get_fw_log(self.__bladerf_device, c_filename) raise_error('pybladerf_get_fw_log()', result) + def pybladerf_read_flash_bytes(self, address: int, count: int) -> bytes: + cdef bytearray buf = bytearray(count) + cdef unsigned char[::1] view = buf + result = cbladerf.bladerf_read_flash_bytes(self.__bladerf_device, &view[0], address, count) + raise_error('pybladerf_read_flash_bytes()', result) + return bytes(buf) + def pybladerf_set_vctcxo_tamer_mode(self, mode: pybladerf_vctcxo_tamer_mode) -> None: result = cbladerf.bladerf_set_vctcxo_tamer_mode(self.__bladerf_device, mode) raise_error('pybladerf_set_vctcxo_tamer_mode()', result) From 49f559f34e5d20d9f53aba12d49e9e8fabf55545 Mon Sep 17 00:00:00 2001 From: wormuz Date: Thu, 20 Aug 2026 18:07:48 +0300 Subject: [PATCH 09/11] build: rebuild when a .pxd changes The .pxd files carry the PyBladerfDevice layout and the libbladeRF declarations, and none of the four extensions listed them in depends. Editing a .pxd therefore left pybladerf_tools compiled against the previous struct layout, with nothing rebuilt and nothing to warn about until something crashed. --- setup.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/setup.py b/setup.py index 812dbed..7f2545f 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,16 @@ SETUP_REQUIRES = ['Cython>=3.1.0,<3.2.1', 'numpy'] libbladerf_h_paths = [] +# The .pxd files carry the layout of PyBladerfDevice and the libbladeRF +# declarations. Without them in depends, editing a .pxd leaves the modules that +# cimport it compiled against the old struct layout, which shows up at import +# time as "PyBladerfDevice size changed, may indicate binary incompatibility" +# and then as a segfault. +PXD_DEPENDS = [ + 'python_bladerf/pylibbladerf/pybladerf.pxd', + 'python_bladerf/pylibbladerf/cbladerf.pxd', +] + PLATFORM = sys.platform if getenv('LIBLINK'): @@ -104,6 +114,7 @@ def run(self) -> None: # type: ignore sources=['python_bladerf/pylibbladerf/pybladerf.pyx'], include_dirs=['python_bladerf/pylibbladerf', *libbladerf_h_paths, numpy.get_include()], extra_compile_args=['-w'], + depends=PXD_DEPENDS, language='c++', ), Extension( # type: ignore @@ -111,6 +122,7 @@ def run(self) -> None: # type: ignore sources=['python_bladerf/pybladerf_tools/pybladerf_sweep.pyx'], include_dirs=['python_bladerf/pylibbladerf', 'python_bladerf/pybladerf_tools', *libbladerf_h_paths, numpy.get_include()], extra_compile_args=['-w'], + depends=PXD_DEPENDS, language='c++', ), Extension( # type: ignore @@ -118,6 +130,7 @@ def run(self) -> None: # type: ignore sources=['python_bladerf/pybladerf_tools/pybladerf_scan.pyx'], include_dirs=['python_bladerf/pylibbladerf', 'python_bladerf/pybladerf_tools', *libbladerf_h_paths, numpy.get_include()], extra_compile_args=['-w'], + depends=PXD_DEPENDS, language='c++', ), Extension( # type: ignore @@ -125,6 +138,7 @@ def run(self) -> None: # type: ignore sources=['python_bladerf/pybladerf_tools/pybladerf_transfer.pyx'], include_dirs=['python_bladerf/pylibbladerf', 'python_bladerf/pybladerf_tools', *libbladerf_h_paths, numpy.get_include()], extra_compile_args=['-w'], + depends=PXD_DEPENDS, language='c++', ), ], From 0f8f1d3d714cbb1f5564bf580de126b378db3978 Mon Sep 17 00:00:00 2001 From: wormuz Date: Sat, 19 Sep 2026 05:59:48 +0300 Subject: [PATCH 10/11] bind bladerf_get_sample_loss_count Counts the FPGA keeps of samples it had to drop (RX) or transmit as a hole (TX). Distinct from the OVERRUN metadata flag, which is derived on the host from USB queue state and never reads the fabric. --- python_bladerf/pylibbladerf/cbladerf.pxd | 1 + python_bladerf/pylibbladerf/pybladerf.pyx | 36 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/python_bladerf/pylibbladerf/cbladerf.pxd b/python_bladerf/pylibbladerf/cbladerf.pxd index 948844c..9f443a8 100644 --- a/python_bladerf/pylibbladerf/cbladerf.pxd +++ b/python_bladerf/pylibbladerf/cbladerf.pxd @@ -345,6 +345,7 @@ cdef extern from 'libbladeRF.h' nogil: int bladerf_enable_module(bladerf *dev, int ch, c_bool enable) int bladerf_get_timestamp(bladerf *dev, bladerf_direction dir, uint64_t *timestamp) + int bladerf_get_sample_loss_count(bladerf *dev, bladerf_direction dir, uint64_t *count) int bladerf_sync_config(bladerf *dev, bladerf_channel_layout layout, bladerf_format format, unsigned int num_buffers, unsigned int buffer_size, unsigned int num_transfers, unsigned int stream_timeout) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index 2bda967..bf53047 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1460,6 +1460,29 @@ cdef class PyBladerfDevice: raise_error('pybladerf_get_gain()', result) return gain + def pybladerf_get_rfic_register(self, address: int) -> int: + """Read-only читання RFIC-регістра (`bladerf_get_rfic_register`). + + RFIC-HEALTH-001B: BBPLL lock (0x05E), RX/TX RF PLL lock + (0x247/0x287), RX/TX RF PLL cal done (0x244/0x284). Це САМЕ + read-only виклик — не змінює стан RFIC (на відміну від + `pybladerf_set_rfic_register`, який тут навмисно НЕ обгорнутий: + write-шлях лишається окремим рішенням, не випадковим побічним + ефектом наявності read-обгортки). + """ + cdef uint8_t val + result = cbladerf.bladerf_get_rfic_register( + self.__bladerf_device, address, &val) + raise_error('pybladerf_get_rfic_register()', result) + return val + + def pybladerf_get_rfic_temperature(self) -> float: + cdef float val + result = cbladerf.bladerf_get_rfic_temperature( + self.__bladerf_device, &val) + raise_error('pybladerf_get_rfic_temperature()', result) + return val + def pybladerf_set_gain_mode(self, channel: int, mode: pybladerf_gain_mode) -> None: result = cbladerf.bladerf_set_gain_mode(self.__bladerf_device, channel, mode) raise_error('pybladerf_set_gain_mode()', result) @@ -1719,6 +1742,19 @@ cdef class PyBladerfDevice: raise_error('pybladerf_get_timestamp()', result) return timestamp + def pybladerf_get_sample_loss_count(self, direction: pybladerf_direction) -> int: + '''Samples the FPGA itself dropped, per direction. + + Not the same as the OVERRUN metadata flag: that one is computed on + the host from USB queue state and never reads the fabric, so a loss + the FPGA absorbed on its own leaves it clear. Free-running and + monotonic, cleared only by a fabric reset -- take differences. + ''' + cdef uint64_t count + result = cbladerf.bladerf_get_sample_loss_count(self.__bladerf_device, direction, &count) + raise_error('pybladerf_get_sample_loss_count()', result) + return count + def pybladerf_sync_config(self, layout: pybladerf_channel_layout, data_format: pybladerf_format, num_buffers: int, buffer_size: int, num_transfers: int, stream_timeout: int) -> None: 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) From 742f14add886b146de31760cd2e2439b0619a5c8 Mon Sep 17 00:00:00 2001 From: wormuz Date: Sat, 19 Sep 2026 07:01:36 +0300 Subject: [PATCH 11/11] rename to pybladerf_get_loss_event_count: the fabric counts episodes, not samples --- python_bladerf/pylibbladerf/cbladerf.pxd | 2 +- python_bladerf/pylibbladerf/pybladerf.pyx | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/python_bladerf/pylibbladerf/cbladerf.pxd b/python_bladerf/pylibbladerf/cbladerf.pxd index 9f443a8..f37cb9d 100644 --- a/python_bladerf/pylibbladerf/cbladerf.pxd +++ b/python_bladerf/pylibbladerf/cbladerf.pxd @@ -345,7 +345,7 @@ cdef extern from 'libbladeRF.h' nogil: int bladerf_enable_module(bladerf *dev, int ch, c_bool enable) int bladerf_get_timestamp(bladerf *dev, bladerf_direction dir, uint64_t *timestamp) - int bladerf_get_sample_loss_count(bladerf *dev, bladerf_direction dir, uint64_t *count) + int bladerf_get_loss_event_count(bladerf *dev, bladerf_direction dir, uint64_t *count) int bladerf_sync_config(bladerf *dev, bladerf_channel_layout layout, bladerf_format format, unsigned int num_buffers, unsigned int buffer_size, unsigned int num_transfers, unsigned int stream_timeout) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index bf53047..98b0a3d 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1742,17 +1742,22 @@ cdef class PyBladerfDevice: raise_error('pybladerf_get_timestamp()', result) return timestamp - def pybladerf_get_sample_loss_count(self, direction: pybladerf_direction) -> int: - '''Samples the FPGA itself dropped, per direction. + def pybladerf_get_loss_event_count(self, direction: pybladerf_direction) -> int: + '''Loss EPISODES the FPGA counted, per direction. + + Episodes, not samples: an unbroken run of overflow increments this + once however long it lasts. Do not scale it into a sample count. Not the same as the OVERRUN metadata flag: that one is computed on the host from USB queue state and never reads the fabric, so a loss - the FPGA absorbed on its own leaves it clear. Free-running and - monotonic, cleared only by a fabric reset -- take differences. + the FPGA absorbed on its own leaves it clear. + + Cleared by enable_module(True) -- a difference across an enable + boundary is meaningless. Monotonic within one enabled session. ''' cdef uint64_t count - result = cbladerf.bladerf_get_sample_loss_count(self.__bladerf_device, direction, &count) - raise_error('pybladerf_get_sample_loss_count()', result) + result = cbladerf.bladerf_get_loss_event_count(self.__bladerf_device, direction, &count) + raise_error('pybladerf_get_loss_event_count()', result) return count def pybladerf_sync_config(self, layout: pybladerf_channel_layout, data_format: pybladerf_format, num_buffers: int, buffer_size: int, num_transfers: int, stream_timeout: int) -> None: