With -DENABLE_CUDA=ON, band<n>_cuda=true computes that band's
frequency-translating FIR (complex composite taps applied to the real
capture) on an NVIDIA GPU through the new CudaDdcEngine, instead of on
a CPU worker thread. As with the CUDA acquisition engine, the public
interface has no CUDA types and hides the implementation behind a
PIMPL. Each band falls back to the CPU, with a warning in the log, if
its engine cannot be created or a GPU call fails. Builds without CUDA,
and bands without the flag, are unchanged.
Taken from PR #1144, plus:
- Report the CUDA status per band with LOG instead of std::cout/cerr
- Check cudaGetLastError() after the kernel launch, so a failed launch
is not mistaken for valid output
- Select the engine's device in set_taps(), compute() and the
destructor, since compute() runs on the band worker threads
- Build the engine only with both ENABLE_CUDA and ENABLE_EVK1029
With enable_freq_xlating=true, Evk1029Source translates, filters and
decimates each band itself and exposes one gr_complex output port per
band, so Bypass Signal Conditioners can connect the channels straight
to the source. Each band's FIR runs on its own worker thread, and the
next chunk of the capture is decoded while the current one is filtered.
Per-band filter design: band<n>_IF, _decimation_factor, _bw, _tw,
_window, _attenuation_dB and _kaiser_beta.
Taken from PR #1143 without its SignalConditioner Bypass commits (next
already provides Bypass), plus:
- Center shorter band filters on the longest one, so every band has the
same group delay (mismatched tap counts biased pseudoranges between
bands by up to km)
- Fix source dump in fused mode: one gr_complex file per band (_ch<n>)
- Reject unequal or non-positive band decimation factors
- Build with GNU Radio < 3.9 (firdes window type) and VOLK < 2.5
(rotator), use volk_gnsssdr::vector
- Link Gnuradio::filter privately and only with ENABLE_EVK1029
- Start worker threads after the capture file is opened
With blocking=false and max_dwells > 1, copy the whole integration window
into a per-block buffer, so that general_work() keeps consuming input while
the worker integrates. The shared conditioner buffer no longer holds back
every channel during a search.
- set_history() keeps the last window readable, so a search starts as soon
as one new dwell arrives. Dwell offsets in the window follow the
dwell-residual skip, and the sample stamp is the end of the window (the
end of the first dwell while the window starts at stream index 0).
- The buffer is allocated at the first search and freed on a positive
acquisition or when the channel is deactivated.
- Deactivation stops the worker and drops the search, so the next
activation starts a new one. stop() replaces the destructor wait.
- Idle input is consumed in one call per wakeup, keeping the history.
Based on #1108 by Vladislav P.
Replace the fixed 20 ms limit on SignalConditioner.batch_size_ms and
max_batch_latency_ms. A configured batch is accepted if it is no longer
than the automatic one (min(10 ms, observable_interval_ms)), or if the
buffer it needs (whole batches, with room for one on top of the largest
reader window) fits the alignment budget: the signal time channels can
trail the receiver clock before the observables miss epochs. Batches
are also limited to 4 MiB, since GNU Radio allocates two of them on
each side of the batching block.
At observable_interval_ms=20 and 4 MSps gr_complex this allows about
90 ms instead of 20 ms. At 5 ms it now rejects batches that the old
limit let through. In automatic mode, an explicit max_batch_latency_ms
sets the batch size, so that batch gets the same check. Defaults are
unchanged.
add_obs_sat_record_line() formatted every observable group (pseudorange,
carrier phase, Doppler, CN0) unconditionally, even for the placeholder
Gnss_Synchro{} slot get_constellation_observables_map() creates for a
signal with no valid observation for a given satellite (e.g. untracked,
or simply a frequency that satellite doesn't transmit, such as a GPS
PRN5/PRN7 which has an L5 slot but only tracks/transmits L1). That
placeholder is all-zero, so it printed as a formatted "0.000" value with
a Signal Strength Indicator floored to its minimum class (1), which
reads as a real but degraded observation rather than "no data".
Per RINEX 3/4, an unavailable observation must be a fully blank field,
not a zero value. Gate the formatter on Flag_valid_pseudorange, which
already reliably distinguishes a real observable (set true by the
Observables block) from the placeholder (defaults false) -- write 64
spaces instead when false.
Select batch targets from sample rate, latency and memory limits, and
consumer buffer requirements. Replace eligible pass-through stages before
channel fan-out without machine-specific profiles or configuration edits.
Preserve samples and tags across partial batches, deadlines, and EOF.
Handle conditioned output ports consistently and clean up stream and
message connections on disconnect or connection failure.
GPS L5 is pre-operational, and the L5 health bit of CNAV message type 10 (IS-GPS-200N 30.3.3.1.1.2) is broadcast as "unhealthy" on every SV. As a result, every GPS L5 signal was reported with healthy = false in the monitor_pvt tracked_satellites list, and "not used for navigation" was printed for every GPS satellite. Neither affected the PVT solution: CNAV L2C/L5/J5 observables are not gated on these bits (the L5 exclusion was dropped in v0.0.17), and the CNAV ephemeris handed to RTKLIB carries no svh. GPS L5 now reports the LNAV SV health of the same satellite; GPS L2C and QZSS L5 keep the CNAV bits; and the console message is replaced by a LOG(INFO) line.
Rtklib_Solver::get_broadcast_signal_health() excluded these signals from
the fix based on CNAV message type 10's per-carrier L1/L2/L5 health bit
alone. Per IS-GPS-200N 30.3.3.1.1.2, that bit is only meaningful together
with the SV's LNAV configuration code (20.3.3.5.1.4), which this receiver
does not track; the spec's own instruction for a receiver in that position
is to assume every signal is available on every SV.
Confirmed this was excluding real, operational satellites: cross-checked
the adjacent Transmission Week Number field against ground truth (computed
expected GPS week for the capture date, matched exactly), ruling out a
decode/bit-offset bug. L2C/L5/J5 now fall through to the same SV-wide
health source L1 C/A already uses. Also removes the now-misleading
"not used for navigation" message tied to the same bit.
Tested against real EVK1029 hardware captures; GPS L5 satellites now
report correctly instead of being universally flagged unhealthy.
d_samples_to_consume (pcps_acquisition_adapter.cc) is floor(sampled_ms *
samples_per_ms), truncating to an integer. When the configured internal_fs_sps
does not divide evenly into a dwell (any decimation ratio leaving a fractional
number of samples per dwell), every dwell is silently shorter than the true
dwell length by a fixed residual, and that residual compounds linearly across
non-coherent dwells: the true code epoch drifts within the accumulation grid
instead of landing on the same index every time, and the sample stamp handed
to tracking drifts out of sync with it. Tracking then reconstructs the wrong
code phase at pull-in. Only shows up for short, high-chip-rate codes (Galileo
E5a, GPS L5) accumulated over several dwells -- low chip rate codes (GPS L1,
Galileo E1) absorb the same absolute drift as a much smaller fraction of a
chip.
Fix: keep each dwell's FFT/correlation window at the existing fixed
d_samples_to_consume size (so none of the buffer/FFT-plan sizing set up at
construction needs to change), but let the actual stream consumption between
dwells track the true dwell boundary with a standard DDA/Bresenham
accumulator -- skip one extra sample (discarded, never buffered) whenever the
accumulated residual reaches a full sample. This keeps every dwell's window
within half a sample of the true boundary permanently, so the drift this fix
is about never accumulates in the first place. The residual itself is
computed once, in the constructor, directly from the integer resampled_fs
(not through the lossy intermediate float samples_per_ms), and is always in
[0, 1) by construction, so there is no signed/unsigned wraparound risk.
This lives entirely in the sample-consumption state machine in general_work(),
upstream of doppler_grid_cpu()/doppler_grid_cuda()/update_synchro(), so CUDA
builds and bit_transition_flag mode need no special-casing -- they share the
same corrected consumption automatically. It is also correct for
coherent_integration_time_ms spanning more than one code period, since the
residual is derived from the same sampled_ms used for d_samples_to_consume,
not from a single code period.
A no-op whenever samples_per_code is already an integer (any internal_fs_sps
dividing evenly into one dwell): the residual is then exactly 0, so the skip
counter never fires.
Verified against a real EVK1029 dual-band capture (Galileo E1+E5a, GPS
L1+L5): the untouched case (samples/dwell already integer) stayed at ~2
spurious loss-of-lock events per 90s; a case with 0.5 samples/dwell of
truncation error went from 50+ loss-of-lock events per 4 minutes (tracking's
own frequency-error-reduction diagnostic scan showing no discernible
correlation peak at all) down to 1 event per ~41s -- the same order as the
untouched case -- with sharp, correct correlation peaks and CN0 estimates
jumping from ~20-30 dB-Hz (meaningless, noise-floor) to ~42-52 dB-Hz (genuine
lock).
Dll_Pll_Conf::f_error_dump_filename has a non-empty default, so the
frequency-error-reduction scan's per-bin CSV dump was effectively
always on with no clean way to disable it. Add a new f_error_dump
boolean (default false), matching the existing dump/Tracking_<sig>.dump
convention, and gate the dump call on it.
Contrary to ICD, these satellites actually broadcast B2a (OTA upgraded
in the orbit).
Verified by decoding live signal in realtime configuration.
Signed-off-by: Vladislav P <vladisslav2011@gmail.com>
SatelliteVisibility::PredictedDopplerHz() previously required a live PVT
fix and was only used for a satellite's primary signal
(GNSSFlowgraph::acquisition_manager()). Two extensions:
1. Secondary signals (e.g. GPS L5, Galileo E5a) can now use the same
geometric prediction, opt-in per acquisition role via
<acq role>.alm_ephe_assisted_doppler_narrowing (default false). It's
only reached at all when dual-frequency assist is unavailable or
disabled (GNSS-SDR.assist_dual_frequency_acq=false), and is a distinct,
less-proven Doppler source than that dual-frequency projection, hence
opt-in rather than defaulted on.
2. Before a live fix exists, PredictedDopplerHz() can now predict from the
AGNSS reference position (zero assumed velocity) and the configured
nominal clock offset, opt-in via GNSS-SDR.doppler_prediction_before_fix
(default false). A new PredictedDopplerUncertaintyHz() reports the
Doppler spread implied by GNSS-SDR.receiver_max_velocity_m_s and
GNSS-SDR.clock_frequency_max_error_ppm for this pre-fix case (zero once
a fix exists, since position/velocity/clock are all solved then, not
assumed) -- the caller widens its acquisition Doppler-bin search by this
amount instead of trusting a single, possibly-wrong bin.
Verified against an EVK1029 GPS L5 capture (single-band, so every L5
channel is a secondary signal): with both flags enabled, the almanac-based
prediction narrows acquisition Doppler search before and after the fix,
logged via a new low-noise [alm_ephe_doppler] line; with them left at
their defaults, behavior is unchanged from before this change.
SatelliteVisibility::PredictedDopplerHz() gates its live-fix path on a
freshness check against last_fix_time_s_/last_fix_receiver_time_s_, both of
which are only stamped by SatelliteVisibility::Tick() (called from
GNSSFlowgraph::MaybeUpdateVisibility()).
In ControlThread::event_dispatcher(), acquisition_manager() -- which calls
PredictedDopplerHz() -- ran before MaybeUpdateVisibility() in the same
iteration. Since PVT updates roughly once per second, PredictedDopplerHz()
was therefore always comparing against a timestamp exactly one PVT epoch
stale, failing its own freshness check on every single call once a live
fix existed. The prediction only ever succeeded in the brief window right
after a periodic full visibility recompute happened to re-trigger a fresh
pick synchronously within the same call.
Reordering so MaybeUpdateVisibility() (and therefore Tick()'s stamping)
runs before acquisition_manager() each iteration fixes this: the timestamp
PredictedDopplerHz() checks is always current for the epoch it's
evaluating. acquisition_manager()'s existing "only when idle" gate is
preserved exactly (previously the implicit else of the valid_event check,
now an explicit !valid_event condition).
Verified against an EVK1029 GPS L5 capture with visibility-aware search
and almanac-based Doppler prediction enabled: before this fix, the
prediction log line never appeared after the first fix except in narrow
bursts tied to the ~120s full-recompute interval; after, it fires
continuously for every still-unlocked visible satellite each cycle.