Commit Graph
10980 Commits
Author SHA1 Message Date
Carles Fernandez 1f91e48e1f Merge commit '5efbfd16c9eed710211f5532d30a0681cc67add2' into joebre-freq-error-reduction 2026-09-16 13:35:35 +02:00
Carles Fernandez e451bd110f Fix compiler warning 2026-09-16 12:39:49 +02:00
Carles Fernandez 66782155ec Fix clang-tidy job 2026-09-16 12:23:45 +02:00
joebre 5efbfd16c9 Remove time-to-fix breakdown diagnostic
log_time_to_fix_breakdown() and its six supporting state variables
were left-over instrumentation from developing the frequency-error-
reduction feature, not part of its documented behavior -- strip
before this is mergeable.
2026-09-15 10:28:54 +02:00
Carles Fernandez 4d3ad83957 Fix polarity, Doppler rate, and timers after the frequency-error scan
Follow-ups to the frequency-error-reduction merge (tracking state 5):

- Anchor Flag_PLL_180_deg_phase_locked to the sign of the last Prompt
  symbol. The differential bit/secondary-code detector reconstructs bits
  relative to an arbitrary first bit, so the previous corr_value sign rule
  was independent of the actual carrier polarity.
- Do not feed the deliberate Doppler bin hops of the scan into the
  high_dyn carrier/code phase-rate smoothers: clear the histories and
  zero the rate steps while scanning.
- Start the pull_in_time_s and bit_synchronization_time_limit_s budgets
  at the first sample after the scan instead of at acquisition, so long
  scans no longer consume the FLL pull-in or the sync time limit. Scan
  completion re-arms pull-in and resets the lock-fail counters. The
  non-scan path keeps the acquisition time origin.
- Clamp f_error_accumulation to at least 1, both when parsed from the
  configuration and in the block constructor, to avoid an out-of-bounds
  write and a scan that never advances.
- Add unit tests for polarity, rate suppression, timers, and a full L2C
  scan, and document the new options in the CHANGELOG.
2026-09-11 09:00:01 +02:00
joebre 89fa1d9dfd Retrigger CI (macOS-Xcode flowgraph_test timeout looked flaky, unrelated to this PR's diff) 2026-09-10 11:21:53 +02:00
joebre 2ae26c3920 Revert "Add full_grid_search option to acquisition"
Split out into its own PR (#1096) for independent review, since it's
an acquisition-only change unrelated to this PR's actual subject
(frequency-error-reduction tracking). This reverts commit
27832f4f53.
2026-09-10 10:08:20 +02:00
Carles Fernandez 379c99a9de Merge branch 'freq-error-reduction' of https://github.com/joebre/gnss-sdr into joebre-freq-error-reduction 2026-09-09 16:20:53 +02:00
Carles Fernandez 352e1e9fea Fix formatting 2026-09-09 16:00:13 +02:00
Carles Fernandez 4e0d7837e9 Let PVT.iono_model decide band combination in the single point solution 2026-09-09 15:57:41 +02:00
Carles Fernandez f542f6024d Merge branch 'joebre-galileo-navtype-fallback' into next 2026-09-08 13:36:56 +02:00
Carles Fernandez 08d24faa9d Restrict Galileo nav-service fallback to E1 and make it stateless
Follow-up to the galileo-navtype-fallback merge. The original fallback let
E5a/E5b/E6 observations use the other service's ephemeris, but the BGD index
for those signals is fixed by frequency, so an E5a range modeled with an
I/NAV clock would be off by BGD(E1,E5a) - BGD(E1,E5b). Only E1 selects its
BGD from the ephemeris clock model, so only E1 may fall back.

Drop the per-PRN locked set: I/NAV and F/NAV carry the same IODnav/toe,
so they expire together, and an E1 switch between services costs only BGD
quantization. select_galileo_ephemeris() is const again and simply prefers
the primary service while its ephemeris is fresh.

Also normalize a legacy Unknown provenance to the primary service on the
returned ephemeris, and allow E1 Reduced CED in F/NAV-primary receivers.
2026-09-08 11:16:34 +02:00
Carles Fernandez 1397220f76 Merge branch 'galileo-navtype-fallback' of https://github.com/joebre/gnss-sdr into joebre-galileo-navtype-fallback 2026-09-08 09:56:02 +02:00
Carles Fernandez b5332457ca Merge branch 'joebre-evk1029-signal-source' into next 2026-09-07 21:35:29 +02:00
joebreandClaude Sonnet 5 dff90d5f73 Fix CHANGELOG.md prose-wrap to match CI's prettier invocation
CI runs prettier with --prose-wrap always, which reflows Markdown
prose to the print-width regardless of existing line breaks; the
local default (preserve) had left the added bullet's original
wrapping untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jb6oqf5UHnLP1yQoMdpb3S
2026-09-07 17:20:15 +02:00
joebre 0849172803 Add an opt-in EVK1029_Signal_Source for the SAPHYRION EVK1029
EVK1029 is an Evaluation Kit for SAPHYRION SAGL space-grade ASICs: the
SY1009 RF Front-End and the SY1019 ADC/DSP. The kit includes a USB 3.0
device connected to a dual-band (E1/E5a) or triple-band (E1/E5a/E6) GNSS
front-end board, a Linux application for controlling/monitoring the
EVK1029 USB device and the SY10x9 ASICs and for saving the real-time
captured GNSS data stream to a binary file, and an EVK1029-enabled
version of GNSS-SDR.

This adds a signal source that reads the EVK1029 host application's raw
capture files directly: a continuous, header-less stream of OBA-encoded
4-bit samples (two per byte, 16 samples per little-endian 64-bit word),
with no periodic block header -- distinct enough from the generic
XML-metadata-driven ION_GSMS_Signal_Source path to warrant a dedicated
reader rather than a configuration of it.

Evk1029Source (the GNU Radio block) supports emitting multiple identical
output streams from a single read/file, so a dual-band capture (e.g. two
Freq_Xlating_Fir_Filter instances tuned to different IFs from the same
raw stream) stays sample-locked between bands: GNU Radio computes
noutput_items as the minimum available space across all declared output
ports, so backpressure from whichever downstream chain is slower stalls
production for every port together, instead of two independently-read
SignalSource instances that can silently drift apart over a long run.
The scheduler's output-multiple quantum is derived from the configured
sampling_frequency (targeting ~0.7 ms of data, rounded to the nearest
power of two) rather than hardcoded, since this block runs at the raw,
pre-decimation sample rate (up to ~180 Msps) where call-frequency
overhead measurably affects both this block and downstream readers.

Disabled by default, matching this project's convention for new
optional signal sources: build with -DENABLE_EVK1029=ON to enable it
(mirrors ENABLE_ION's wiring exactly -- CMake option, compile
definition on core_receiver, conditional registration in
gnss_block_factory.cc). Verified: clean build with the option off
(zero EVK1029 code compiled in) and with it on.

EVK1029 is being developed under an ESA contract and is expected to
become commercially available within 6-12 months.
2026-09-07 16:47:55 +02:00
joebre 088a4d2593 Fall back to the other Galileo nav message type when the primary is unavailable
Rtklib_Solver selects a single Galileo navigation service (I/NAV or F/NAV)
deterministically for the whole run based on which E5 signal is enabled.
For E1+E5a receivers this selects F/NAV, but F/NAV can take much longer to
decode than I/NAV (or never fully decode on a weak E5a signal), especially
when E1B is Doppler-assisting E5a acquisition via
GNSS-SDR.assist_dual_frequency_acq. Until now this meant such a satellite
was excluded from PVT entirely, even though a perfectly usable I/NAV
ephemeris was already available from E1B.

This fix is necessary for a cold start (no ephemeris/AGNSS assistance) to
work at all in an E1+E5a configuration with
GNSS-SDR.assist_dual_frequency_acq=true: without it, TTFF explodes because
the receiver sits waiting for F/NAV to decode on E5a before Galileo ever
contributes to the solution, even though a usable I/NAV ephemeris from
E1B is available much sooner.

select_galileo_ephemeris() now falls back to the other service per
satellite when the primary one isn't usable yet. Once a satellite
successfully uses the primary service it's locked onto it, so its
clock/orbit model never bounces between the two services once promoted.
The lock is released if the primary ephemeris later expires, so an
expired primary ephemeris never permanently excludes a satellite that
still has a usable ephemeris from the other service.
2026-09-07 11:01:05 +02:00
joebre 84a50d98d6 Decouple bit/secondary-code sync from the carrier phase-lock gate, add time-to-fix diagnostics
acquire_secondary()'s differential (delta-phase) demodulator only needs
inter-symbol phase drift to stay under +-90 deg -- a much looser
requirement than d_carrier_lock_test's coarse cos(2*phi) phase-lock
test (NBD/NBP over the Prompt correlator output). Gating symbol
accumulation on that phase-lock test was therefore blocking bit sync
in cases where the differential decoder would have worked fine, and
-- since the gate cleared the whole accumulation window on any brief
dip below threshold -- made bit sync sensitive to short-lived phase
instability that has nothing to do with whether consecutive symbols
can still be differentially decoded.

Removed the gate from both accumulation sites (secondary-code and
non-secondary preamble-correlation branches); accumulation now runs
unconditionally once pull-in has cleared, same as before this gate was
first added.

Added instrumentation (log_time_to_fix_breakdown(), gated on nothing
extra -- prints once per channel when bit/secondary-code sync
completes) reporting, per channel: total time from tracking start to
bit sync, how much of that was spent before first phase lock vs. after
it, how many accumulation-buffer resets happened, how many
acquire_secondary() attempts were made, and raw case-2 cycle counts.
This is what surfaced the fix above: several channels were shown to
complete bit sync entirely before ever reaching phase lock at all
(time_to_phase_lock_ms: never_reached), and channels that did reach
lock still showed hundreds of accumulation-buffer resets from brief
phase-lock flicker despite the carrier never truly losing lock by any
other measure.

Verified live against a real capture (single-band Galileo E1, 8
channels): 6/6 repeated runs after this change used all 8 configured
satellites in the first position fix, versus 6 of 8 before it -- some
channels' bit sync had not finished in time to contribute. Time to
first fix itself is unchanged (dominated by the separate, fixed
pull_in_time_s pull-in timer, unrelated to this gate), so the benefit
here is completeness/robustness of the first fix, not raw speed.

A prior attempt at this same gate removal was reverted for showing
loss-of-lock regressions in earlier testing, before this
diagnostic existed and before the carrier-aiding code-phase fix
in the previous commit. Flagging that history rather than claiming
certainty it's resolved -- this needs continued real-world testing,
not just the runs described above.

Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-06 16:58:09 +02:00
joebre 77dfb80a2d Re-trigger CI (unrelated flaky test-data download failure)
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-06 14:35:51 +02:00
joebre a8a534b66e Fix bit/secondary-code sync and acquisition-to-tracking code phase, needed for the frequency-error-reduction improvement to hold
Three fixes, developed and validated together, that the frequency-error
reduction scan's measured improvement (26/30 -> 4/30 trials with a
loss-of-lock event) depends on but that were missing from this PR:

- acquire_secondary(): replace the absolute-sign bit detector with a
  differential (delta-phase) demodulator. Re{s[i] * conj(s[i-1])} < 0
  means the bit flipped between symbols i-1 and i, so this only needs
  inter-symbol phase stability, not absolute phase lock -- letting
  bit/secondary-code sync start as soon as pull-in clears. The
  d_carrier_lock_test >= d_carrier_lock_threshold gate on symbol
  accumulation is kept (in both the secondary-code and non-secondary
  preamble-search branches): A/B testing without it showed loss-of-lock
  regressions and less repeatable runs, so accumulation is restarted
  (buffer cleared) whenever lock drops mid-window instead.

- Doppler-aid the code rate used to bridge the acquisition-to-tracking
  alignment gap, same formula run_dll() already applies every epoch.
  Without this, the elapsed code phase over that gap (which varies
  attempt to attempt with scheduling/threading jitter) is computed at
  the bare nominal chip rate; the resulting uncompensated phase error
  can reach a full chip or more at typical Doppler and gap values,
  landing the very first correlation on an arbitrary, unpredictable
  fractional-chip offset instead of the true code phase.

Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-06 13:37:35 +02:00
Carles Fernandez 09a42eb5dc Merge branch 'vladisslav2011-monitor_decimation_fix' into next 2026-09-05 18:59:11 +02:00
Carles Fernandez 1ba1f37d3b Polish the gnss_synchro_monitor decimation fix 2026-09-05 12:46:30 +02:00
joebre da697f70be Re-trigger CI (previous run's only failure was the unrelated flaky RinexPrinterTest, not this change)
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-05 10:46:04 +02:00
joebre 27832f4f53 Add full_grid_search option to acquisition
Previously, the (possibly still noisy, partially non-coherently
accumulated) grid was checked against threshold after every single
dwell, and the first bin anywhere to cross it won immediately -- even
if a later dwell's fuller integration would have revealed a different,
genuinely stronger peak elsewhere in the same grid that never got the
chance to be compared.

Adds Acquisition_<Sig>.full_grid_search (default false, preserving the
existing early-exit behavior): when true, the accept/reject decision
is deferred until the final dwell, from the fully max_dwells-
accumulated grid, instead of exiting on the first dwell whose
still-partial accumulation happens to cross threshold.

Combined with the frequency-error reduction scan, this addresses a
recurring Galileo E1-B loss-of-lock failure mode: measured over 30
trials against a live capture, enabling full_grid_search alone still
left 26/30 trials with at least one loss-of-lock event (66 total
events); adding the frequency-error scan on top brought that down to
4/30 trials (5 total events).

Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-05 10:46:04 +02:00
joebre d7a767f775 Extract frequency-error reduction back into its own state 5
State 5's Doppler-bin scan had drifted into being interleaved with
state 2's normal tracking code (a per-epoch "is the scan still active"
branch inside run_dll()/run_dll_pll()) instead of staying a genuinely
separate state, making it harder to reason about and review in
isolation. Restores the original architecture: case 1 (pull-in) enters
state 5 when f_error_step_num != 0, state 5 is fully passive (drives
only the carrier, never touches the DLL/PLL loop filters), and
transitions to state 2 once all bins are scanned.

Also folds in refinements made since the original state-5 design:

- Per-bin scan results (Doppler tested, accumulated power, selected
  bin) are dumped to a CSV file (Tracking_<Sig>.f_error_dump_filename,
  default ./f_error_dump.csv) for offline inspection of the scan's
  frequency-response envelope.
- A CN0 estimate for the winning bin, from its own raw Prompt samples.
- The carrier loop filter is explicitly re-seeded with the scan's
  winning Doppler once it completes -- otherwise the first closed-loop
  epoch after the scan would compute its output from the loop filter's
  stale, acquisition-anchored internal state and jump back toward the
  raw acquisition Doppler before re-converging on its own, discarding
  the scan's result.

Because state 5 never runs the DLL/PLL loops at all, code phase and
the carrier loop filter's internal state stay exactly as pull-in left
them for the scan's whole duration -- this is what keeps Galileo E1
(BOC(1,1), whose autocorrelation has real side lobes, unlike GPS C/A's
clean single-peak triangle) from occasionally settling the code loop
onto a side lobe mid-scan, which previously produced a scrambled,
non-sinc-shaped envelope in the per-bin dump.

Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-05 10:45:17 +02:00
joebreandClaude Sonnet 5 b35df9b78f Fix clang-tidy warnings in frequency-error-reduction code
Drop the redundant d_f_error_power() member-initializer (readability-
redundant-member-init: std::vector already default-constructs to
empty), and split f_error_bin_multiplier()'s integer division from
its cast to double into two statements so the intent (integer
half-step count, then converted to a Doppler multiplier) is explicit
rather than triggering bugprone-integer-division/modernize-use-auto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-05 10:45:17 +02:00
joebreandClaude Sonnet 5 9f3b4a16da Add unit tests for tracking frequency-error reduction
Covers Dll_Pll_Conf::SetFromConfiguration() parsing of f_error_step_num
(default disabled, odd values pass through, even values round up to
odd, zero stays disabled) and f_error_accumulation/f_error_doppler_step
pass-through, plus dll_pll_veml_tracking::f_error_bin_multiplier()'s
bin-index-to-Doppler-offset mapping, including that step_num=5
reproduces the original fixed 5-bin scan order.

f_error_bin_multiplier() is made public so it can be exercised
directly from the test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-05 10:45:16 +02:00
joebreandClaude Sonnet 5 50b1fef480 Add configurable frequency-error reduction state to DLL/PLL tracking
After pull-in and before entering wide tracking (state 2), optionally run
a passive Doppler bin scan around the pull-in Doppler estimate: for each
of an odd number of symmetric frequency bins (center, then alternating
+step/-step outward), correlate for a configurable number of incoherent
accumulations with no DLL/PLL filter updates. At the end of the scan, the
carrier Doppler is set to whichever bin produced the highest accumulated
power before the tracking filters are engaged. This helps in cases where
the residual carrier frequency error left after acquisition/pull-in would
otherwise leave the narrow tracking loops outside their pull-in range.

New per-tracking-role configuration parameters (all optional):

  Tracking_1C.f_error_step_num     = 5    ; number of Doppler bins to
                                           ; test. Must be odd; if an
                                           ; even value is given, 1 is
                                           ; added. 0 (the default)
                                           ; disables the mechanism
                                           ; entirely.
  Tracking_1C.f_error_accumulation = 20   ; incoherent accumulations
                                           ; per bin
  Tracking_1C.f_error_doppler_step = 250  ; bin spacing, in Hz

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-09-05 10:45:15 +02:00
Carles Fernandez 3053cebd3a Merge branch 'monitor_decimation_fix' of https://github.com/vladisslav2011/gnss-sdr into vladisslav2011-monitor_decimation_fix 2026-09-04 20:22:17 +02:00
Carles Fernandez 980185eb3d Reinitialize CNAV Viterbi decoder on channel restart (GPS L5/L2C). Contributed by @joebre at #1087 2026-09-04 19:55:08 +02:00
Carles Fernandez 72bfc059d7 Fix sample-counter drift in GPS L1 C/A fine-Doppler acquisition (Fixes #1080) 2026-09-04 19:45:43 +02:00
Carles Fernandez 343272ad3c Merge branch 'joebre-pcps-acquisition-idle-cpu-fix' into next 2026-09-04 18:54:36 +02:00
Carles Fernandez 81569a8771 Batch idle-state general_work() calls in the E5a CAF acquisition block 2026-09-04 18:50:00 +02:00
Vladslav P 0437bd8c65 Add gnss_syncro_monitor test
Test basic gnss_syncro_monitor operation.
Test decimation at different decimation factors.

Signed-off-by: Vladslav P <vladisslav2011@gmail.com>
2026-09-04 18:13:33 +03:00
Vladslav P 32911d3894 Fix run_tests random crash during termination
Implement proper protobuf library cleanup mechanism.
This prevents use after free and hard to debug crashes.
2026-09-04 18:12:44 +03:00
Vladslav P 431f788a76 gnss_synchro_monitor: fix decimation
Consuming input samples only when the decimation counter reaches it's
limit prevents decimation from working correctly. It just changes one-by-one
output to batch output and attempts to decimate batches instead of
individual synchos.
Consume unconditionally in every outer loop iteration and change loop
nesting to make sure, that all channels will be decimated equally and no
batches will be formed.
Add some safty checks against read beyond the end of the buffer and empty
array submission to Gnss_Synchro_Udp_Sink.

Signed-off-by: Vladslav P <vladisslav2011@gmail.com>
2026-09-04 18:10:08 +03:00
Carles Fernandez cb3566c276 Merge branch 'pcps-acquisition-idle-cpu-fix' of https://github.com/joebre/gnss-sdr into joebre-pcps-acquisition-idle-cpu-fix 2026-09-04 16:22:40 +02:00
joebre 71dc011994 pcps_acquisition: batch idle-state general_work() calls to cut scheduler overhead
While a channel is idle (not actively searching), general_work() only
drains its input via consume_each() to avoid stalling the upstream
block -- it produces no output either way. Without any batching hint,
GNU Radio's thread-per-block scheduler was waking this block's thread
for every small burst of new input: observed live at 5,000-22,000
calls/s per idle channel, handling only ~850-3,400 samples each. Total
throughput matched the configured sample rate exactly (no excess data
movement), confirming this was pure per-call scheduling overhead
(mutex lock, function call, thread wake), not real work -- measured as
a steady 17-23% CPU per idle acquisition thread that should have been
doing nothing.

Fixed with set_output_multiple(), forcing the scheduler to accumulate
more input per wakeup. The batch size is derived from the signal's own
Acq_Conf::samples_per_ms (one PRN code period at that signal's
decimated rate) rather than a fixed constant: the algorithm never does
anything meaningful below that granularity, and deriving it self-scales
across signals/configs at different sample rates instead of being
tuned for one particular setup. Verified live: call rate dropped ~10x,
idle-thread CPU dropped from a steady 17-23% to mostly 3-12%. Also
tested output_multiple values well beyond samples_per_ms (up to 8x):
no further CPU reduction, confirming this is the practical floor for
this fix, not an arbitrarily chosen number.
2026-09-04 11:19:35 +02:00
Carles Fernandez 8c116a7d1e Make RinexPrinterTest more robust in CI environments 2026-09-03 21:45:37 +02:00
Carles Fernandez 89a2004d26 Fix E6 TOW anchoring for pseudorange and PVT computation 2026-09-01 22:04:52 +02:00
Carles Fernandez 6d32699ea6 Update local version of Protocol Buffers to v36.1 2026-09-01 12:11:10 +02:00
Carles Fernandez cae59e5b1b Merge branch 'joebre-feat/assisted-acq-doppler-narrowing' into next 2026-09-01 12:10:14 +02:00
Carles Fernandez 1f8f688ed1 Fix test building in some environments 2026-09-01 10:09:41 +02:00
Carles Fernandez bd66dd2c25 Improve dual-band acquisition assistance Doppler projection 2026-09-01 09:25:49 +02:00
Carles Fernandez 0af508713a Fix Acquisition dump readers
Update changelog
Add Joel to the CITATION.cff file
2026-09-01 09:22:38 +02:00
Carles Fernandez b980b55ed2 Fix one-bin assisted acquisition narrowing 2026-08-31 15:19:58 +02:00
Carles Fernandez df2300d89f Merge branch 'feat/assisted-acq-doppler-narrowing' of https://github.com/joebre/gnss-sdr into joebre-feat/assisted-acq-doppler-narrowing 2026-08-31 14:44:16 +02:00
Carles Fernandez f0482a9b00 CI: update android job 2026-08-31 10:26:48 +02:00
joebre 8f67752b9e Address review: memory safety, candidate selection, PFA, dump, naming
- Rename doppler_uncertanty -> doppler_uncertainty throughout (interface,
  adapter, block, comments, log message).

- Fix the out-of-bounds write and mis-detection in narrowed-mode
  activation: track it with an explicit d_doppler_search_narrowed flag,
  set directly in set_doppler_uncertainty() and guarded on
  d_num_doppler_bins > 1, instead of inferring it from
  "d_num_doppler_bins_active < d_num_doppler_bins". That inferred check
  broke two ways: for a 1-bin full grid (a valid, already-accepted
  config), it still forced 2 active bins, writing past the grid/wipeoff
  allocations sized to d_num_doppler_bins; for a 2-bin full grid, an
  actually-narrowed request computes the same active count as the full
  grid (2 < 2 is false), so it silently searched the full grid's own
  bin layout instead of the assisted {center, center + doppler_max}
  pair. Reproduced the out-of-bounds write directly (temporarily
  reverting to the inferred check crashes with "corrupted double-linked
  list" under the new NarrowingEnabledOneBinGrid test) and the
  mis-detection (NarrowingEnabledTwoBinGrid).

- Keep the noise-reference bin out of candidate selection: both
  max_to_input_power_statistic() and first_vs_second_peak_statistic()
  now take a separate candidate_count, used only for the peak-search
  loop bound; the CFAR path's own num_doppler_bins (used for the
  "opposite bin" reference lookup) is unchanged. In narrowed mode,
  candidate_count is 1 -- the noise-reference bin can no longer win and
  be reported as the acquisition result, regardless of its own power
  (verified against a live signal deliberately placed in the reference
  bin instead of the known/candidate bin: reverting this fix, both the
  CFAR and peak-ratio statistic mistakenly report the reference bin's
  Doppler as if it were the assisted center).

- Recalibrate the acceptance threshold for narrowed acquisition: adds
  d_threshold_narrowed, computed like d_threshold but with
  num_doppler_bins = 1 (the true candidate count in narrowed mode)
  instead of the full grid's bin count, since compute_threshold() folds
  the candidate count into the false-alarm probability and reusing the
  full-grid threshold in narrowed mode was effectively far stricter
  than the requested PFA. get_threshold() returns it whenever
  d_doppler_search_narrowed is true.

- Fix narrowed dump layout/metadata: ensure_dump_grid_allocated() now
  sizes acq_grid to d_num_doppler_bins_active (not the full grid width),
  matching what copy_magnitude_grid_to_dump_grid() actually writes each
  cycle -- this also makes narrow<->full transitions between dumps
  self-correcting. dump_results() writes accurate, mode-appropriate
  doppler_max/doppler_step (the same {0, doppler_max} encoding
  compute_statistics() uses internally when narrowed), plus new
  doppler_center and doppler_narrowed fields so offline post-processing
  can tell which encoding is in effect. Verified end to end against a
  live dump (NarrowedDumpMetadata).

- Strengthen Acq_Conf::enable_doppler_narrowing's doc comment (default,
  which implementations it applies to, its dependency on the caller
  passing doppler_uncertainty == 0) and add a commented usage example
  next to GNSS-SDR.assist_dual_frequency_acq in a dual-frequency
  Galileo E1B+E5a example config.

- Add tests/.../pcps_acquisition_doppler_narrowing_test.cc: 8 focused
  tests (narrowing enabled/disabled, 1-bin and 2-bin full-grid
  configurations, CFAR and peak-ratio statistics, a full-narrow-full
  transition, two-step acquisition with step-1 narrowing, and narrowed
  dump metadata), built on the same real-capture/known-ground-truth
  pattern as gps_l1_ca_pcps_acquisition_test.cc. Confirmed each targeted
  regression test actually fails without its corresponding fix before
  finalizing them.

Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-08-31 09:38:13 +02:00
Carles Fernandez 443a93a63f Merge branch 'feat/assisted-acq-doppler-narrowing' of https://github.com/joebre/gnss-sdr into joebre-feat/assisted-acq-doppler-narrowing 2026-08-29 14:15:28 +02:00