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.
This commit is contained in:
joebre
2026-09-10 12:20:38 +02:00
committed by Carles Fernandez
parent 5f5152e936
commit 02a22a470e
2 changed files with 61 additions and 7 deletions
+45 -6
View File
@@ -1415,7 +1415,7 @@ std::map<int, Galileo_Ephemeris> Rtklib_Solver::get_galileo_ephemeris_map_for_pv
bool Rtklib_Solver::select_galileo_ephemeris(uint32_t prn, const std::string &signal, uint32_t observation_tow,
Galileo_Ephemeris &ephemeris, bool &from_reduced_ced) const
Galileo_Ephemeris &ephemeris, bool &from_reduced_ced)
{
from_reduced_ced = false;
if (!is_galileo_signal_used_in_pvt(signal) || observation_tow >= 604800U)
@@ -1423,12 +1423,49 @@ bool Rtklib_Solver::select_galileo_ephemeris(uint32_t prn, const std::string &si
return false;
}
// Prefer this satellite's ephemeris in the receiver's primary navigation
// service (see the constructor for how that's chosen). Once a satellite
// has successfully used it, it's locked onto that service (below) and
// this function stops trying the other service for it, so a promoted
// satellite's clock/orbit model never bounces back and forth between
// services for no reason -- each such bounce would be a small
// discontinuity even though both services are individually accurate.
// The lock is released the moment the primary-service ephemeris is
// found unusable, whether that's because it just expired for a
// previously-locked satellite or because it was never decoded in the
// first place -- either way, this (re)starts the same bootstrap
// fallback below, so an expired primary ephemeris never permanently
// excludes a satellite that still has a perfectly usable ephemeris from
// the other service. E.g. E1B's I/NAV ephemeris typically decodes well
// before E5a's F/NAV does, so an F/NAV-primary receiver would otherwise
// exclude a satellite from PVT for no reason while a perfectly usable
// I/NAV ephemeris sits unused (this matters most when E1B is Doppler-
// assisting E5a acquisition via GNSS-SDR.assist_dual_frequency_acq,
// since E5a reception can be weak enough that F/NAV never fully decodes
// at all).
const Galileo_Ephemeris *full_ephemeris = galileo_ephemeris_store.find(
static_cast<int>(prn), d_galileo_nav_message_type_for_pvt);
if (full_ephemeris != nullptr && galileo_ephemeris_is_usable(*full_ephemeris, observation_tow))
{
ephemeris = *full_ephemeris;
d_galileo_primary_nav_locked_prns_.insert(prn);
return true;
}
d_galileo_primary_nav_locked_prns_.erase(prn);
// Not locked to the primary service (either never was, or just got
// un-locked above) -- bootstrap with whichever other service IS
// currently available for this satellite instead.
const auto other_nav_message_type = (d_galileo_nav_message_type_for_pvt == Galileo_Nav_Message_Type::INAV)
? Galileo_Nav_Message_Type::FNAV
: Galileo_Nav_Message_Type::INAV;
full_ephemeris = galileo_ephemeris_store.find(static_cast<int>(prn), other_nav_message_type);
const auto compatibility_ephemeris = galileo_ephemeris_map.find(static_cast<int>(prn));
if (full_ephemeris == nullptr && compatibility_ephemeris != galileo_ephemeris_map.cend() &&
if ((full_ephemeris == nullptr || !galileo_ephemeris_is_usable(*full_ephemeris, observation_tow)) &&
compatibility_ephemeris != galileo_ephemeris_map.cend() &&
(compatibility_ephemeris->second.nav_message_type == Galileo_Nav_Message_Type::Unknown ||
compatibility_ephemeris->second.nav_message_type == d_galileo_nav_message_type_for_pvt))
compatibility_ephemeris->second.nav_message_type == d_galileo_nav_message_type_for_pvt ||
compatibility_ephemeris->second.nav_message_type == other_nav_message_type))
{
full_ephemeris = &compatibility_ephemeris->second;
}
@@ -1438,9 +1475,11 @@ bool Rtklib_Solver::select_galileo_ephemeris(uint32_t prn, const std::string &si
return true;
}
// The ICD only defines Reduced CED use for the E1/E5b service.
if (d_galileo_nav_message_type_for_pvt != Galileo_Nav_Message_Type::INAV ||
(signal != "1B" && signal != "7X"))
// The ICD only defines Reduced CED use for the E1/E5b service (I/NAV),
// as a startup bootstrap the same as the fallback above -- gate on the
// signal itself; reaching here already means the primary service isn't
// in use for this satellite this epoch (see above).
if (signal != "1B" && signal != "7X")
{
return false;
}
+16 -1
View File
@@ -81,6 +81,7 @@
#include <cstdint>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <utility>
@@ -163,11 +164,25 @@ public:
bool get_galileo_signal_health(uint32_t prn, const std::string& signal, uint32_t observation_tow, bool& healthy) const;
std::map<int, Galileo_Ephemeris> get_galileo_ephemeris_map_for_pvt() const;
bool select_galileo_ephemeris(uint32_t prn, const std::string& signal, uint32_t observation_tow,
Galileo_Ephemeris& ephemeris, bool& from_reduced_ced) const;
Galileo_Ephemeris& ephemeris, bool& from_reduced_ced);
sol_t pvt_sol{};
std::array<ssat_t, MAXSAT> pvt_ssat{};
// PRNs that have used the receiver's primary Galileo navigation service
// (see the constructor for how that's chosen) at least once. Once a PRN
// is in this set, select_galileo_ephemeris() never falls back to the
// other service for it again, even if the primary type's ephemeris is
// temporarily unusable -- the fallback exists purely to bootstrap
// Galileo availability at startup, before any given satellite's primary-
// service ephemeris has decoded yet, not as a general per-epoch
// failover. Without this, a satellite could bounce between services
// epoch to epoch (e.g. if the fallback type's ephemeris happens to sit
// right at its staleness cutoff), each bounce introducing a small
// clock/orbit discontinuity for that satellite even though both
// services are individually accurate.
std::set<uint32_t> d_galileo_primary_nav_locked_prns_;
Galileo_Ephemeris_Store galileo_ephemeris_store; //!< Source-aware Galileo ephemeris storage
std::map<int, Galileo_Ephemeris> galileo_ephemeris_map; //!< Compatibility PVT view; source-aware storage is authoritative
std::map<int, Galileo_Reduced_CED> galileo_reduced_ced_map; //!< Map storing provisional Galileo Reduced CED