Skip to content

Fix posix_cond_timedwait return value - #1060

Open
Algafix wants to merge 1 commit into
Nuand:masterfrom
Algafix:bladerfcli-fix-txrx-wait
Open

Algafix wants to merge 1 commit into
Nuand:masterfrom
Algafix:bladerfcli-fix-txrx-wait

Conversation

@Algafix

@Algafix Algafix commented Mar 13, 2026

Copy link
Copy Markdown

When using the bladeRF-cli I saw that the tx/rx wait Xs command was not returning.

After further inspection, I believe it is because posix_cond_timedwait() is returning -1 instead of ETIMEDOUT.

In the bladeRF-cli code, the comparison is done with THREAD_TIMEOUT, which is mapped to ETIMEDOUT.

At a quick look, I have not seen any part of the codebase where the comparison is with -1 instead of THREAD_TIMEOUT or similar. I might be wrong, then the change should be in the bladeRF-cli code to compare with -1.

#1025

@manoskav

Copy link
Copy Markdown

Thanks for the fix @Algafix !

@nathansizemore

Copy link
Copy Markdown

Can confirm this makes a normal timeout report as an unexpected error. Would be great to get in since this bug made it into 2025.10, but also 2025.10 contains gpif and fx3 dma changes which are nice to have.

@wormuz

wormuz commented Aug 17, 2026

Copy link
Copy Markdown

Yep, just found it today

bradley-DS added a commit to DistributedSpectrum/bladeRF that referenced this pull request Aug 24, 2026
Nuand#1060 thread.h: posix_cond_timedwait повертав -1 замість ETIMEDOUT.
      sync.c:393 порівнює з THREAD_TIMEOUT (=ETIMEDOUT), тож умова ніколи
      не спрацьовувала і таймаут очікування буфера ставав ERR_UNEXPECTED.
      Споживачі в bladeRF-fsk уже чекають ETIMEDOUT — патч їх узгоджує.

Nuand#1024 sync_worker.c: 1000 мс на ініціалізацію воркера замало; при
      спрацюванні звільнявся s->worker, а потік ще виконувався.

Nuand#1052 libusb.c: LIBUSB_ERROR_TIMEOUT 


* libbladeRF: do not log an error per transfer when the device is gone

cancel_all_transfers() logs LIBUSB_ERROR_NO_DEVICE as an error for every
in-flight transfer. That condition is expected when the device has been
unplugged or reset by the host controller: the transfer cannot be
cancelled because the device is no longer there.

With num_transfers=32 this produces a burst of 32 identical error lines
per reset, which buries the one line that actually matters.

libusb still delivers the completion callback for these transfers, so the
status is marked TRANSFER_CANCEL_PENDING like any other cancellation; only
the logging changes.

Measured on bladeRF 2.0 micro xA4 (FX3 2.6.0, FPGA 0.16.0) with the device
reset via sysfs unbind/bind during continuous RX: error lines per reset
go from tens to zero, and the stream still tears down and recovers.

* libbladeRF: reject stream configs that exceed the usbfs memory budget

Every in-flight USB transfer is pinned by the kernel, so a stream needs
num_transfers * buffer_size_bytes of usbfs memory simultaneously. The
default limit is 16 MiB.

Exceeding it fails far from its cause: bladerf_sync_config() returns
success, submit_transfer() then fails asynchronously with
LIBUSB_ERROR_NO_MEM, and the caller only sees BLADERF_ERR_MEM from the
first bladerf_sync_rx() - with nothing pointing at the transfer count.

Measured on bladeRF 2.0 micro xA4 (FX3 2.6.0, FPGA 0.16.0), SC16_Q11_META
at 61.44 Msps with buffer_size=131072 samples:

  num_transfers=31  ->  ok
  num_transfers=32  ->  ERR_MEM from the first sync_rx

32 * 512 KiB is exactly the 16 MiB limit. The larger GPIF buffers in FX3
2.6.0 make this easier to hit than before.

Now checked up front: bladerf_sync_config() returns BLADERF_ERR_INVAL and
logs the numbers plus the maximum transfer count that fits.

* libbladeRF: unlock the device mutex on the 8-bit format error path

bladerf_sync_config() and bladerf_init_stream() take dev->lock, then
return BLADERF_ERR_UNSUPPORTED without releasing it when an SC8_Q7 format
is requested on a bladeRF1.

The device is left locked, so every subsequent API call on that handle
blocks forever. The caller sees a hang rather than the error that was
actually returned.

Found while auditing 2.6.0 changes; both call sites are in bladerf.c.

* libbladeRF: fix buffer overflows and a missing NULL check in gain calibration

Three issues in the gain calibration paths added in 2.6.0:

device_calibration.c: strcat() appends a caller-supplied CSV path of
arbitrary length to a 1000-byte buffer that already holds the working
directory. Long paths overflow the stack buffer. Print both parts with
a format specifier instead of concatenating in place.

bladerf.c: strcpy() copies a caller-supplied calibration path into a
PATH_MAX buffer without a length check. Reject paths that do not fit.

bladerf.c: malloc() result used by strcpy() without a NULL check.

All three are reachable through bladerf_load_gain_calibration() with a
caller-controlled path.

* libbladeRF: document the usbfs memory limit on num_transfers

The number of in-flight transfers is bounded by the kernel's usbfs memory
limit, not just by num_buffers. With the larger GPIF buffers in FX3 2.6.0
the default 16 MiB is easy to reach: 32 transfers of a 131072-sample
SC16_Q11 buffer is exactly 16 MiB.

Documents the limit and the error now returned for configurations that
exceed it.

* libbladeRF: report RX overruns and leading discontinuities to sync_rx callers

Two ways an RX gap could reach the caller unreported.

First, the worker's overrun recovery. sync_worker.c carried a TODO for
this: on overrun it resubmits buffers and logs, but nothing propagates the
condition. Because recovery restarts the timestamp sequence, the gap is
not visible in the message headers either, so bladerf_sync_rx() returns
samples that are not contiguous with the previous call and reports
success. Add buf_mgmt.overrun_pending, set by the worker and consumed by
the next sync_rx(), which now raises BLADERF_META_STATUS_OVERRUN.

Second, a discontinuity landing on the first message of a read. The check
in sync_rx() required copied_data, so only gaps found mid-read were
flagged; a gap at the start was silently skipped. The status flag is now
raised regardless, while the early return stays conditional: with data
already copied it must be handed back before the gap, with none copied
the read continues past it.

The first header of a stream has nothing to compare against, so
meta.have_timestamp distinguishes it from a real discontinuity. It is
cleared on init and whenever the stream restarts.

Measured on a bladeRF 2.0 micro xA4 (FX3 2.6.0, FPGA 0.16.0), SC16_Q11_META
at 61.44 Msps, host stalled 400 ms between reads to force overruns:

  before:  driver logged 141 overruns, meta.status stayed 0x0
  after:   2 of 6 reads report BLADERF_META_STATUS_OVERRUN
  no stall: 0 of 15 reads report it, driver logs none

* no-OS: три виправлення ad9361 (гілка nuand-fixes)

  ad9361: do not leave the part in ALERT when update_rf_bandwidth fails
  ad9361: load the gain table with multibyte writes
  ad9361: express the gain-table row delay as udelay(), not dummy writes

Перше усуває нескінченний потік Calibration TIMEOUT (0x247, 0x2) після
одного збою: функція форсувала ALERT і виходила в обхід відновлення
стану. Два інші здешевлюють перетин межі таблиці підсилення з 161.3 до
63.4 мс (p50, 15 перетинів), вміст SRAM при цьому побайтно тотожний.

PR в апстрим: analogdevicesinc/no-OS#3273, #3274.

* no-OS: помилка SPI більше не звітується як таймаут калібрування

ad9361_check_cal_done порівнював результат ad9361_spi_readf із done_state,
а той при збої повертає від'ємний errno. Від'ємне ніколи не дорівнює
done_state, тож обрив транспорту крутив увесь цикл по мертвій шині й
звітувався як Calibration TIMEOUT (0x247, 0x2).

Тепер помилка читання повертається одразу, а справжній таймаут несе
лічильники: polls, spi_err, first, last.

PR analogdevicesinc/no-OS#3277.

* bladerf2: wrap quick tune profile indices instead of running out

The Nios quick tune profile counter only ever incremented, and was reset
in exactly one place: board initialisation. An application that keeps
requesting quick tunes therefore had a hard budget of
NUM_BBP_FASTLOCK_PROFILES (256) for the lifetime of the device handle.
Past that, every further bladerf_get_quick_tune() failed with
BLADERF_ERR_UNEXPECTED and there was no way to recover short of closing
and reopening the device.

That budget is not a hardware limit on how many retune targets may exist
over time. Both the RFIC and the Nios hold caches that this code already
overwrites: the RFFE index is assigned modulo NUM_RFFE_FASTLOCK_PROFILES,
so profile 8 has always overwritten profile 0. Letting the Nios index
wrap the same way makes the two consistent and keeps long-running
frequency sweeps working.

Measured on a bladeRF 2.0 micro xA4 sweeping 70 MHz - 6 GHz with 4700
distinct stops, taking a quick tune profile on first visit to each:

  before: 320 stops -> counter stuck at 256, every later stop logged
          "Reached maximum number of RX quick tune profiles"
  after:  600 stops -> 600 profiles, no errors
          3392 stops in 90 s -> 3392 profiles, no errors

The counter is uint16_t and 65536 is a multiple of 256, so wrapping it
does not disturb the modulo alignment.

* no-OS: bump submodule to the RF PLL lock-wait bound

Picks up 90c4d663f, which gives the RF PLL lock wait its own budget
instead of sharing the calibration one. Measured ceiling on a failing
retune drops from 1955 ms to 39.1 ms.

* no-OS: bump submodule to the signed temperature read

* bladerf2: leave AD9361 fastlock after an immediate quick tune

The Nios recalls a quick tune profile by writing the RFIC directly. That
leaves the part in fastlock mode with FORCE_ALC_ENABLE asserted, and the
AD9361 driver cannot undo it on its own: its exit path is gated on
phy->fastlock.current_profile, which a recall performed by the FPGA never
touches. Ownership of the RFPLL returns to host tuning with forced
controls still active, and the next bladerf_set_frequency() programs the
synthesiser as if ALC were automatic.

Measured on a bladeRF 2.0 micro xA4, interleaving bladerf_get_quick_tune()
plus an immediate bladerf_schedule_retune() with ordinary tuning every
fifth stop of a 242-point sweep across 82-5988 MHz, 65536 samples per
stop:

  before   55 lock failures in 706 tunes, first at tune 280
  after     2 lock failures in 758 tunes, first at tune 495

The shape matters more than the rate. Before, the failures form a series
that never recovers: 44 consecutive failures with no successful tune
inside them, and 0x247 reading 0x40 the whole time, meaning the RX charge
pump has saturated low. After, there are no consecutive failures at all.
Both figures reproduced across three runs each, identical to the tune.

Leaked controls at the end of a run, before and after:

  0x236  0xD3 (FORCE_ALC set)   ->  0x50-0x5B (clear)
  0x25A  0x01-0xA1 (mode set)   ->  0x00

The leak was present on every recall rather than occasionally: 145 of
145, 146 of 146, 146 of 146 across three runs.

Only immediate retunes are handled here. One scheduled for a future
timestamp completes inside the FPGA long after the call returns, so the
exit would have to happen there instead; that case is left alone rather
than guessed at.

Also picks up the corresponding no-OS change, which adds
ad9361_fastlock_exit_foreign().

* bladerf2: leave foreign fastlock before ordinary host tuning

A quick tune scheduled for a future timestamp is recalled by the FPGA's
Nios core, which writes REG_(RX|TX)_FAST_LOCK_SETUP directly and leaves
the part in fastlock mode behind the driver's back. The existing exit in
bladerf2_schedule_retune() only covers BLADERF_RETUNE_NOW, because a
deferred recall completes inside the FPGA long after that call returns.

Tuning ordinarily while that leaked state is in place pins the RF PLL
charge pump: 0x247 reads 0x80 (CP overrange high, no lock) during the
failed tune and 0x40 after it. Measured on a bladeRF 2.0 micro xA4
running a sweep that mixes deferred quick tune recalls with ordinary
tunes to off-grid frequencies, five 360 s runs:

    without this exit    190 / 327 / 587 / 330 / 667 lock failures
    with it                0 / 0

A sweep that uses only deferred recalls (never tuning ordinarily) shows
zero failures either way, which is what isolated the mix as the trigger.

ad9361_fastlock_exit_foreign() reads the setup register first and writes
nothing when the part is not in fastlock mode, so the ordinary tuning
path stays a single SPI read when no foreign recall happened.

* no-OS: bump submodule to the documented 0x80 reading

* libbladeRF: fix unit mix-up in ts_remaining() for X2 layouts

curr_msg_off counts samples: it is advanced by samples_to_copy and
compared against samples_per_msg. ts_remaining() subtracted it from
samples_per_msg / samples_per_ts, which counts TIME STEPS per message.
The units only coincide for single-channel layouts (samples_per_ts == 1).

In X2 mode the unsigned subtraction underflows as soon as the offset
passes half a message. With assertions enabled a bladerf_sync_rx() call
that seeks to a timestamp inside the current message aborts the process:

    sync.c:451: ts_remaining: Assertion 'ret <= UINT_MAX' failed.

With NDEBUG the huge result sends the seek logic into the 'fast forward
within the current message' branch when the target lies beyond it, and
curr_msg_off runs past the message boundary - the stream stalls instead
of crashing. This matches the dual-channel stalls reported in Nuand#944.

Reproduced on a bladeRF 2.0 micro xA4 (FX3 2.6.0, FPGA 0.16.0) with an
RX_X2 SC16_Q11_META stream reading back-to-back explicit timestamps:
the abort fires within the first few reads. With the units fixed the
same sequence completes 3000/3000 contiguous reads with no timestamp
drift, and single-channel metadata reads are unaffected.

Subtract the sample offset first, then convert to time steps. The new
assertion documents the invariant the old expression silently assumed.

* libbladeRF: drop the stale RX backlog after an overrun

When the consumer stalls and the ring fills, the worker stops storing:
everything the hardware produces after that moment is discarded, so the
buffers that sit full in the ring are the OLDEST data, not the newest.
On resume the consumer first reads up to (num_buffers - num_transfers)
buffers of history. With a metadata format the timestamps expose this;
with SC16_Q11 and the other plain formats nothing does - the stale
prefix is indistinguishable from live samples.

Measured on a bladeRF 2.0 micro xA4 (TX1-to-RX1 loopback, FFT verdict
per read): 8-16 stale reads after every stall across a stall-length
matrix - the exact num_buffers - num_transfers of the configuration.
This shows up as 'the receiver ignores what just changed on the air'
and once masqueraded as a TX defect that cost half a day to debunk.

The worker now flags the backlog when it detects the overrun, and the
consumer drops the contiguous run of full buffers before claiming the
next one, resuming at the live edge. In WAIT_FOR_BUFFER cons_i is never
PARTIAL, so the walk is safe, and the producer resumes storing into the
slots freed here. BLADERF_META_STATUS_OVERRUN reporting is unchanged.

After the fix the same matrix shows 59-60/60 live reads in every
combination; metadata timestamps and overrun reporting stay intact.

* libbladeRF: exempt metadata formats from the stale-backlog drop

Metadata consumers already see the gap in the timestamps, and they may
legitimately want the backlog: a scheduled capture seeks THROUGH the
buffers that are full at overrun time to reach its target timestamp.
Dropping them under that seek discards the very samples the caller
asked for - a sweep that overruns on every stop went from over a
thousand detections to zero.

The drop stays for the plain formats, where nothing marks the stale
prefix and the caller has no way to tell history from live data.
Verified after the change: the non-metadata matrix still shows 59-60/60
live reads, metadata timestamps advance, and META_STATUS_OVERRUN
reporting is unaffected.

* Keep the RFIC port when a direction is disabled

The shutdown entry in the band port map exists for the RF SPDT switches:
they have a genuine "no connection" state, RFFE_CONTROL_SPDT_SHUTDOWN is
0x0, and putting them there on disable is what the entry was added for.

The rfic_port field in the same struct has no such state. Every value in
that enum is a real input, and the 0 the shutdown entry carries is
AD936X_A_BALANCED, the high-band input. So disabling an RX channel tuned
to 915 MHz moved the RFIC input from B_BALANCED to A_BALANCED, and
re-enabling moved it back.

That round trip is not free. Measured on a TX1 -> 50 dB pad -> RX1
loopback at 915 MHz, manual gain control, an unmodulated tone that was
never touched between reads:

  20 disable/enable cycles, REG_INPUT_SELECT toggling 0x43 <-> 0x4C,
  reported gain constant at 40.0 dB:      spread 0.48 dB, sigma 0.161

Isolating it further, with the TX stream never interrupted:

                                   before          after
  sync_config only            0.20 / 0.045    0.09 / 0.028
  enable(false->true) only    0.68 / 0.211    0.32 / 0.079
  both                        0.63 / 0.182    0.35 / 0.080
  both + 300 ms settle        0.42 / 0.176    0.21 / 0.062

REG_INPUT_SELECT now reads 0x4C before and after each cycle.

Leaving the port alone costs nothing while disabled: the direction is
off and the SPDT is already disconnected. The port is still selected by
frequency on every enable, so nothing else changes.

Some residual spread remains, so this is not the whole story -- but it
is half of it, and the half that had a clear mechanism.

* fifo_reader: don't let a stale meta header read as META_NOW

meta_p_time was derived from meta_current.meta_fifo_data, the copy of the
meta FIFO output registered on the previous cycle. That copy is all zeros
coming out of reset, and the reader subtracts 1 from the header timestamp,
so 0 - 1 wraps to all ones. All ones is exactly the META_NOW sentinel, so
META_WAIT's "meta_p_time = MAX_TIMESTAMP" test passes and the feed gate
opens immediately for a burst that was scheduled in the future. The burst
is emitted at once and nothing remains to send when its timestamp arrives.

Take the timestamp from meta_fifo_data, the header the FIFO is actually
presenting when META_LOAD issues the read.

Adds fifo_reader_tb, which had no testbench before. Two cases:

  scheduled (TGT=4000)  before: 2045 reads early, 0 on time
                         after: 0 early, 2001 on time
  TX_NOW (TGT=0)        before: 2045 reads    after: 2045 reads

Verified with GHDL against this source, including a negative control:
reverting the one line makes the scheduled case fail the assertion with
"gate leaked", so the test does detect the defect.

Found while chasing a transmitter that goes silent after a TX module
enable/disable cycle, where the RFIC holds a latched sample of the
transmitted waveform while the timestamp counter keeps advancing. Whether
this fully explains that symptom is not yet established - it needs an FPGA
build and a hardware re-measurement.

* bladerf2: read the layout of the direction being enabled

board_data->sync is indexed by direction, so a plain sync-> is sync[0],
which is always the RX stream. Enabling or disabling a TX channel therefore
read the RX layout here and answered the MIMO question from the wrong
stream.

Found while chasing an intermittent TX wedge. It is not the cause of that
wedge - measured on a bladeRF 2.0 micro xA4, this change alone leaves the
wedge rate unchanged at 5 of 10 runs clean, same as master - but the read is
wrong on its own terms and worth fixing.

* libusb: do not cut a TX transfer in the middle of a buffer

Cancelling an in-flight TX transfer ends it wherever it happens to be, and
the device cannot use a partial buffer. On the wire the cancelled OUT
transfers come back having moved 1024, 3072, 5120, 6144, 8192, 13312 and
27648 bytes - all multiples of the 1024-byte SuperSpeed packet, none a
multiple of the 32768-byte buffer. The FX3 TX channel is
CY_U3P_DMA_TYPE_AUTO over 8192-byte buffers, so it assembles whole
buffers; the cut leaves it holding an unfinished one and later
submissions have nowhere to complete. What follows is eight submissions
sitting for a full stream timeout while the RX endpoint keeps completing
normally, which is the "TX feed stops after repeating sync_config(TX)
while RX is read" report.

So an in-flight TX transfer is now given until its own timeout to finish
by itself, and is cancelled only after that - by which point it was not
going to complete anyway. Transfers that never started are unaffected:
they complete immediately either way.

Measured, same session, 10 s of continuous load with the TX stream
rebuilt in a tight loop and RX read throughout
(tools/repro/tx_wedge_soak.py in the sdr-scanner tree):

  before   feed died at round 2, 5, 3        3 of 3 runs
  after    survived 681, 676, 679 rounds     3 of 3 runs

Feed rate is unchanged - roughly 19k buffers submitted across 680
stream teardown/setup cycles in 10 s.

* libusb: wait for events the way libusb requires when two streams share a context

RX and TX each run their own lusb_stream() loop calling
libusb_handle_events_timeout() on the same libusb context. libusb permits
that, but only one thread actually handles events while the others wait
inside it, and a waiter that entered the wait before the state it is
waiting on changed keeps waiting until its timeout expires. Checking
stream->state outside the event lock, which a plain
handle_events_timeout() call forces you to do, is exactly the lost-wakeup
pattern that libusb_handle_events_timeout_completed() exists to close.

The loop now passes a completion flag that libusb re-checks under its own
event lock, and every transition to STREAM_DONE goes through
mark_stream_done() so the state and the flag always move together.

Measured: no change to the TX feed stall this was chased for (137
transfer timeouts before, 135 after, same conditions), so this is
protocol correctness rather than a fix for that. The stall is still open.

* sync: keep submitting until the TX ring is out of the parked state

The previous commit found the buffer the callback still owed a submission
for, but submitted exactly one. That is not enough to leave the ring in a
live state: every buffer still FULL behind the first needs its own
callback to be shipped, and the callbacks that would have run were
consumed by libusb's cancellation path. So one reclaim moved one buffer
and left submission parked again, with the rest of the data waiting.

sync_tx now drains while the ring stays parked, stopping as soon as a
reclaim moves nothing - which is what WOULD_BLOCK looks like from here.
The check also runs before the wait rather than only after it times out:
waiting in a state no callback can resolve just burns the stream timeout,
which on the wire showed up as a 1002 ms silence with 941 RX completions
inside it.

tx_submission_parked() names the forbidden state: duty with the callback
and a buffer recorded as owed. Buffer status deliberately is not used to
count live transfers - IN_FLIGHT is only cleared in the sync callback, so
a cancelled transfer leaves its buffer marked IN_FLIGHT forever, which is
what made the ring look busy when nothing was.

test_tx_submit_liveness.c replays the exact ordering from the trace
(WOULD_BLOCK, duty to callback, cancelled completions bypassing the sync
callback, stale cons_i, data behind it) and asserts the forbidden state is
not reachable. Checked against the single-submission version: it fails on
that case and passes the rest, so the test separates the two.

Device check: feed rate unchanged, 1553 buffers in 3.8 s, same as before.

* sync_deinit: retire the handle before destroying its mutexes

sync_rx() and sync_tx() test sync->initialized without holding a lock - they
cannot, because the lock they would take is the one being destroyed - and
then go on to take buf_mgmt.lock. Clearing the flag last left a window where
a concurrent reader passed the test and then locked a mutex MUTEX_DESTROY had
already reclaimed. Clearing it first closes that window.

This is a use-after-destroy on a mutex, reachable whenever one direction is
reconfigured while the other is being read, which is exactly what
bladerf_sync_config() does via sync_deinit().

Does NOT fix the TX wedge: measured 24 runs, mean repeat 13, identical to
baseline 13. Committed on its own terms.

* sync: do not reset the TX consumer index to 0 on worker restart

SYNC_STATE_RESET_BUF_MGMT sets buf_mgmt.cons_i = 0 for both directions. That
is right for RX, where the consumer index is where the reader picks up. For TX
the same field means the opposite: it names the buffer tx_callback should ship
next, and sync_init() sets BUFFER_MGMT_INVALID_INDEX to say 'nothing is
deferred yet'.

So a restarted TX worker comes back believing buffer 0 is a deferred, full
buffer. tx_callback then submits a buffer the producer never filled, and the
ring accounting drifts from there.

Found while investigating the TX reconfiguration wedge. It is NOT that wedge:
this path only runs when the worker is already IDLE, and instrumentation shows
RESET_BUF_MGMT is never reached during a plain sync_config() reconfigure
(0 occurrences). It will bite on the stream-error recovery path, where the
caller is documented to be able to call again to restart the stream.

* streaming: let a TX worker see STOP when nothing is completing

A TX worker only inspects w->requests from inside tx_callback, and that
callback only runs when a transfer completes. With the feed stopped nothing
completes, so a STOP request is never observed: the worker sits in RUNNING
until sync_worker_deinit() times out after 3 s and resorts to THREAD_CANCEL.

Two halves:

  - sync_worker_submit_request() nudges a RUNNING TX stream into
    STREAM_SHUTTING_DOWN, so the backend event loop, which spins while the
    state is not STREAM_DONE, can leave on its own.
  - that loop now finishes the shutdown itself: cancel what is outstanding,
    and once every transfer is accounted for, move to STREAM_DONE.
    Previously the SHUTTING_DOWN to DONE transition lived only in
    lusb_stream_cb(), so a shutdown with nothing in flight had no way to
    complete - which is why setting SHUTTING_DOWN unconditionally used to
    hang the worker.

Verified on a bladeRF 2.0 micro xA4: the worker stops cleanly with no
THREAD_CANCEL, and RX is unaffected (40/40 frames across five enable cycles).

Does NOT fix the TX reconfiguration wedge. Measured in one sitting, 8
reconfigurations per run: 7 of 10 runs clean with this change against 6 of 10
without, which is inside the spread. An early 4-of-4 result was luck.

* sync: submission can be parked with the duty held by sync_tx too

tx_submission_parked() only recognised the state where the duty sits with
the worker callback. There is a second one, and it is what a wide TX ring
runs into: the duty comes back to sync_tx, the callback path shipped what
it could, and everything that piled up while the duty was parked stays
FULL - because sync_tx only ever submits the buffer prod_i points at, and
with nothing in flight no callback arrives to move the rest.

Measured on a bladeRF 2.0 micro, TX stream of 64 buffers x 16384 samples
with 16 transfers, RX read concurrently: submitter FN, 0 in flight, 26 of
64 buffers FULL, and sync_tx() timing out at 1000 ms. The feed had a full
ring of data to send and sent none of it.

oldest_full_buffer() now starts from prod_i when cons_i is INVALID, which
is how the duty-is-ours case presents, so the same drain loop covers both.

test_tx_submit_liveness.c gets the case, and its stuck() no longer cares
which side holds the duty - what makes it stuck is data waiting with
nothing in flight. Checked against the previous predicate: it fails on
exactly this case and passes the rest.

Also: a failed RX worker start was logged at debug level, so in practice
it was invisible - the caller then waits out its timeout and reports a
timeout, which reads like a device that went quiet rather than a worker
that never started. Now a warning, matching the TX side.

* libusb: say how much arrived when a transfer times out

A timed-out transfer is reported as just "Transfer timed out for RX buffer
0x...". That leaves the two most useful facts out: how much of the buffer
actually arrived, and whether this stream had ever received anything at
all.

Those two numbers separate faults that are otherwise identical from the
caller's side, since both surface as BLADERF_ERR_TIMEOUT:

  - a device that never started producing samples
    (zero completed transfers, zero bytes)

  - a device that produced for a while and then stopped
    (nonzero completed transfers)

Without them, working out which one you are looking at means adding
printfs to the library and rebuilding it. I did exactly that while chasing
an RX stall, and the counter was the fact that ended the search.

Track completed full-buffer transfers per stream and include the byte
counts in the timeout message. No behaviour change: same error code, same
control flow, one extra counter and a longer log line on a path that is
already an error path.

* libbladeRF: strip development leftovers from the merged TX work

PR Nuand#1086 came in with two standalone test programs dropped into
src/streaming/. libbladeRF's CMakeLists lists its sources explicitly, so
those files never built and never will; remove them.

The same series left two log_verbose() calls in sync_rx() that were plainly
debug instrumentation, three comment blocks stacked above a single forward
declaration -- one belonging to each of oldest_full_buffer(),
tx_submission_parked() and reclaim_tx_submission() -- and three overlapping
comments on the TX wait path. Reattach each comment to the function it
describes, collapse the TX wait ones into a single explanation, and drop a
non-ASCII character that had crept into a C comment.

No functional change.

* bladeRF-cli: name the load_gain helptext after the command

generate.bash derives the macro name from the second field of the Usage:
line, so interactive-help.md's "Usage: load_gain <rx|tx> <filename>" yields
CLI_CMD_HELPTEXT_load_gain. cmd.c and the checked-in cmd_help.h.in both
spelled it CLI_CMD_HELPTEXT_gain_calibration_load, after the C identifier
rather than the command, so any build with pandoc present regenerated a
header without that symbol and failed to compile cmd.c.

Rename the macro in both places. The C identifier and the source file keep
their names; only the helptext macro moves.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: wormuz <wormuz@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants