From 98e0fb25dbe5b5fbb87258000e5c62f65e420cef Mon Sep 17 00:00:00 2001 From: joebre Date: Tue, 15 Sep 2026 17:16:14 +0200 Subject: [PATCH 1/2] Add ephemeris/almanac-based Doppler prediction as an acquisition search center (fix-only) SatelliteVisibility::PredictedDopplerHz() predicts a satellite's Doppler on a given signal from whichever of its ephemeris/almanac is currently usable (same freshness rules as compute_visible_satellites()), evaluated at the live PVT fix position/time/velocity, plus the receiver's live solved clock drift (fix_status.user_clk_drift_ppm) scaled to the signal's carrier. Requires a currently-valid fix; returns std::nullopt otherwise. Wired into acquisition_manager(): when dual-frequency assist isn't available, a satellite already classified visible now gets a single-bin Doppler search centered on this prediction instead of a full-range search. No-op unless GNSS-SDR.enable_visibility_aware_search is enabled, the satellite is actually visible, and a live fix currently exists, so behavior is unchanged with the feature off or before the first fix. First step only: no AGNSS-reference (no-fix) fallback yet. Before a fix, the geometric prediction would have to assume zero receiver velocity and a static clock offset, and a single search bin can't absorb the resulting uncertainty -- that needs GNSS-SDR.clock_frequency_max_error_ppm / receiver_max_velocity_m_s to widen the search accordingly, deferred to a follow-up PR. --- src/core/receiver/gnss_flowgraph.cc | 30 +++++- src/core/receiver/satellite_visibility.cc | 112 ++++++++++++++++++++++ src/core/receiver/satellite_visibility.h | 17 ++++ 3 files changed, 157 insertions(+), 2 deletions(-) diff --git a/src/core/receiver/gnss_flowgraph.cc b/src/core/receiver/gnss_flowgraph.cc index 3cd94d260..618e18acb 100644 --- a/src/core/receiver/gnss_flowgraph.cc +++ b/src/core/receiver/gnss_flowgraph.cc @@ -63,6 +63,7 @@ #include // for insert_iterator, inserter #include // for std::map #include // for std::shared_ptr +#include // for std::optional #include // for set #include // for std::stringstream #include // for invalid_argument @@ -1824,8 +1825,33 @@ void GNSSFlowgraph::acquisition_manager(unsigned int who) } else { - // set Doppler center to 0 Hz and search the full Doppler range - channels_[current_channel]->assist_acquisition_doppler(0, 0); + // No dual-frequency assist: fall back to a geometric Doppler + // prediction from ephemeris/almanac (SatelliteVisibility), if + // this is the primary signal of a satellite already classified + // visible and a live PVT fix currently exists. No-op (stays + // nullopt) unless visibility-aware search is enabled -- + // PredictedDopplerHz() itself requires a live fix for now; the + // AGNSS-reference (no-fix) case is a follow-up. + std::optional predicted_doppler_hz; + if (is_primary_freq && satellite_visibility_ && satellite_visibility_->enabled()) + { + const auto pvt_ptr = get_pvt(); + const Gnss_Satellite& sat = channels_[current_channel]->get_signal().get_satellite(); + if (pvt_ptr && satellite_visibility_->IsVisible(sat)) + { + const Monitor_Pvt fix_status = channels_status_->get_current_status_pvt(); + predicted_doppler_hz = satellite_visibility_->PredictedDopplerHz(pvt_ptr, fix_status, sat, channels_[current_channel]->get_signal().get_signal_str()); + } + } + if (predicted_doppler_hz.has_value()) + { + channels_[current_channel]->assist_acquisition_doppler(*predicted_doppler_hz, 1); + } + else + { + // set Doppler center to 0 Hz and search the full Doppler range + channels_[current_channel]->assist_acquisition_doppler(0, 0); + } } #if ENABLE_FPGA if (enable_fpga_offloading_) diff --git a/src/core/receiver/satellite_visibility.cc b/src/core/receiver/satellite_visibility.cc index a7a83e7ee..565d141eb 100644 --- a/src/core/receiver/satellite_visibility.cc +++ b/src/core/receiver/satellite_visibility.cc @@ -20,6 +20,7 @@ #include "agnss_ref_time.h" // for parse_agnss_ref_utc_time #include "configuration_interface.h" #include "geofunctions.h" +#include "gnss_frequencies.h" // for SIGNAL_FREQ_MAP #include "monitor_pvt.h" #include "pvt_interface.h" #include "qzss.h" @@ -1001,3 +1002,114 @@ bool SatelliteVisibility::IsSearchExcluded(const Gnss_Satellite& sat) const { return GetSearchVisibility(sat) == SearchVisibility::Excluded; } + + +std::optional SatelliteVisibility::PredictedDopplerHz(const std::shared_ptr& pvt_ptr, + const Monitor_Pvt& fix_status, const Gnss_Satellite& sat, const std::string& signal) const +{ + if (!enabled_ || !pvt_ptr || fix_status.RX_time < 0.0) + { + return std::nullopt; + } + + // Gnss_Ephemeris::predicted_doppler()/Gnss_Almanac::predicted_doppler()'s + // own band codes, keyed by signal the same way SIGNAL_FREQ_MAP is. + static const std::map kBandForSignal = { + {"1C", 1}, {"1B", 1}, {"1D", 1}, {"J1", 1}, {"B1", 1}, + {"2S", 2}, {"B2", 2}, + {"L5", 5}, {"5X", 5}, {"J5", 5}, + {"E6", 6}, + {"7X", 7}, + {"B3", 3}}; + const auto band_it = kBandForSignal.find(signal); + const auto freq_it = SIGNAL_FREQ_MAP.find(signal); + if (band_it == kBandForSignal.cend() || freq_it == SIGNAL_FREQ_MAP.cend()) + { + return std::nullopt; + } + const int band = band_it->second; + const double carrier_freq_hz = freq_it->second; + + const double lat_deg = fix_status.latitude; + const double lon_deg = fix_status.longitude; + const double h_m = fix_status.height; + const double ve_mps = fix_status.vel_e; + const double vn_mps = fix_status.vel_n; + const double vu_mps = fix_status.vel_u; + const gtime_t gps_gtime = gpst2time(static_cast(fix_status.week), fix_status.RX_time); + const double rx_time_s = time2gpst(gps_gtime, nullptr); + int ref_gps_week = 0; + time2gpst(gps_gtime, &ref_gps_week); + const std::string system = sat.get_system(); + const auto prn = static_cast(sat.get_PRN()); + + std::optional geometric_doppler_hz; + if (system == "GPS") + { + const auto eph_map = pvt_ptr->get_gps_ephemeris(); + const auto eph_it = eph_map.find(prn); + if (eph_it != eph_map.cend() && std::abs(timediff(gps_gtime, eph_to_rtklib(eph_it->second, ref_gps_week).toe)) <= MAXDTOE) + { + geometric_doppler_hz = eph_it->second.predicted_doppler(rx_time_s, lat_deg, lon_deg, h_m, ve_mps, vn_mps, vu_mps, band); + } + else + { + const auto alm_map = pvt_ptr->get_gps_almanac(); + const auto alm_it = alm_map.find(prn); + if (alm_it != alm_map.cend() && std::abs(timediff(gps_gtime, alm_to_rtklib(alm_it->second, ref_gps_week).toa)) <= almanac_max_age_s_) + { + geometric_doppler_hz = alm_it->second.predicted_doppler(rx_time_s, lat_deg, lon_deg, h_m, ve_mps, vn_mps, vu_mps, band); + } + } + } + else if (system == "Galileo") + { + const auto eph_map = pvt_ptr->get_galileo_ephemeris(); + const auto eph_it = eph_map.find(prn); + if (eph_it != eph_map.cend() && std::abs(timediff(gps_gtime, eph_to_rtklib(eph_it->second).toe)) <= MAXDTOE_GAL) + { + geometric_doppler_hz = eph_it->second.predicted_doppler(rx_time_s, lat_deg, lon_deg, h_m, ve_mps, vn_mps, vu_mps, band); + } + else + { + const auto alm_map = pvt_ptr->get_galileo_almanac(); + const auto alm_it = alm_map.find(prn); + if (alm_it != alm_map.cend() && std::abs(timediff(gps_gtime, alm_to_rtklib(alm_it->second, ref_gps_week).toa)) <= almanac_max_age_s_) + { + geometric_doppler_hz = alm_it->second.predicted_doppler(rx_time_s, lat_deg, lon_deg, h_m, ve_mps, vn_mps, vu_mps, band); + } + } + } + else if (system == "Beidou") + { + const auto eph_map = pvt_ptr->get_beidou_dnav_ephemeris(); + const auto eph_it = eph_map.find(prn); + if (eph_it != eph_map.cend() && std::abs(timediff(gps_gtime, eph_to_rtklib(eph_it->second).toe)) <= MAXDTOE_BDS) + { + geometric_doppler_hz = eph_it->second.predicted_doppler(rx_time_s, lat_deg, lon_deg, h_m, ve_mps, vn_mps, vu_mps, band); + } + else + { + const auto alm_map = pvt_ptr->get_beidou_dnav_almanac(); + const auto alm_it = alm_map.find(prn); + if (alm_it != alm_map.cend() && std::abs(timediff(gps_gtime, alm_to_rtklib(alm_it->second).toa)) <= almanac_max_age_s_) + { + geometric_doppler_hz = alm_it->second.predicted_doppler(rx_time_s, lat_deg, lon_deg, h_m, ve_mps, vn_mps, vu_mps, band); + } + } + } + + if (!geometric_doppler_hz.has_value()) + { + return std::nullopt; + } + + // Live PVT-solved clock drift, shared by every satellite's observed + // Doppler, on top of the per-satellite geometric term above. + // SUBTRACTED, not added -- empirically verified against + // project_doppler()'s dual-frequency projection (gnss_flowgraph.cc): + // geometric - clock_offset matched to within noise, geometric + + // clock_offset was off by 2x it. + const double clock_offset_hz = fix_status.user_clk_drift_ppm * 1.0e-6 * carrier_freq_hz; + return *geometric_doppler_hz - clock_offset_hz; +} diff --git a/src/core/receiver/satellite_visibility.h b/src/core/receiver/satellite_visibility.h index c51179a19..eacab1f54 100644 --- a/src/core/receiver/satellite_visibility.h +++ b/src/core/receiver/satellite_visibility.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -159,6 +160,22 @@ public: bool IsSearchVisible(const Gnss_Satellite& sat) const; bool IsSearchExcluded(const Gnss_Satellite& sat) const; + // Predicted Doppler (Hz) for sat on the given signal (e.g. "1B", "5X", + // "L5", "7X"), for use as an acquisition search center. Geometric term + // from whichever of sat's ephemeris/almanac is usable (same freshness + // rules as compute_visible_satellites()), plus the receiver's live + // solved clock drift (fix_status.user_clk_drift_ppm) scaled to this + // signal's carrier. Only meaningful with a live PVT fix -- std::nullopt + // when disabled, no fix is currently valid, the signal isn't a + // recognized carrier, or neither ephemeris nor almanac is currently + // usable for sat. No AGNSS-reference fallback yet: that's deferred to a + // follow-up (needs the pre-fix clock/velocity uncertainty this signal's + // Doppler tolerance can't absorb from a single search bin -- see + // GNSS-SDR.clock_frequency_max_error_ppm / receiver_max_velocity_m_s in + // the follow-up PR). + std::optional PredictedDopplerHz(const std::shared_ptr& pvt_ptr, + const Monitor_Pvt& fix_status, const Gnss_Satellite& sat, const std::string& signal) const; + private: enum class SearchVisibility { From 9345efac9e515745d494869c109058eacfa324f8 Mon Sep 17 00:00:00 2001 From: joebre Date: Tue, 15 Sep 2026 17:44:11 +0200 Subject: [PATCH 2/2] Re-trigger CI (unrelated flaky NtripRtcmClientTest localhost-socket test on macOS, not this PR's diff) Signed-off-by: joebre