Visibility-aware acquisition search (GNSS-SDR.enable_visibility_aware_search):
- Classify GLONASS and QZSS satellites too. GLONASS positions come from the
GNAV almanac (new Glonass_Gnav_Almanac::satellite_position with the
standard perturbation model), so the search order benefits before any
GLONASS ephemeris is decoded.
- PVT publishes a lock-free navigation snapshot (ephemeris, almanac, UTC
models per system) that the classifier reads instead of locking the PVT
maps on every recompute.
- Recompute on new navigation data, on position changes larger than
visibility_recompute_position_threshold_m, every
visibility_recompute_interval_s, and when the almanac ages out; fix the
periodic and position-triggered recomputes that never fired, and the
off-by-one that excluded satellites in [mask, mask+1) degrees.
- Enter the start-up standby only when the feature is enabled, so the
default search order is untouched.
Secondary-frequency acquisition assistance:
- Do not start an acquisition on the PRN 0 placeholder left on assisted
channels at connect time; it threw inside the control thread and left the
flowgraph running headless with the secondary band never acquired.
- Match GLONASS assistance by FDMA frequency channel: the search pool keeps
one slot per channel while tracked L1 channels report the decoded slot, so
L2 was never picked.
- Assist from locked channels before their telemetry is decoded: the
observables status now reports tracking-only channels and the flowgraph
keeps a separate tracking map for the assisted search.
compute_visible_satellites() correctly decides visible-vs-excluded
using the raw double elevation (`El > elevation_mask_deg`), but then
truncates the returned elevation to floor(El) for the diagnostic dump.
Tick() later re-derives the same visible/excluded decision from that
already-floored value (twice: once to separate a health-only exclusion
from an elevation one, once for the final visible_/excluded_ split).
For a satellite whose true elevation lands in [mask, mask+1) --
e.g. El=15.63 against a 15 deg mask -- floor(El) collapses to exactly
`mask`, and the strict `>` re-check then wrongly excludes a satellite
that was already correctly accepted by the untruncated comparison.
Verified live: G11 at El=15.63 (15 deg mask) stayed stuck in EXCLUDED
across a full recompute instead of promoting to VISIBLE.
Both re-derivations need `>=` instead of `>` to correctly compensate
for the floor() truncation bias (proof in the added comments); this is
exact for every fractional elevation except one landing exactly on the
integer mask value, a measure-zero case for a continuous real angle.
last_recompute_rx_time_s_ and last_recompute_r_eb_e_ were being reset
on every Tick() that passed the early-return gate, including a
data_changed-only (targeted) recompute -- not just on an actual full
sweep, despite both being documented as tracking the last *full*
recompute. With a dozen+ satellites actively updating their ephemeris/
almanac, data_changed fires often enough to keep resetting both clocks
before visibility_recompute_interval_s (or the position-displacement
threshold) is ever reached, silently starving the periodic full
recompute -- the one mechanism that can catch a satellite rising above
the horizon with no ephemeris/almanac change of its own to trigger a
targeted recompute.
Move both assignments inside the needs_full_recompute block so they
only advance when a full sweep actually just ran.
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.
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.
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
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.
The previous commit filtered NaN azel out of tracked_satellites
entirely, which was the wrong place: NaN there is a legitimate,
informative result (ephemeris/almanac was corrupt enough to produce
one) that the monitor and its consumers should see and handle
explicitly, not have silently hidden.
What must never happen is a NaN satellite influencing the actual
position fix. Add that guard where it belongs: rescode() in
rtklib_pntpos.cc, right after satazel() and prange(), using
std::isfinite() (neither elaux < opt->elmin nor prange()'s P == 0.0
sentinel catches NaN, since ordinary relational comparisons are
vacuously false against it). This is the single gatekeeper for
vsat[]/nv, so the guard transitively protects everything downstream
that filters on vsat[] too: DOP computation, the chi-square residual
check, RAIM FDE, and the velocity/Doppler solve (resdop()/estvel()) --
not just the position solve directly.
tracked_satellites reporting goes back to reporting azel as computed,
NaN included.
None of RTKLIB's own sanity checks (eph2pos()/alm2pos()'s A <= 0 guard,
the bounded Kepler iteration, geodist()'s Earth-radius check, or the
elevation mask) catch a NaN azel: they're all ordinary numeric
comparisons, which are vacuously false against NaN. A sufficiently
corrupt ephemeris/almanac (e.g. an out-of-range eccentricity feeding
sqrt() a negative argument) can therefore produce a NaN azimuth/
elevation that sails through every check -- including the old
azel != 0.0 test, since NaN != 0.0 is true -- and gets reported into
tracked_satellites as a literal NaN.
Add std::isfinite() to that check so a NaN azel is treated the same as
"no data" (skipped) instead of being reported.
A field named "used" inside a message called UsedSatellite read
oddly, especially since the message reports every tracked satellite
(used or excluded), not only used ones. Renamed the message and its
repeated field (tracked_satellites), plus the matching C++/protobuf
serialization types (Monitor_Pvt::TrackedSatelliteInfo,
tracked_satellites), so "used" now reads sensibly as a per-satellite
flag on a TrackedSatellite rather than inside a UsedSatellite.
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.
rescode() (rtklib_pntpos.cc) computes each satellite's azimuth/elevation via
satazel() before checking it against PVT.elevation_mask, and pntpos() copies
that azel into ssat[] unconditionally -- only ssat[].vs is gated by the mask
(the same applies to a satellite dropped by RAIM FDE). The used_satellites
loop in rtklib_solver.cc was keying its entire per-satellite entry on vs, so
a tracked satellite excluded from the fix (below the elevation mask, or by
RAIM) disappeared from MonitorPvt entirely -- indistinguishable from a
genuine tracking problem.
Key entries on whether azel was actually computed this epoch instead, and
add a separate `used` flag (default true) reflecting vs. A satellite that
is tracked but excluded from the fix now still reports its real az/el, with
used = false.
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
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.
- satellite_visibility.cc/.h were missing SPDX-FileCopyrightText, causing
the REUSE-compliance check to fail. Added, matching the format used by
other recently-added files in this project.
- alm_to_rtklib(const Gps_Almanac&, int ref_week) and the Galileo overload
dropped the old single-argument form entirely, breaking two existing
unit tests (qzss_lnav_navigation_message_test.cc) that call
alm_to_rtklib() without a reference week and only check fields unrelated
to the resolved week. Gave ref_week a default of 0, which
resolve_truncated_week() already treats as "no reference -- use the
value as-is" -- reproducing the exact old behavior for callers with no
reference week available, with no change for callers that do pass one.
Verified: full local test suite (662 tests) now passes, including both
previously-failing tests; matches the exact failures reported by
build-ubuntu/build-macos/build-macos-xcode CI on this PR.
GNSS-SDR.visibility_ephemeris_max_age_s applied one shared threshold
(default 7200 s) across GPS, Galileo, and BeiDou ephemeris alike, and
duplicated (with the wrong value for two of the three systems) validity
figures RTKLIB itself already defines and uses internally in seleph():
MAXDTOE (GPS/QZSS, 2h), MAXDTOE_GAL (Galileo, 3h), MAXDTOE_BDS (BeiDou,
6h).
compute_visible_satellites() now checks each system's ephemeris age
against its own RTKLIB constant directly instead of a single configurable
value, removing the parameter and its config surface entirely. This also
means ControlThread::get_visible_sats() -- which never passed an
ephemeris age at all, relying on the old default of infinity (no
staleness check) -- now gets the same per-system staleness filtering for
free, consistent with what RTKLIB's own solver would actually consider
usable.
Almanac has no RTKLIB-side equivalent (RTKLIB's own solver never uses
almanac), so GNSS-SDR.visibility_almanac_max_age_s remains as-is.
GNSS-SDR.visibility_sanity_check_threshold_deg gated a diagnostic-only
warning for when a satellite's classified elevation jumped implausibly
far between readings. It was added to catch a specific decoding
regression (an unresolved truncated almanac week number) that's since
been fixed at its source, and hasn't fired since. Keeping it as a
user-facing .conf parameter added config surface for a threshold
essentially no one would ever need to tune.
reference_elevation_deg_ itself stays: it's also the cache the
diagnostic report now reads from for satellites a targeted recompute
didn't touch this tick. It's simplified to record every reading
unconditionally, including the health-gated (forced-excluded) case
that used to skip it purely to avoid a spurious jump warning on that
now-removed path -- which also fixes a minor gap where a health-
excluded satellite's elevation was never cached for the report.
Today, GNSSFlowgraph picks the next satellite to acquire on an idle
channel from a single FIFO queue per signal type. Visibility only ever
affects that queue's initial order, computed once at hot/warm start;
after that, every satellite -- visible or not -- is just a FIFO entry,
and a satellite that fails acquisition goes to the back of the same
queue. On a receiver with a large candidate PRN pool relative to the
satellites actually in view, this means channels keep grinding through
acquisition attempts against satellites that were never going to be
found, burning CPU that could go toward satellites actually overhead.
This adds continuous visibility tracking instead: as the receiver gets
a fix (or an AGNSS reference position) and as ephemeris/almanac data
arrives, satellites are classified into "visible" (elevation computable
and above GNSS-SDR.search_elevation_mask), "excluded" (elevation
computable and at/below the mask), or implicitly "maybe visible" (no
data yet -- still searched, just at lower priority). Classification is
re-evaluated whenever the fix becomes valid, new/updated ephemeris or
almanac data arrives for a PRN, the receiver moves significantly, a
periodic interval elapses, or classified data ages past its staleness
threshold -- not just once at startup.
A satellite that's actually acquired or tracking is never torn down
because of this classification: the search mask exists purely to avoid
wasting CPU on unlikely candidates, not as a correctness gate --
PVT.elevation_mask remains the real gate on whether a tracked
satellite's observations get used in the fix. Only the search layer
(picking new acquisitions, and not retrying a specific satellite after
a genuine loss of lock) is scoped by visibility.
Whole feature is gated behind GNSS-SDR.enable_visibility_aware_search,
default false -- disabled, every existing code path is untouched.
Key pieces:
- New SatelliteVisibility class (src/core/receiver/satellite_visibility.cc/.h):
owns the classification state and the recompute logic, including a
targeted-recompute path that only touches the satellite(s) whose data
actually changed instead of the whole constellation, when nothing else
that could affect every satellite's elevation (receiver position,
elapsed time) has changed.
- GNSSFlowgraph: new visible/maybe-visible queue selection in
search_next_signal(), ratio-controlled via
GNSS-SDR.visible_vs_mayvisible_search_ratio, plus reclassification
handling when a channel's assigned-but-not-yet-started satellite
becomes known excluded.
- ControlThread: MaybeUpdateVisibility() is called unconditionally from
event_dispatcher(), not just its idle branch -- the search layer's own
acquisition churn keeps the control queue busy enough that an
idle-only trigger would starve for the entire run right when
reclassifying satellites out of "maybe visible" is what would reduce
that churn.
- rtklib_conversions/rtklib_ephemeris: resolve_truncated_week() and
reference-week-aware alm_to_rtklib() overloads, needed to correctly
resolve Galileo's 2-bit and GPS's 8-bit truncated almanac week numbers
against an arbitrary query time rather than only the receiver's own
current time.
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.
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>
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.