Skip to content
Open
2 changes: 2 additions & 0 deletions python_bladerf/pylibbladerf/cbladerf.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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_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)

Expand Down Expand Up @@ -552,6 +553,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)

Expand Down
5 changes: 5 additions & 0 deletions python_bladerf/pylibbladerf/pybladerf.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ 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 bint __auto_reconfig

cdef cbladerf.bladerf *get_ptr(self)

Expand Down
188 changes: 188 additions & 0 deletions python_bladerf/pylibbladerf/pybladerf.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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[<size_t> dev]['__rx_callback'] is not None:
result = global_callbacks[<size_t> dev]['__rx_callback'](global_callbacks[<size_t> dev]['device'], pystream, np_buffer, num_samples)

Expand Down Expand Up @@ -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[<size_t> dev]['__rx_callback'] is not None:
result = global_callbacks[<size_t> dev]['__rx_callback'](global_callbacks[<size_t> dev]['device'], pystream, np_buffer, num_samples)

Expand Down Expand Up @@ -1318,6 +1330,11 @@ 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()
self.__auto_reconfig = True

def __dealloc__(self):
global global_callbacks
Expand Down Expand Up @@ -1443,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)
Expand Down Expand Up @@ -1656,26 +1696,91 @@ 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)
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, &timestamp)
raise_error('pybladerf_get_timestamp()', result)
return timestamp

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.

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_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:
result = cbladerf.bladerf_sync_config(self.__bladerf_device, layout, data_format, <unsigned int> num_buffers, <unsigned int> buffer_size, <unsigned int> num_transfers, <unsigned int> 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

if isinstance(metadata, pybladerf_metadata):
metadata_link = metadata
c_metadata_ptr = <cbladerf.bladerf_metadata*> metadata_link.get_ptr()
else:
self.__check_metadata_required(1, 'pybladerf_sync_tx')

cdef unsigned int c_num_samples = <unsigned int> num_samples
cdef unsigned int c_timeout_ms = <unsigned int> timeout_ms
Expand All @@ -1693,6 +1798,8 @@ cdef class PyBladerfDevice:
if isinstance(metadata, pybladerf_metadata):
metadata_link = metadata
c_metadata_ptr = <cbladerf.bladerf_metadata*> metadata_link.get_ptr()
else:
self.__check_metadata_required(0, 'pybladerf_sync_rx')

cdef unsigned int c_num_samples = <unsigned int> num_samples
cdef unsigned int c_timeout_ms = <unsigned int> timeout_ms
Expand All @@ -1703,6 +1810,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 = <pybladerf_async_data*> malloc(sizeof(pybladerf_async_data))
Expand Down Expand Up @@ -1810,6 +1944,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)
Expand Down Expand Up @@ -1905,6 +2046,53 @@ cdef class PyBladerfDevice:
result = cbladerf.bladerf_set_rfic_register(self.__bladerf_device, <uint16_t> address, <uint8_t> 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).

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_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, <uint16_t> 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, <uint16_t> address, <uint8_t> 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)
Expand Down
14 changes: 14 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down Expand Up @@ -104,27 +114,31 @@ 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
name='python_bladerf.pybladerf_tools.pybladerf_sweep',
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
name='python_bladerf.pybladerf_tools.pybladerf_scan',
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
name='python_bladerf.pybladerf_tools.pybladerf_transfer',
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++',
),
],
Expand Down