Files
gnss-sdr/src/algorithms/PVT/libs/rtklib_solver.cc
T

3325 lines
174 KiB
C++

/*!
* \file rtklib_solver.cc
* \brief PVT solver based on rtklib library functions adapted to the GNSS-SDR
* data flow and structures
* \authors <ul>
* <li> 2017-2019, Javier Arribas
* <li> 2017-2023, Carles Fernandez
* <li> 2007-2013, T. Takasu
* </ul>
*
* This is a derived work from RTKLIB http://www.rtklib.com/
* The original source code at https://github.com/tomojitakasu/RTKLIB is
* released under the BSD 2-clause license with an additional exclusive clause
* that does not apply here. This additional clause is reproduced below:
*
* " The software package includes some companion executive binaries or shared
* libraries necessary to execute APs on Windows. These licenses succeed to the
* original ones of these software. "
*
* Neither the executive binaries nor the shared libraries are required by, used
* or included in GNSS-SDR.
*
* -----------------------------------------------------------------------------
* Copyright (C) 2007-2013, T. Takasu
* Copyright (C) 2017-2019, Javier Arribas
* Copyright (C) 2017-2023, Carles Fernandez
* All rights reserved.
*
* SPDX-License-Identifier: BSD-2-Clause
*
* -----------------------------------------------------------------------*/
#include "rtklib_solver.h"
#include "Beidou_CNAV1.h"
#include "Beidou_CNAV2.h"
#include "Beidou_DNAV.h"
#include "Galileo_CNAV.h"
#include "gnss_frequencies.h"
#include "gnss_obs_codes.h"
#include "gnss_sdr_filesystem.h"
#include "matlab_writter_helper.h"
#include "ntrip_rtcm_client.h"
#include "ntrip_rtk_signals.h"
#include "rtklib_rtkcmn.h"
#include "rtklib_rtkpos.h"
#include "signal_enabled_flags.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <exception>
#include <iostream>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>
#if USE_GLOG_AND_GFLAGS
#include <glog/logging.h>
#else
#include <absl/log/log.h>
#endif
/* out-of-line definition: pre-C++17 builds require it when the constant is
odr-used (e.g. streamed to a logger by const reference); in C++17 and
later this is a harmless redeclaration */
constexpr int Rtklib_Solver::NTRIP_MIN_COMMON_SATELLITES;
Rtklib_Solver::Rtklib_Solver(const rtk_t &rtk,
const Pvt_Conf &conf,
const std::string &dump_filename,
uint32_t signal_enabled_flags,
bool flag_dump_to_file,
bool flag_dump_to_mat) : d_dump_filename(dump_filename),
d_sbas_corrections(rtk.opt.sbassatsel),
d_conf(conf),
d_signal_enabled_flags(signal_enabled_flags),
d_flag_dump_enabled(flag_dump_to_file),
d_flag_dump_mat_enabled(flag_dump_to_mat)
{
// Solver instances need correction policy but never caster credentials.
// Remove secrets from this long-lived configuration copy immediately.
secure_clear_ntrip_credentials(&d_conf);
// see freq index at src/algorithms/libs/rtklib/rtklib_rtkcmn.cc
// function: satwavelen
d_rtklib_freq_index[0] = 0;
d_rtklib_freq_index[1] = 1;
d_rtklib_freq_index[2] = 2;
// BDS: B1I and B1C share slot 0 (prefer-B1C XOR). Override lam[0] for B1C (FREQ1).
d_rtklib_band_index["1G"] = 0;
d_rtklib_band_index["1C"] = 0;
d_rtklib_band_index["1B"] = 0;
d_rtklib_band_index["B1"] = 0;
d_rtklib_band_index["1D"] = 0;
d_rtklib_band_index["5D"] = 2;
d_rtklib_band_index["B3"] = 2;
d_rtklib_band_index["2G"] = 1;
d_rtklib_band_index["2S"] = 1;
d_rtklib_band_index["L5"] = 2;
d_rtklib_band_index["E6"] = 0;
d_rtklib_band_index["J1"] = 0;
d_rtklib_band_index["J5"] = 2;
const Signal_Enabled_Flags flags(d_signal_enabled_flags);
// Prefer the ICD clock model for E1/E5b (I/NAV) or E1/E5a (F/NAV).
// E5b takes priority; E1-only receivers use I/NAV.
if (flags.check_any_enabled(GAL_E5b))
{
d_galileo_nav_message_type_for_pvt = Galileo_Nav_Message_Type::INAV;
}
else if (flags.check_any_enabled(GAL_E5a))
{
d_galileo_nav_message_type_for_pvt = Galileo_Nav_Message_Type::FNAV;
}
// Only the E5 signal belonging to the selected Galileo navigation service
// is admitted to RTKLIB. Keeping the inactive signal out of this map makes
// the shared third observation slot an explicit invariant rather than an
// ordering-dependent overwrite.
if (d_galileo_nav_message_type_for_pvt == Galileo_Nav_Message_Type::INAV &&
flags.check_any_enabled(GAL_E5b))
{
d_rtklib_band_index["7X"] = 2;
}
else if (d_galileo_nav_message_type_for_pvt == Galileo_Nav_Message_Type::FNAV &&
flags.check_any_enabled(GAL_E5a))
{
d_rtklib_band_index["5X"] = 2;
}
const bool has_selected_galileo_e5 =
d_rtklib_band_index.find("5X") != d_rtklib_band_index.cend() ||
d_rtklib_band_index.find("7X") != d_rtklib_band_index.cend();
// RTKLIB's Galileo SPP/PPP paths use slots 0 and 2 for their primary
// dual-frequency pair. Put E6 in slot 2 when E1+E6 is that pair. If the
// automatically selected E5 signal already owns slot 2, keep E6 in the
// remaining slot so E1, E5, and E6 can coexist without overwriting one
// another. E6-only reception stays in the primary slot.
if (flags.check_any_enabled(GAL_E6))
{
if (has_selected_galileo_e5)
{
d_rtklib_band_index["E6"] = 1;
}
else if (flags.check_any_enabled(GAL_1B))
{
d_rtklib_band_index["E6"] = 2;
}
}
// GPS L5 needs the primary slot in this single-frequency GPS setup. The
// Galileo wavelength is corrected from its observation code below, so the
// two constellations do not need a shared nominal frequency index.
if (flags.check_only_enabled(GPS_L5, GAL_E5b))
{
d_rtklib_band_index["L5"] = 0;
d_rtklib_freq_index[0] = 2;
}
// Resolve BeiDou slots independently of the other constellations. B2a
// owns slot 2 in a frequency pair; B3I uses the remaining slot when a
// B1 signal is enabled, or the primary slot for B3I+B2a reception.
if (flags.check_any_enabled(BDS_B2A))
{
if (flags.check_any_enabled(BDS_B1, BDS_B1C))
{
d_rtklib_band_index["B3"] = 1;
}
else if (flags.check_any_enabled(BDS_B3))
{
d_rtklib_band_index["B3"] = 0;
}
else
{
d_rtklib_band_index["5D"] = 0;
}
}
// In automatic I/NAV mode E5a observations are not admitted to PVT, so
// Galileo-only multi-band configurations must use the E5b wavelength in
// the third RTKLIB slot. This also covers E1+E5a+E5b, which was not part
// of the legacy exact-flag special cases above.
if (d_galileo_nav_message_type_for_pvt == Galileo_Nav_Message_Type::INAV &&
flags.check_any_enabled(GAL_E5b) &&
!flags.check_any_enabled(GPS_L5, QZS_J5))
{
d_rtklib_freq_index[2] = 4;
}
if (d_conf.ntrip_client_enabled)
{
// the fixed-base path pairs rover and base observations by slot,
// so the signal table's slot column and this solver's finished
// band map must agree for every enabled NTRIP signal — fail
// loudly at construction instead of silently pairing across bands
for (const auto &signal : ntrip_rtk_signals())
{
if (!flags.check_any_enabled(signal.flag))
{
continue;
}
const auto mapped_slot = d_rtklib_band_index.find(signal.signal);
if (mapped_slot == d_rtklib_band_index.cend() ||
mapped_slot->second != signal.band_slot)
{
throw std::runtime_error(
std::string("NTRIP RTK signal table and solver band mapping disagree for signal ") +
signal.signal);
}
}
}
// resolve the per-system second-band slots once: the channel set is
// fixed for the solver's lifetime, and the per-epoch fixed-base path
// must not rescan the signal table per observation
d_ntrip_second_band_slot_gps = ntrip_rtk_second_band_slot(SYS_GPS, flags);
d_ntrip_second_band_slot_gal = ntrip_rtk_second_band_slot(SYS_GAL, flags);
d_ntrip_second_band_slot_bds = ntrip_rtk_second_band_slot(SYS_BDS, flags);
// auto empty_map = std::map < int, HAS_obs_corrections >> ();
// d_has_obs_corr_map["L1 C/A"] = empty_map;
// ############# ENABLE DATA FILE LOG #################
if (d_flag_dump_enabled == true)
{
if (d_dump_file.is_open() == false)
{
try
{
d_dump_file.exceptions(std::ofstream::failbit | std::ofstream::badbit);
d_dump_file.open(d_dump_filename.c_str(), std::ios::out | std::ios::binary);
LOG(INFO) << "PVT lib dump enabled Log file: " << d_dump_filename.c_str();
}
catch (const std::ofstream::failure &e)
{
LOG(WARNING) << "Exception opening RTKLIB dump file " << e.what();
}
}
}
// rtk_t owns dynamically allocated filter state. A struct copy would make
// the adapter, clock solver, and user solver alias the same state and would
// make independent RTK updates unsafe. Initialize it after all potentially
// throwing C++ setup so a failed constructor cannot leak the C allocation.
rtkinit(&d_rtk, &rtk.opt);
}
Rtklib_Solver::~Rtklib_Solver() noexcept
{
DLOG(INFO) << "Rtklib_Solver destructor called.";
if (d_dump_file.is_open() == true)
{
std::ofstream::pos_type pos = -1;
try
{
pos = d_dump_file.tellp();
d_dump_file.close();
}
catch (const std::exception &ex)
{
LOG(WARNING) << "Exception in destructor closing the RTKLIB dump file " << ex.what();
}
if (pos == 0)
{
try
{
errorlib::error_code ec;
if (!fs::remove(fs::path(d_dump_filename), ec))
{
std::cerr << "Problem removing temporary file " << d_dump_filename << '\n';
}
d_flag_dump_mat_enabled = false;
}
catch (const std::exception &ex)
{
LOG(WARNING) << "Exception in destructor removing the RTKLIB dump file " << ex.what();
}
}
}
if (d_flag_dump_mat_enabled)
{
try
{
save_matfile();
}
catch (const std::exception &ex)
{
LOG(WARNING) << "Exception in destructor saving the PVT .mat dump file " << ex.what();
}
}
rtkfree(&d_rtk);
}
bool Rtklib_Solver::save_matfile() const
{
// READ DUMP FILE
const std::string dump_filename = d_dump_filename;
const int32_t number_of_double_vars = 21;
const int32_t number_of_uint32_vars = 2;
const int32_t number_of_uint8_vars = 3;
const int32_t number_of_float_vars = 2;
const int32_t epoch_size_bytes = sizeof(double) * number_of_double_vars +
sizeof(uint32_t) * number_of_uint32_vars +
sizeof(uint8_t) * number_of_uint8_vars +
sizeof(float) * number_of_float_vars;
std::ifstream dump_file;
dump_file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
dump_file.open(dump_filename.c_str(), std::ios::binary | std::ios::ate);
}
catch (const std::ifstream::failure &e)
{
std::cerr << "Problem opening dump file:" << e.what() << '\n';
return false;
}
// count number of epochs and rewind
int64_t num_epoch = 0LL;
if (dump_file.is_open())
{
std::cout << "Generating .mat file for " << dump_filename << '\n';
const std::ifstream::pos_type size = dump_file.tellg();
num_epoch = static_cast<int64_t>(size) / static_cast<int64_t>(epoch_size_bytes);
dump_file.seekg(0, std::ios::beg);
}
else
{
return false;
}
auto TOW_at_current_symbol_ms = std::vector<uint32_t>(num_epoch);
auto week = std::vector<uint32_t>(num_epoch);
auto RX_time = std::vector<double>(num_epoch);
auto user_clk_offset = std::vector<double>(num_epoch);
auto pos_x = std::vector<double>(num_epoch);
auto pos_y = std::vector<double>(num_epoch);
auto pos_z = std::vector<double>(num_epoch);
auto vel_x = std::vector<double>(num_epoch);
auto vel_y = std::vector<double>(num_epoch);
auto vel_z = std::vector<double>(num_epoch);
auto cov_xx = std::vector<double>(num_epoch);
auto cov_yy = std::vector<double>(num_epoch);
auto cov_zz = std::vector<double>(num_epoch);
auto cov_xy = std::vector<double>(num_epoch);
auto cov_yz = std::vector<double>(num_epoch);
auto cov_zx = std::vector<double>(num_epoch);
auto latitude = std::vector<double>(num_epoch);
auto longitude = std::vector<double>(num_epoch);
auto height = std::vector<double>(num_epoch);
auto valid_sats = std::vector<uint8_t>(num_epoch);
auto solution_status = std::vector<uint8_t>(num_epoch);
auto solution_type = std::vector<uint8_t>(num_epoch);
auto AR_ratio_factor = std::vector<float>(num_epoch);
auto AR_ratio_threshold = std::vector<float>(num_epoch);
auto gdop = std::vector<double>(num_epoch);
auto pdop = std::vector<double>(num_epoch);
auto hdop = std::vector<double>(num_epoch);
auto vdop = std::vector<double>(num_epoch);
try
{
if (dump_file.is_open())
{
for (int64_t i = 0; i < num_epoch; i++)
{
dump_file.read(reinterpret_cast<char *>(&TOW_at_current_symbol_ms[i]), sizeof(uint32_t));
dump_file.read(reinterpret_cast<char *>(&week[i]), sizeof(uint32_t));
dump_file.read(reinterpret_cast<char *>(&RX_time[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&user_clk_offset[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&pos_x[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&pos_y[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&pos_z[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&vel_x[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&vel_y[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&vel_z[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&cov_xx[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&cov_yy[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&cov_zz[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&cov_xy[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&cov_yz[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&cov_zx[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&latitude[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&longitude[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&height[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&valid_sats[i]), sizeof(uint8_t));
dump_file.read(reinterpret_cast<char *>(&solution_status[i]), sizeof(uint8_t));
dump_file.read(reinterpret_cast<char *>(&solution_type[i]), sizeof(uint8_t));
dump_file.read(reinterpret_cast<char *>(&AR_ratio_factor[i]), sizeof(float));
dump_file.read(reinterpret_cast<char *>(&AR_ratio_threshold[i]), sizeof(float));
dump_file.read(reinterpret_cast<char *>(&gdop[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&pdop[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&hdop[i]), sizeof(double));
dump_file.read(reinterpret_cast<char *>(&vdop[i]), sizeof(double));
}
}
dump_file.close();
}
catch (const std::ifstream::failure &e)
{
std::cerr << "Problem reading dump file:" << e.what() << '\n';
return false;
}
// WRITE MAT FILE
std::string filename = dump_filename;
filename.erase(filename.length() - 4, 4);
filename.append(".mat");
mat_t *matfp = Mat_CreateVer(filename.c_str(), nullptr, MAT_FT_MAT73);
if (reinterpret_cast<int64_t *>(matfp) != nullptr)
{
std::array<size_t, 2> dims{1, static_cast<size_t>(num_epoch)};
write_matlab_var<2>("TOW_at_current_symbol_ms", TOW_at_current_symbol_ms.data(), matfp, dims);
write_matlab_var<2>("week", week.data(), matfp, dims);
write_matlab_var<2>("RX_time", RX_time.data(), matfp, dims);
write_matlab_var<2>("user_clk_offset", user_clk_offset.data(), matfp, dims);
write_matlab_var<2>("pos_x", pos_x.data(), matfp, dims);
write_matlab_var<2>("pos_y", pos_y.data(), matfp, dims);
write_matlab_var<2>("pos_z", pos_z.data(), matfp, dims);
write_matlab_var<2>("vel_x", vel_x.data(), matfp, dims);
write_matlab_var<2>("vel_y", vel_y.data(), matfp, dims);
write_matlab_var<2>("vel_z", vel_z.data(), matfp, dims);
write_matlab_var<2>("cov_xx", cov_xx.data(), matfp, dims);
write_matlab_var<2>("cov_yy", cov_yy.data(), matfp, dims);
write_matlab_var<2>("cov_zz", cov_zz.data(), matfp, dims);
write_matlab_var<2>("cov_xy", cov_xy.data(), matfp, dims);
write_matlab_var<2>("cov_yz", cov_yz.data(), matfp, dims);
write_matlab_var<2>("cov_zx", cov_zx.data(), matfp, dims);
write_matlab_var<2>("latitude", latitude.data(), matfp, dims);
write_matlab_var<2>("longitude", longitude.data(), matfp, dims);
write_matlab_var<2>("height", height.data(), matfp, dims);
write_matlab_var<2>("valid_sats", valid_sats.data(), matfp, dims);
write_matlab_var<2>("solution_status", solution_status.data(), matfp, dims);
write_matlab_var<2>("solution_type", solution_type.data(), matfp, dims);
write_matlab_var<2>("AR_ratio_factor", AR_ratio_factor.data(), matfp, dims);
write_matlab_var<2>("AR_ratio_threshold", AR_ratio_threshold.data(), matfp, dims);
write_matlab_var<2>("gdop", gdop.data(), matfp, dims);
write_matlab_var<2>("pdop", pdop.data(), matfp, dims);
write_matlab_var<2>("hdop", hdop.data(), matfp, dims);
write_matlab_var<2>("vdop", vdop.data(), matfp, dims);
}
Mat_Close(matfp);
return true;
}
double Rtklib_Solver::get_gdop() const
{
return d_dop[0];
}
double Rtklib_Solver::get_pdop() const
{
return d_dop[1];
}
double Rtklib_Solver::get_hdop() const
{
return d_dop[2];
}
double Rtklib_Solver::get_vdop() const
{
return d_dop[3];
}
Monitor_Pvt Rtklib_Solver::get_monitor_pvt() const
{
return d_monitor_pvt;
}
Rtklib_Fixed_Base_Status Rtklib_Solver::get_fixed_base_status() const
{
return d_fixed_base_status;
}
double Rtklib_Solver::get_fixed_base_age_s() const
{
return d_fixed_base_age_s;
}
std::size_t Rtklib_Solver::get_fixed_base_common_satellites() const
{
return d_fixed_base_common_satellites;
}
void Rtklib_Solver::store_sbas_message(const Sbas_Raw_Message &message)
{
d_sbas_corrections.push(message);
}
void Rtklib_Solver::store_gps_ephemeris(const Gps_Ephemeris &ephemeris)
{
const auto current_ephemeris = gps_ephemeris_map.find(ephemeris.PRN);
if (current_ephemeris != gps_ephemeris_map.cend() &&
current_ephemeris->second.IODE_SF3 != ephemeris.IODE_SF3)
{
// SBAS long-term corrections identify the broadcast ephemeris by
// IODE. Retain the preceding issue across the handover so RTKLIB
// can keep applying a still-current MT24/25 correction.
gps_previous_ephemeris_map[ephemeris.PRN] = current_ephemeris->second;
}
gps_ephemeris_map[ephemeris.PRN] = ephemeris;
}
void Rtklib_Solver::clear_gps_ephemerides()
{
gps_ephemeris_map.clear();
gps_previous_ephemeris_map.clear();
}
void Rtklib_Solver::clear_has_corrections()
{
d_has_orbit_corrections_store_map.clear();
d_has_clock_corrections_store_map.clear();
d_has_satellite_do_not_use_until.clear();
d_has_code_bias_store_map.clear();
d_has_phase_bias_store_map.clear();
d_has_phase_discontinuity_indicator_store_map.clear();
d_has_phase_bias_discontinuity_map.clear();
d_has_obs_corr_map.clear();
}
void Rtklib_Solver::store_has_data(const Galileo_HAS_data &new_has_data)
{
if (new_has_data.week == GALILEO_HAS_INVALID_WEEK || new_has_data.tow >= GALILEO_HAS_SECONDS_PER_WEEK)
{
LOG(INFO) << "Ignoring Galileo HAS corrections without valid GST";
return;
}
// Compute time of application HAS SIS ICD, Issue 1.0, Section 7.7
const uint32_t tmt = new_has_data.get_time_of_message_s();
const std::string gps_str("GPS");
const std::string gal_str("Galileo");
if (new_has_data.header.orbit_correction_flag)
{
LOG(INFO) << "Received HAS orbit corrections";
// for each satellite in GPS ephemeris
for (const auto &gpseph : gps_ephemeris_map)
{
int prn = gpseph.second.PRN;
int32_t sis_iod = gpseph.second.IODE_SF3;
uint16_t gnss_iod = new_has_data.get_gnss_iod(gps_str, prn);
if (static_cast<int32_t>(gnss_iod) == sis_iod)
{
float radial_m = new_has_data.get_delta_radial_m(gps_str, prn);
float in_track_m = new_has_data.get_delta_in_track_m(gps_str, prn);
float cross_track_m = new_has_data.get_delta_cross_track_m(gps_str, prn);
if (Galileo_HAS_data::is_orbit_radial_unavailable(radial_m) ||
Galileo_HAS_data::is_orbit_along_cross_unavailable(in_track_m) ||
Galileo_HAS_data::is_orbit_along_cross_unavailable(cross_track_m))
{
d_has_orbit_corrections_store_map[gps_str].erase(prn);
d_has_clock_corrections_store_map[gps_str].erase(prn);
continue;
}
auto &orbit_correction = d_has_orbit_corrections_store_map[gps_str][prn];
orbit_correction.radial_m = radial_m;
orbit_correction.in_track_m = in_track_m;
orbit_correction.cross_track_m = cross_track_m;
orbit_correction.valid_until = tmt +
new_has_data.get_validity_interval_s(new_has_data.validity_interval_index_orbit_corrections);
orbit_correction.iod = gnss_iod;
orbit_correction.mask_id = new_has_data.header.mask_id;
orbit_correction.iod_set_id = new_has_data.header.iod_set_id;
}
}
// for each satellite in Galileo ephemeris
for (const auto &galeph : galileo_ephemeris_store.by_source(d_galileo_nav_message_type_for_pvt))
{
int prn = galeph.second.PRN;
int32_t sis_iod = galeph.second.IOD_ephemeris;
uint16_t gnss_iod = new_has_data.get_gnss_iod(gal_str, prn);
if (static_cast<int32_t>(gnss_iod) == sis_iod)
{
float radial_m = new_has_data.get_delta_radial_m(gal_str, prn);
float in_track_m = new_has_data.get_delta_in_track_m(gal_str, prn);
float cross_track_m = new_has_data.get_delta_cross_track_m(gal_str, prn);
if (Galileo_HAS_data::is_orbit_radial_unavailable(radial_m) ||
Galileo_HAS_data::is_orbit_along_cross_unavailable(in_track_m) ||
Galileo_HAS_data::is_orbit_along_cross_unavailable(cross_track_m))
{
d_has_orbit_corrections_store_map[gal_str].erase(prn);
d_has_clock_corrections_store_map[gal_str].erase(prn);
continue;
}
auto &orbit_correction = d_has_orbit_corrections_store_map[gal_str][prn];
orbit_correction.radial_m = radial_m;
orbit_correction.in_track_m = in_track_m;
orbit_correction.cross_track_m = cross_track_m;
orbit_correction.valid_until = tmt +
new_has_data.get_validity_interval_s(new_has_data.validity_interval_index_orbit_corrections);
orbit_correction.iod = gnss_iod;
orbit_correction.mask_id = new_has_data.header.mask_id;
orbit_correction.iod_set_id = new_has_data.header.iod_set_id;
}
}
}
if (new_has_data.header.clock_fullset_flag)
{
LOG(INFO) << "Received HAS clock fullset corrections";
const uint32_t valid_until = tmt +
new_has_data.get_validity_interval_s(new_has_data.validity_interval_index_clock_fullset_corrections);
const auto gps_prns = new_has_data.get_PRNs_in_mask(gps_str);
for (const auto &gpseph : gps_ephemeris_map)
{
int prn = gpseph.second.PRN;
if (std::find(gps_prns.cbegin(), gps_prns.cend(), prn) == gps_prns.cend())
{
continue;
}
int32_t sis_iod = gpseph.second.IODE_SF3;
auto it = d_has_orbit_corrections_store_map[gps_str].find(prn);
if (it != d_has_orbit_corrections_store_map[gps_str].end())
{
const uint16_t gnss_iod = new_has_data.get_gnss_iod(gps_str, prn);
if ((static_cast<int32_t>(gnss_iod) == sis_iod) &&
(it->second.iod == gnss_iod) &&
(it->second.mask_id == new_has_data.header.mask_id) &&
(it->second.iod_set_id == new_has_data.header.iod_set_id))
{
float clock_correction_mult_m = new_has_data.get_clock_correction_mult_m(gps_str, prn);
if (Galileo_HAS_data::is_clock_do_not_use(clock_correction_mult_m))
{
d_has_clock_corrections_store_map[gps_str].erase(prn);
d_has_satellite_do_not_use_until[gps_str][prn] = valid_until;
continue;
}
if (Galileo_HAS_data::is_clock_unavailable(clock_correction_mult_m))
{
d_has_clock_corrections_store_map[gps_str].erase(prn);
continue;
}
d_has_satellite_do_not_use_until[gps_str].erase(prn);
auto &clock_correction = d_has_clock_corrections_store_map[gps_str][prn];
clock_correction.clock_correction_m = clock_correction_mult_m;
clock_correction.valid_until = valid_until;
clock_correction.iod = gnss_iod;
clock_correction.mask_id = new_has_data.header.mask_id;
clock_correction.iod_set_id = new_has_data.header.iod_set_id;
}
}
}
// for each satellite in Galileo ephemeris
const auto gal_prns = new_has_data.get_PRNs_in_mask(gal_str);
for (const auto &galeph : galileo_ephemeris_store.by_source(d_galileo_nav_message_type_for_pvt))
{
int prn = galeph.second.PRN;
if (std::find(gal_prns.cbegin(), gal_prns.cend(), prn) == gal_prns.cend())
{
continue;
}
int32_t iod_sis = galeph.second.IOD_ephemeris;
auto it = d_has_orbit_corrections_store_map[gal_str].find(prn);
if (it != d_has_orbit_corrections_store_map[gal_str].end())
{
const uint16_t gnss_iod = new_has_data.get_gnss_iod(gal_str, prn);
if ((static_cast<int32_t>(gnss_iod) == iod_sis) &&
(it->second.iod == gnss_iod) &&
(it->second.mask_id == new_has_data.header.mask_id) &&
(it->second.iod_set_id == new_has_data.header.iod_set_id))
{
float clock_correction_mult_m = new_has_data.get_clock_correction_mult_m(gal_str, prn);
// std::cout << "Galileo Satellite " << prn
// << " clock correction=" << new_has_data.get_clock_correction_mult_m(gal_str, prn)
// << std::endl;
if (Galileo_HAS_data::is_clock_do_not_use(clock_correction_mult_m))
{
d_has_clock_corrections_store_map[gal_str].erase(prn);
d_has_satellite_do_not_use_until[gal_str][prn] = valid_until;
continue;
}
if (Galileo_HAS_data::is_clock_unavailable(clock_correction_mult_m))
{
d_has_clock_corrections_store_map[gal_str].erase(prn);
continue;
}
d_has_satellite_do_not_use_until[gal_str].erase(prn);
auto &clock_correction = d_has_clock_corrections_store_map[gal_str][prn];
clock_correction.clock_correction_m = clock_correction_mult_m;
clock_correction.valid_until = valid_until;
clock_correction.iod = gnss_iod;
clock_correction.mask_id = new_has_data.header.mask_id;
clock_correction.iod_set_id = new_has_data.header.iod_set_id;
}
}
}
}
if (new_has_data.header.clock_subset_flag)
{
LOG(INFO) << "Received HAS clock subset corrections";
const uint32_t valid_until = tmt +
new_has_data.get_validity_interval_s(new_has_data.validity_interval_index_clock_subset_corrections);
const auto systems = new_has_data.get_systems_string();
const auto gps_sys_it = std::find(systems.cbegin(), systems.cend(), gps_str);
if (gps_sys_it != systems.cend())
{
const auto gps_sys_index = static_cast<uint8_t>(std::distance(systems.cbegin(), gps_sys_it));
const auto gps_subset_prns = new_has_data.get_PRNs_in_submask(gps_sys_index);
for (const auto &gpseph : gps_ephemeris_map)
{
int prn = gpseph.second.PRN;
if (std::find(gps_subset_prns.cbegin(), gps_subset_prns.cend(), prn) == gps_subset_prns.cend())
{
continue;
}
int32_t sis_iod = gpseph.second.IODE_SF3;
auto it = d_has_orbit_corrections_store_map[gps_str].find(prn);
if (it != d_has_orbit_corrections_store_map[gps_str].end())
{
const uint16_t gnss_iod = new_has_data.get_gnss_iod(gps_str, prn);
if ((static_cast<int32_t>(gnss_iod) == sis_iod) &&
(it->second.iod == gnss_iod) &&
(it->second.mask_id == new_has_data.header.mask_id) &&
(it->second.iod_set_id == new_has_data.header.iod_set_id))
{
float clock_correction_mult_m = new_has_data.get_clock_subset_correction_mult_m(gps_str, prn);
if (Galileo_HAS_data::is_clock_do_not_use(clock_correction_mult_m))
{
d_has_clock_corrections_store_map[gps_str].erase(prn);
d_has_satellite_do_not_use_until[gps_str][prn] = valid_until;
continue;
}
if (Galileo_HAS_data::is_clock_unavailable(clock_correction_mult_m))
{
d_has_clock_corrections_store_map[gps_str].erase(prn);
continue;
}
d_has_satellite_do_not_use_until[gps_str].erase(prn);
auto &clock_correction = d_has_clock_corrections_store_map[gps_str][prn];
clock_correction.clock_correction_m = clock_correction_mult_m;
clock_correction.valid_until = valid_until;
clock_correction.iod = gnss_iod;
clock_correction.mask_id = new_has_data.header.mask_id;
clock_correction.iod_set_id = new_has_data.header.iod_set_id;
}
}
}
}
const auto gal_sys_it = std::find(systems.cbegin(), systems.cend(), gal_str);
if (gal_sys_it != systems.cend())
{
const auto gal_sys_index = static_cast<uint8_t>(std::distance(systems.cbegin(), gal_sys_it));
const auto gal_subset_prns = new_has_data.get_PRNs_in_submask(gal_sys_index);
for (const auto &galeph : galileo_ephemeris_store.by_source(d_galileo_nav_message_type_for_pvt))
{
int prn = galeph.second.PRN;
if (std::find(gal_subset_prns.cbegin(), gal_subset_prns.cend(), prn) == gal_subset_prns.cend())
{
continue;
}
int32_t iod_sis = galeph.second.IOD_ephemeris;
auto it = d_has_orbit_corrections_store_map[gal_str].find(prn);
if (it != d_has_orbit_corrections_store_map[gal_str].end())
{
const uint16_t gnss_iod = new_has_data.get_gnss_iod(gal_str, prn);
if ((static_cast<int32_t>(gnss_iod) == iod_sis) &&
(it->second.iod == gnss_iod) &&
(it->second.mask_id == new_has_data.header.mask_id) &&
(it->second.iod_set_id == new_has_data.header.iod_set_id))
{
float clock_correction_mult_m = new_has_data.get_clock_subset_correction_mult_m(gal_str, prn);
if (Galileo_HAS_data::is_clock_do_not_use(clock_correction_mult_m))
{
d_has_clock_corrections_store_map[gal_str].erase(prn);
d_has_satellite_do_not_use_until[gal_str][prn] = valid_until;
continue;
}
if (Galileo_HAS_data::is_clock_unavailable(clock_correction_mult_m))
{
d_has_clock_corrections_store_map[gal_str].erase(prn);
continue;
}
d_has_satellite_do_not_use_until[gal_str].erase(prn);
auto &clock_correction = d_has_clock_corrections_store_map[gal_str][prn];
clock_correction.clock_correction_m = clock_correction_mult_m;
clock_correction.valid_until = valid_until;
clock_correction.iod = gnss_iod;
clock_correction.mask_id = new_has_data.header.mask_id;
clock_correction.iod_set_id = new_has_data.header.iod_set_id;
}
}
}
}
}
if (new_has_data.header.code_bias_flag)
{
LOG(INFO) << "Received HAS code bias corrections";
uint32_t valid_until = tmt +
new_has_data.get_validity_interval_s(new_has_data.validity_interval_index_code_bias_corrections);
auto signals_gal = new_has_data.get_signals_in_mask(gal_str);
for (const auto &it : signals_gal)
{
auto prns = new_has_data.get_PRNs_in_mask(gal_str);
for (auto prn : prns)
{
float code_bias_m = new_has_data.get_code_bias_m(it, prn);
if (Galileo_HAS_data::is_code_bias_unavailable(code_bias_m))
{
d_has_code_bias_store_map[it].erase(prn);
continue;
}
auto &code_bias_correction = d_has_code_bias_store_map[it][prn];
code_bias_correction.bias = code_bias_m;
code_bias_correction.valid_until = valid_until;
code_bias_correction.mask_id = new_has_data.header.mask_id;
code_bias_correction.iod_set_id = new_has_data.header.iod_set_id;
}
}
auto signals_gps = new_has_data.get_signals_in_mask(gps_str);
for (const auto &it : signals_gps)
{
auto prns = new_has_data.get_PRNs_in_mask(gps_str);
for (auto prn : prns)
{
float code_bias_m = new_has_data.get_code_bias_m(it, prn);
if (Galileo_HAS_data::is_code_bias_unavailable(code_bias_m))
{
d_has_code_bias_store_map[it].erase(prn);
continue;
}
auto &code_bias_correction = d_has_code_bias_store_map[it][prn];
code_bias_correction.bias = code_bias_m;
code_bias_correction.valid_until = valid_until;
code_bias_correction.mask_id = new_has_data.header.mask_id;
code_bias_correction.iod_set_id = new_has_data.header.iod_set_id;
}
}
}
if (new_has_data.header.phase_bias_flag)
{
LOG(INFO) << "Received HAS phase bias corrections";
uint32_t valid_until = tmt +
new_has_data.get_validity_interval_s(new_has_data.validity_interval_index_phase_bias_corrections);
auto signals_gal = new_has_data.get_signals_in_mask(gal_str);
for (const auto &it : signals_gal)
{
auto prns = new_has_data.get_PRNs_in_mask(gal_str);
for (auto prn : prns)
{
float phase_bias_correction_cycles = new_has_data.get_phase_bias_cycle(it, prn);
if (Galileo_HAS_data::is_phase_bias_unavailable(phase_bias_correction_cycles))
{
d_has_phase_bias_store_map[it].erase(prn);
d_has_phase_discontinuity_indicator_store_map[it].erase(prn);
d_has_phase_bias_discontinuity_map[it].erase(prn);
continue;
}
auto &phase_bias_correction = d_has_phase_bias_store_map[it][prn];
phase_bias_correction.bias = phase_bias_correction_cycles;
phase_bias_correction.valid_until = valid_until;
phase_bias_correction.mask_id = new_has_data.header.mask_id;
phase_bias_correction.iod_set_id = new_has_data.header.iod_set_id;
uint8_t phase_discontinuity_indicator = new_has_data.get_phase_discontinuity_indicator(it, prn);
auto stored_pdi_it = d_has_phase_discontinuity_indicator_store_map[it].find(prn);
if ((stored_pdi_it != d_has_phase_discontinuity_indicator_store_map[it].end()) &&
(stored_pdi_it->second != phase_discontinuity_indicator))
{
d_has_phase_bias_discontinuity_map[it][prn] = true;
}
else if (d_has_phase_bias_discontinuity_map[it].find(prn) == d_has_phase_bias_discontinuity_map[it].end())
{
d_has_phase_bias_discontinuity_map[it][prn] = false;
}
d_has_phase_discontinuity_indicator_store_map[it][prn] = phase_discontinuity_indicator;
}
}
auto signals_gps = new_has_data.get_signals_in_mask(gps_str);
for (const auto &it : signals_gps)
{
auto prns = new_has_data.get_PRNs_in_mask(gps_str);
for (auto prn : prns)
{
float phase_bias_correction_cycles = new_has_data.get_phase_bias_cycle(it, prn);
if (Galileo_HAS_data::is_phase_bias_unavailable(phase_bias_correction_cycles))
{
d_has_phase_bias_store_map[it].erase(prn);
d_has_phase_discontinuity_indicator_store_map[it].erase(prn);
d_has_phase_bias_discontinuity_map[it].erase(prn);
continue;
}
auto &phase_bias_correction = d_has_phase_bias_store_map[it][prn];
phase_bias_correction.bias = phase_bias_correction_cycles;
phase_bias_correction.valid_until = valid_until;
phase_bias_correction.mask_id = new_has_data.header.mask_id;
phase_bias_correction.iod_set_id = new_has_data.header.iod_set_id;
uint8_t phase_discontinuity_indicator = new_has_data.get_phase_discontinuity_indicator(it, prn);
auto stored_pdi_it = d_has_phase_discontinuity_indicator_store_map[it].find(prn);
if ((stored_pdi_it != d_has_phase_discontinuity_indicator_store_map[it].end()) &&
(stored_pdi_it->second != phase_discontinuity_indicator))
{
d_has_phase_bias_discontinuity_map[it][prn] = true;
}
else if (d_has_phase_bias_discontinuity_map[it].find(prn) == d_has_phase_bias_discontinuity_map[it].end())
{
d_has_phase_bias_discontinuity_map[it][prn] = false;
}
d_has_phase_discontinuity_indicator_store_map[it][prn] = phase_discontinuity_indicator;
}
}
}
}
void Rtklib_Solver::update_has_corrections(const std::map<int, Gnss_Synchro> &obs_map)
{
this->check_has_orbit_clock_validity(obs_map);
this->get_has_biases(obs_map);
}
bool Rtklib_Solver::has_active_has_do_not_use(const std::string &system, int prn, uint32_t tow_obs) const
{
const auto it_sys = d_has_satellite_do_not_use_until.find(system);
if (it_sys == d_has_satellite_do_not_use_until.cend())
{
return false;
}
const auto it_prn = it_sys->second.find(prn);
if (it_prn == it_sys->second.cend())
{
return false;
}
return Galileo_HAS_data::has_validity_interval_at_tow(it_prn->second, tow_obs);
}
bool Rtklib_Solver::get_active_has_context(const std::string &system, int prn, uint16_t sis_iod, uint32_t tow_obs, uint8_t &mask_id, uint8_t &iod_set_id) const
{
const auto orbit_sys_it = d_has_orbit_corrections_store_map.find(system);
if (orbit_sys_it == d_has_orbit_corrections_store_map.cend())
{
return false;
}
const auto orbit_prn_it = orbit_sys_it->second.find(prn);
if (orbit_prn_it == orbit_sys_it->second.cend())
{
return false;
}
if (!Galileo_HAS_data::has_validity_interval_at_tow(orbit_prn_it->second.valid_until, tow_obs))
{
return false;
}
if (orbit_prn_it->second.iod != sis_iod)
{
return false;
}
const auto clock_sys_it = d_has_clock_corrections_store_map.find(system);
if (clock_sys_it == d_has_clock_corrections_store_map.cend())
{
return false;
}
const auto clock_prn_it = clock_sys_it->second.find(prn);
if (clock_prn_it == clock_sys_it->second.cend())
{
return false;
}
if (!Galileo_HAS_data::has_validity_interval_at_tow(clock_prn_it->second.valid_until, tow_obs))
{
return false;
}
if (clock_prn_it->second.iod != sis_iod)
{
return false;
}
if ((orbit_prn_it->second.mask_id != clock_prn_it->second.mask_id) ||
(orbit_prn_it->second.iod_set_id != clock_prn_it->second.iod_set_id))
{
return false;
}
mask_id = orbit_prn_it->second.mask_id;
iod_set_id = orbit_prn_it->second.iod_set_id;
return true;
}
bool Rtklib_Solver::has_active_has_orbit_clock(const std::string &system, int prn, uint16_t sis_iod, uint32_t tow_obs) const
{
uint8_t mask_id = 0;
uint8_t iod_set_id = 0;
return get_active_has_context(system, prn, sis_iod, tow_obs, mask_id, iod_set_id);
}
void Rtklib_Solver::check_has_orbit_clock_validity(const std::map<int, Gnss_Synchro> &obs_map)
{
for (const auto &it : obs_map)
{
uint32_t obs_tow = it.second.interp_TOW_ms / 1000.0;
auto prn = static_cast<int>(it.second.PRN);
std::string system;
bool have_sis_iod = false;
uint16_t sis_iod = 0;
if (it.second.System == 'G')
{
system = "GPS";
auto eph_it = gps_ephemeris_map.find(it.second.PRN);
if (eph_it != gps_ephemeris_map.end())
{
sis_iod = static_cast<uint16_t>(eph_it->second.IODE_SF3);
have_sis_iod = true;
}
}
else if (it.second.System == 'E')
{
system = "Galileo";
const auto &galileo_ephemeris_map = galileo_ephemeris_store.by_source(d_galileo_nav_message_type_for_pvt);
auto eph_it = galileo_ephemeris_map.find(it.second.PRN);
if (eph_it != galileo_ephemeris_map.end())
{
sis_iod = static_cast<uint16_t>(eph_it->second.IOD_ephemeris);
have_sis_iod = true;
}
}
else
{
continue;
}
auto orbit_sys_it = d_has_orbit_corrections_store_map.find(system);
auto clock_sys_it = d_has_clock_corrections_store_map.find(system);
bool erase_orbit = false;
bool erase_clock = false;
if (orbit_sys_it != d_has_orbit_corrections_store_map.end())
{
auto orbit_it = orbit_sys_it->second.find(prn);
if (orbit_it != orbit_sys_it->second.end())
{
if (!Galileo_HAS_data::has_validity_interval_at_tow(orbit_it->second.valid_until, obs_tow) ||
(have_sis_iod && orbit_it->second.iod != sis_iod))
{
erase_orbit = true;
}
}
}
if (clock_sys_it != d_has_clock_corrections_store_map.end())
{
auto clock_it = clock_sys_it->second.find(prn);
if (clock_it != clock_sys_it->second.end())
{
if (!Galileo_HAS_data::has_validity_interval_at_tow(clock_it->second.valid_until, obs_tow) ||
(have_sis_iod && clock_it->second.iod != sis_iod))
{
erase_clock = true;
}
}
}
if (have_sis_iod && !erase_orbit && !erase_clock &&
orbit_sys_it != d_has_orbit_corrections_store_map.end() &&
clock_sys_it != d_has_clock_corrections_store_map.end())
{
auto orbit_it = orbit_sys_it->second.find(prn);
auto clock_it = clock_sys_it->second.find(prn);
if (orbit_it != orbit_sys_it->second.end() &&
clock_it != clock_sys_it->second.end() &&
((orbit_it->second.mask_id != clock_it->second.mask_id) ||
(orbit_it->second.iod_set_id != clock_it->second.iod_set_id)))
{
erase_clock = true;
}
}
if (erase_orbit && orbit_sys_it != d_has_orbit_corrections_store_map.end())
{
orbit_sys_it->second.erase(prn);
erase_clock = true;
}
if (erase_clock && clock_sys_it != d_has_clock_corrections_store_map.end())
{
clock_sys_it->second.erase(prn);
}
auto dnu_sys_it = d_has_satellite_do_not_use_until.find(system);
if (dnu_sys_it != d_has_satellite_do_not_use_until.end())
{
auto dnu_it = dnu_sys_it->second.find(prn);
if (dnu_it != dnu_sys_it->second.end())
{
if (!Galileo_HAS_data::has_validity_interval_at_tow(dnu_it->second, obs_tow))
{
dnu_sys_it->second.erase(prn);
}
}
}
}
}
void Rtklib_Solver::get_has_biases(const std::map<int, Gnss_Synchro> &obs_map)
{
d_has_obs_corr_map.clear();
if (!d_has_clock_corrections_store_map.empty() && !d_has_orbit_corrections_store_map.empty())
{
const std::vector<std::string> e1b_signals = {"E1-B I/NAV OS", "E1-C", "E1-B + E1-C"};
const std::vector<std::string> e6_signals = {"E6-B C/NAV HAS", "E6-C", "E6-B + E6-C"};
const std::vector<std::string> e5_signals = {"E5a-I F/NAV OS", "E5a-Q", "E5a-I+E5a-Q"};
const std::vector<std::string> e7_signals = {"E5b-I I/NAV OS", "E5b-Q", "E5b-I+E5b-Q"};
const std::vector<std::string> g1c_signals = {"L1 C/A"};
const std::vector<std::string> g2s_signals = {"L2 CM", "L2 CL", "L2 CM+CL", "L2 P"};
const std::vector<std::string> g5_signals = {"L5 I", "L5 Q", "L5 I + L5 Q"};
for (const auto &it : obs_map)
{
uint32_t obs_tow = it.second.interp_TOW_ms / 1000.0;
int prn = static_cast<int>(it.second.PRN);
std::string sig(it.second.Signal, 2);
if (it.second.System == 'E')
{
const auto &galileo_ephemeris_map = galileo_ephemeris_store.by_source(d_galileo_nav_message_type_for_pvt);
auto eph_it = galileo_ephemeris_map.find(it.second.PRN);
uint8_t mask_id = 0;
uint8_t iod_set_id = 0;
if (eph_it != galileo_ephemeris_map.end() &&
get_active_has_context("Galileo", prn, static_cast<uint16_t>(eph_it->second.IOD_ephemeris), obs_tow, mask_id, iod_set_id))
{
if (sig == "1B")
{
for (const auto &has_signal : e1b_signals)
{
this->get_current_has_obs_correction(has_signal, obs_tow, prn, mask_id, iod_set_id);
}
}
else if (sig == "E6")
{
for (const auto &has_signal : e6_signals)
{
this->get_current_has_obs_correction(has_signal, obs_tow, prn, mask_id, iod_set_id);
}
}
else if (sig == "5X")
{
for (const auto &has_signal : e5_signals)
{
this->get_current_has_obs_correction(has_signal, obs_tow, prn, mask_id, iod_set_id);
}
}
else if (sig == "7X")
{
for (const auto &has_signal : e7_signals)
{
this->get_current_has_obs_correction(has_signal, obs_tow, prn, mask_id, iod_set_id);
}
}
}
}
if (it.second.System == 'G')
{
auto eph_it = gps_ephemeris_map.find(it.second.PRN);
uint8_t mask_id = 0;
uint8_t iod_set_id = 0;
if (eph_it != gps_ephemeris_map.end() &&
get_active_has_context("GPS", prn, static_cast<uint16_t>(eph_it->second.IODE_SF3), obs_tow, mask_id, iod_set_id))
{
if (sig == "1C")
{
for (const auto &has_signal : g1c_signals)
{
this->get_current_has_obs_correction(has_signal, obs_tow, prn, mask_id, iod_set_id);
}
}
else if (sig == "2S")
{
for (const auto &has_signal : g2s_signals)
{
this->get_current_has_obs_correction(has_signal, obs_tow, prn, mask_id, iod_set_id);
}
}
else if (sig == "L5")
{
for (const auto &has_signal : g5_signals)
{
this->get_current_has_obs_correction(has_signal, obs_tow, prn, mask_id, iod_set_id);
}
}
}
}
}
}
}
void Rtklib_Solver::get_current_has_obs_correction(const std::string &signal, uint32_t tow_obs, int prn, uint8_t mask_id, uint8_t iod_set_id)
{
auto code_bias_signal_it = this->d_has_code_bias_store_map.find(signal);
if (code_bias_signal_it != this->d_has_code_bias_store_map.end())
{
auto code_bias_pair_it = code_bias_signal_it->second.find(prn);
if (code_bias_pair_it != code_bias_signal_it->second.end())
{
const auto &code_bias_correction = code_bias_pair_it->second;
if (Galileo_HAS_data::has_validity_interval_at_tow(code_bias_correction.valid_until, tow_obs) &&
(code_bias_correction.mask_id == mask_id) &&
(code_bias_correction.iod_set_id == iod_set_id))
{
auto &has_obs_correction = this->d_has_obs_corr_map[signal][prn];
has_obs_correction.signal = signal;
has_obs_correction.code_bias_m = code_bias_correction.bias;
}
}
}
auto phase_bias_signal_it = this->d_has_phase_bias_store_map.find(signal);
if (phase_bias_signal_it != this->d_has_phase_bias_store_map.end())
{
auto phase_bias_pair_it = phase_bias_signal_it->second.find(prn);
if (phase_bias_pair_it != phase_bias_signal_it->second.end())
{
const auto &phase_bias_correction = phase_bias_pair_it->second;
if (Galileo_HAS_data::has_validity_interval_at_tow(phase_bias_correction.valid_until, tow_obs) &&
(phase_bias_correction.mask_id == mask_id) &&
(phase_bias_correction.iod_set_id == iod_set_id))
{
auto &has_obs_correction = this->d_has_obs_corr_map[signal][prn];
has_obs_correction.signal = signal;
has_obs_correction.phase_bias_cycle = phase_bias_correction.bias;
const auto pdi_signal_it = this->d_has_phase_discontinuity_indicator_store_map.find(signal);
if (pdi_signal_it != this->d_has_phase_discontinuity_indicator_store_map.end())
{
const auto pdi_it = pdi_signal_it->second.find(prn);
if (pdi_it != pdi_signal_it->second.end())
{
has_obs_correction.phase_discontinuity_indicator = pdi_it->second;
}
}
auto discontinuity_signal_it = this->d_has_phase_bias_discontinuity_map.find(signal);
if (discontinuity_signal_it != this->d_has_phase_bias_discontinuity_map.end())
{
auto discontinuity_it = discontinuity_signal_it->second.find(prn);
if (discontinuity_it != discontinuity_signal_it->second.end())
{
has_obs_correction.phase_bias_discontinuity = discontinuity_it->second;
}
}
}
}
}
}
bool Rtklib_Solver::galileo_ephemeris_is_usable(const Galileo_Ephemeris &ephemeris, uint32_t observation_tow) const
{
if (observation_tow >= 604800U)
{
return false;
}
double toe_distance = std::fabs(static_cast<double>(observation_tow) - ephemeris.toe);
if (toe_distance > 302400.0)
{
toe_distance = 604800.0 - toe_distance;
}
return toe_distance <= MAXDTOE_GAL;
}
void Rtklib_Solver::update_galileo_observation_wavelengths(const obsd_t &observation)
{
if (satsys(observation.sat, nullptr) != SYS_GAL)
{
return;
}
for (int slot = 0; slot < NFREQ; ++slot)
{
if (observation.code[slot] == CODE_NONE)
{
continue;
}
int frequency = 0;
code2obs(observation.code[slot], &frequency);
if (frequency <= 0)
{
continue;
}
const double wavelength = satwavelen(observation.sat, frequency - 1, &d_nav_data);
if (wavelength > 0.0)
{
d_nav_data.lam[observation.sat - 1][slot] = wavelength;
}
}
}
void Rtklib_Solver::clear_applied_has_phase_bias_discontinuity(const HAS_obs_corrections *has_correction, int prn)
{
if (has_correction == nullptr || !has_correction->phase_bias_discontinuity || has_correction->signal.empty())
{
return;
}
auto discontinuity_signal_it = this->d_has_phase_bias_discontinuity_map.find(has_correction->signal);
if (discontinuity_signal_it == this->d_has_phase_bias_discontinuity_map.end())
{
return;
}
auto discontinuity_it = discontinuity_signal_it->second.find(prn);
if (discontinuity_it != discontinuity_signal_it->second.end())
{
discontinuity_it->second = false;
}
}
bool Rtklib_Solver::store_galileo_ephemeris(const Galileo_Ephemeris &ephemeris)
{
Galileo_Ephemeris source_ephemeris = ephemeris;
if (source_ephemeris.nav_message_type == Galileo_Nav_Message_Type::Unknown)
{
source_ephemeris.nav_message_type = d_galileo_nav_message_type_for_pvt;
LOG(WARNING) << "Galileo ephemeris for PRN " << source_ephemeris.PRN
<< " has no navigation-message provenance; assigning the receiver's automatic PVT source";
}
if (!galileo_ephemeris_store.insert(source_ephemeris))
{
return false;
}
const auto compatibility_entry = galileo_ephemeris_map.find(static_cast<int>(source_ephemeris.PRN));
if (source_ephemeris.nav_message_type == d_galileo_nav_message_type_for_pvt ||
compatibility_entry == galileo_ephemeris_map.cend())
{
galileo_ephemeris_map[static_cast<int>(source_ephemeris.PRN)] = source_ephemeris;
}
return true;
}
Galileo_Nav_Message_Type Rtklib_Solver::galileo_nav_message_type_for_pvt() const
{
return d_galileo_nav_message_type_for_pvt;
}
bool Rtklib_Solver::is_galileo_signal_used_in_pvt(const std::string &signal) const
{
if (signal == "E6")
{
return d_conf.use_e6_for_pvt;
}
if (d_galileo_nav_message_type_for_pvt == Galileo_Nav_Message_Type::FNAV)
{
return signal == "1B" ||
(signal == "5X" && d_rtklib_band_index.find(signal) != d_rtklib_band_index.cend());
}
return signal == "1B" ||
(signal == "7X" && d_rtklib_band_index.find(signal) != d_rtklib_band_index.cend());
}
bool Rtklib_Solver::get_galileo_signal_health(uint32_t prn, const std::string &signal,
uint32_t observation_tow, bool &healthy) const
{
Galileo_Nav_Message_Type health_source = Galileo_Nav_Message_Type::Unknown;
if (signal == "5X")
{
health_source = Galileo_Nav_Message_Type::FNAV;
}
else if (signal == "1B" || signal == "7X")
{
health_source = Galileo_Nav_Message_Type::INAV;
}
else
{
return false;
}
const Galileo_Ephemeris *health_ephemeris = galileo_ephemeris_store.find(static_cast<int>(prn), health_source);
if (health_ephemeris == nullptr || !galileo_ephemeris_is_usable(*health_ephemeris, observation_tow))
{
return false;
}
if (signal == "1B")
{
healthy = !health_ephemeris->E1B_DVS && health_ephemeris->E1B_HS == 0;
}
else if (signal == "5X")
{
healthy = !health_ephemeris->E5a_DVS && health_ephemeris->E5a_HS == 0;
}
else
{
healthy = !health_ephemeris->E5b_DVS && health_ephemeris->E5b_HS == 0;
}
return true;
}
bool Rtklib_Solver::get_broadcast_signal_health(char system, uint32_t prn, const std::string &signal,
uint32_t observation_tow, bool &healthy) const
{
const auto prn_key = static_cast<int>(prn);
switch (system)
{
case 'G':
case 'J':
{
// L2C and L5 health comes from the CNAV message that carries them:
// bits 52-54 of message type 10 are the L1, L2 and L5 signal health
// (0 = OK, IS-GPS-200 30.3.3.1.1.2).
if (signal == "2S" || signal == "L5" || signal == "J5")
{
const auto cnav_it = gps_cnav_ephemeris_map.find(prn_key);
if (cnav_it != gps_cnav_ephemeris_map.cend())
{
const int32_t signal_bit = (signal == "2S") ? 0x2 : 0x1;
healthy = ((cnav_it->second.signal_health & signal_bit) == 0);
return true;
}
}
// L1 C/A: per-satellite SV health from LNAV subframe 1, falling back
// to the almanac (broadcast by every satellite) when no ephemeris has
// been decoded for this satellite yet.
const auto eph_it = gps_ephemeris_map.find(prn_key);
if (eph_it != gps_ephemeris_map.cend())
{
healthy = (eph_it->second.SV_health == 0);
return true;
}
const auto alm_it = gps_almanac_map.find(prn_key);
if (alm_it != gps_almanac_map.cend())
{
healthy = (alm_it->second.SV_health == 0);
return true;
}
return false;
}
case 'E':
{
if (get_galileo_signal_health(prn, signal, observation_tow, healthy))
{
return true;
}
// No ephemeris decoded for this signal's service yet -- fall
// back to the almanac, broadcast by every satellite.
const auto alm_it = galileo_almanac_map.find(prn_key);
if (alm_it != galileo_almanac_map.cend())
{
healthy = (alm_it->second.E1B_HS == 0);
return true;
}
return false;
}
case 'R':
{
// Same predicate as the ephemeris handed to RTKLIB (see
// eph_to_rtklib()), so a GLONASS satellite excluded from the solve
// for health reasons is reported with used = false, healthy = false.
const auto eph_it = glonass_gnav_ephemeris_map.find(prn_key);
if (eph_it != glonass_gnav_ephemeris_map.cend())
{
healthy = glonass_gnav_is_healthy(eph_it->second, d_conf.glonass_strict_health);
return true;
}
return false;
}
case 'C':
{
if (signal == "1D")
{
// B1C: B-CNAV1 subframe 3 health status (HS, 0 = healthy). The
// latest page data is preferred over the ephemeris record, as
// when the ephemeris is handed to RTKLIB.
const auto page_it = beidou_cnav1_page_data_map.find(prn_key);
if (page_it != beidou_cnav1_page_data_map.cend())
{
healthy = (page_it->second.common.hs == 0);
return true;
}
const auto cnav1_it = beidou_cnav1_ephemeris_map.find(prn_key);
if (cnav1_it != beidou_cnav1_ephemeris_map.cend())
{
healthy = (cnav1_it->second.hs == 0);
return true;
}
return false;
}
if (signal == "5D")
{
// B2a: B-CNAV2 health status (HS, 0 = healthy)
const auto cnav2_it = beidou_cnav2_ephemeris_map.find(prn_key);
if (cnav2_it != beidou_cnav2_ephemeris_map.cend())
{
healthy = (cnav2_it->second.hs == 0);
return true;
}
return false;
}
// B1I / B3I: DNAV satellite health (SatH1, 0 = healthy)
const auto dnav_it = beidou_dnav_ephemeris_map.find(prn_key);
if (dnav_it != beidou_dnav_ephemeris_map.cend())
{
healthy = (dnav_it->second.SV_health == 0);
return true;
}
return false;
}
default:
return false;
}
}
std::map<int, Galileo_Ephemeris> Rtklib_Solver::get_galileo_ephemeris_map_for_pvt() const
{
auto result = galileo_ephemeris_store.combined_view(d_galileo_nav_message_type_for_pvt);
for (const auto &ephemeris : galileo_ephemeris_map)
{
if (result.find(ephemeris.first) == result.cend())
{
result.insert(ephemeris);
}
}
return result;
}
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
{
from_reduced_ced = false;
if (!is_galileo_signal_used_in_pvt(signal) || observation_tow >= 604800U)
{
return false;
}
// Prefer the primary service while its ephemeris is fresh.
const Galileo_Ephemeris *full_ephemeris = galileo_ephemeris_store.find(
static_cast<int>(prn), d_galileo_nav_message_type_for_pvt);
const auto compatibility_ephemeris = galileo_ephemeris_map.find(static_cast<int>(prn));
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))
{
full_ephemeris = &compatibility_ephemeris->second;
}
if (full_ephemeris != nullptr && galileo_ephemeris_is_usable(*full_ephemeris, observation_tow))
{
ephemeris = *full_ephemeris;
if (ephemeris.nav_message_type == Galileo_Nav_Message_Type::Unknown)
{
ephemeris.nav_message_type = d_galileo_nav_message_type_for_pvt;
}
return true;
}
// E1 can use either clock model with its matching BGD. Other signals
// must retain the primary service's clock reference.
if (signal == "1B")
{
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);
if ((full_ephemeris == nullptr || !galileo_ephemeris_is_usable(*full_ephemeris, observation_tow)) &&
compatibility_ephemeris != galileo_ephemeris_map.cend() &&
compatibility_ephemeris->second.nav_message_type == other_nav_message_type)
{
full_ephemeris = &compatibility_ephemeris->second;
}
if (full_ephemeris != nullptr && galileo_ephemeris_is_usable(*full_ephemeris, observation_tow))
{
ephemeris = *full_ephemeris;
return true;
}
}
// The ICD defines Reduced CED only for E1/E5b.
if (signal != "1B" && signal != "7X")
{
return false;
}
const auto reduced_ced = galileo_reduced_ced_map.find(static_cast<int>(prn));
if (reduced_ced == galileo_reduced_ced_map.cend())
{
return false;
}
uint32_t observation_week = reduced_ced->second.WN;
uint32_t week_reference_tow = reduced_ced->second.TOTRedCED;
// A later GST model prevents a stale Reduced CED from becoming valid
// again after a week. Older assistance data must not override the directly
// decoded Reduced CED epoch.
if (galileo_iono.WN > 0 &&
static_cast<uint32_t>(galileo_iono.WN) > reduced_ced->second.WN &&
galileo_iono.tow >= 0 && galileo_iono.tow < 604800)
{
observation_week = static_cast<uint32_t>(galileo_iono.WN);
week_reference_tow = static_cast<uint32_t>(galileo_iono.tow);
}
if (observation_tow < week_reference_tow &&
week_reference_tow - observation_tow > 302400U)
{
if (observation_week == std::numeric_limits<uint32_t>::max())
{
return false;
}
++observation_week;
}
else if (observation_tow > week_reference_tow &&
observation_tow - week_reference_tow > 302400U)
{
if (observation_week == 0U)
{
return false;
}
--observation_week;
}
if (!reduced_ced->second.is_valid_at(observation_week, observation_tow))
{
return false;
}
ephemeris = reduced_ced->second.compute_eph();
from_reduced_ced = true;
return true;
}
void Rtklib_Solver::reset_relative_filter()
{
const prcopt_t options = d_rtk.opt;
rtkfree(&d_rtk);
rtkinit(&d_rtk, &options);
if (d_conf.enable_pvt_kf)
{
d_pvt_kf.reset_Kf();
}
}
void Rtklib_Solver::update_beidou_observation_wavelengths(const obsd_t &observation)
{
if (satsys(observation.sat, nullptr) != SYS_BDS)
{
return;
}
for (int band = 0; band < NFREQ; ++band)
{
const unsigned char code = observation.code[band];
double frequency = 0.0;
if (is_bds_b1c_code(code))
{
frequency = FREQ1;
}
else if (is_bds_b2a_code(code))
{
frequency = FREQ5;
}
else if (code == CODE_L6I || code == CODE_L6Q)
{
frequency = FREQ3_BDS;
}
else if (code == CODE_L2I || code == CODE_L1I)
{
frequency = FREQ1_BDS;
}
if (frequency > 0.0)
{
d_nav_data.lam[observation.sat - 1][band] = SPEED_OF_LIGHT_M_S / frequency;
}
}
}
int Rtklib_Solver::merge_duplicated_rover_observations(int rover_observation_count)
{
/* Two channels can deliver the same satellite on the same signal (e.g. a
GLONASS false lock labelled with a slot another channel already tracks).
rescode() only rejects duplicates that are adjacent, and an inconsistent
duplicate makes every solution fail validation. Keep, per frequency
slot, the entry with the highest C/N0; preserve the order of the rest. */
int out = 0;
bool merged_any = false;
for (int in = 0; in < rover_observation_count; ++in)
{
const obsd_t &source = d_obs_data[in];
int match = -1;
for (int k = 0; k < out && source.sat > 0; ++k)
{
if (d_obs_data[k].sat == source.sat)
{
match = k;
break;
}
}
if (match < 0)
{
if (out != in)
{
d_obs_data[out] = source;
}
++out;
continue;
}
obsd_t &destination = d_obs_data[match];
bool overlapping_signal = false;
for (int frequency = 0; frequency < NFREQ + NEXOBS; ++frequency)
{
const bool source_has_data = source.code[frequency] != CODE_NONE &&
(source.P[frequency] != 0.0 || source.L[frequency] != 0.0);
if (!source_has_data)
{
continue;
}
const bool destination_has_data = destination.code[frequency] != CODE_NONE &&
(destination.P[frequency] != 0.0 || destination.L[frequency] != 0.0);
overlapping_signal = overlapping_signal || destination_has_data;
if (destination_has_data && destination.SNR[frequency] >= source.SNR[frequency])
{
continue;
}
destination.P[frequency] = source.P[frequency];
destination.L[frequency] = source.L[frequency];
destination.D[frequency] = source.D[frequency];
destination.SNR[frequency] = source.SNR[frequency];
destination.LLI[frequency] = source.LLI[frequency];
destination.code[frequency] = source.code[frequency];
}
merged_any = merged_any || overlapping_signal;
if (overlapping_signal && !d_duplicated_rover_observations_logged)
{
LOG(WARNING) << "Duplicated observation of satellite " << satno2id(source.sat)
<< " received from two channels: keeping the measurement with the highest C/N0";
d_duplicated_rover_observations_logged = true;
}
}
if (!merged_any)
{
d_duplicated_rover_observations_logged = false;
}
return out;
}
bool Rtklib_Solver::prepare_fixed_base_observations(const Ntrip_Rtcm_Snapshot &fixed_base,
int &rover_observation_count,
int &base_observation_count)
{
// Per-constellation band-to-slot mapping, shared with the adapter's
// channel-set validation (see ntrip_rtk_signals() in ntrip_rtk_signals.h)
// and resolved once at construction
const auto second_band_slot = [this](int system) {
switch (system)
{
case SYS_GAL:
return d_ntrip_second_band_slot_gal;
case SYS_BDS:
return d_ntrip_second_band_slot_bds;
default:
return d_ntrip_second_band_slot_gps;
}
};
const auto clear_unsupported_slots = [&second_band_slot](obsd_t &observation, int system) {
const int second = second_band_slot(system);
for (int frequency = 1; frequency < NFREQ + NEXOBS; ++frequency)
{
if (frequency == second)
{
continue;
}
observation.P[frequency] = 0.0;
observation.L[frequency] = 0.0;
observation.D[frequency] = 0.0F;
observation.SNR[frequency] = 0;
observation.LLI[frequency] = 0;
observation.code[frequency] = CODE_NONE;
}
};
const auto has_supported_band_measurement = [&second_band_slot](const obsd_t &observation, int system) {
const std::array<int, 2> slots = {0, second_band_slot(system)};
for (const int frequency : slots)
{
if (observation.code[frequency] != CODE_NONE &&
(observation.P[frequency] != 0.0 || observation.L[frequency] != 0.0))
{
return true;
}
}
return false;
};
const auto merge_observation = [](obsd_t &destination, const obsd_t &source) {
for (int frequency = 0; frequency < NFREQ + NEXOBS; ++frequency)
{
if (source.code[frequency] == CODE_NONE && source.P[frequency] == 0.0 && source.L[frequency] == 0.0)
{
continue;
}
destination.P[frequency] = source.P[frequency];
destination.L[frequency] = source.L[frequency];
destination.D[frequency] = source.D[frequency];
destination.SNR[frequency] = source.SNR[frequency];
destination.LLI[frequency] = source.LLI[frequency];
destination.code[frequency] = source.code[frequency];
}
};
const auto sort_and_merge = [&merge_observation](std::vector<obsd_t> &observations) {
std::sort(observations.begin(), observations.end(), [](const obsd_t &left, const obsd_t &right) {
return left.sat < right.sat;
});
/* in-place merge of same-satellite entries: no scratch allocation */
std::size_t out = 0;
for (std::size_t in = 0; in < observations.size(); ++in)
{
if (out > 0 && observations[out - 1].sat == observations[in].sat)
{
merge_observation(observations[out - 1], observations[in]);
}
else
{
if (out != in)
{
observations[out] = observations[in];
}
++out;
}
}
observations.resize(out);
};
/* member scratch vectors: their capacity persists across epochs, so this
per-epoch path stops allocating once warmed up */
std::vector<obsd_t> &rover_observations = d_fixed_base_rover_scratch;
rover_observations.clear();
rover_observations.reserve(static_cast<std::size_t>(rover_observation_count));
for (int index = 0; index < rover_observation_count; ++index)
{
obsd_t observation = d_obs_data[index];
/* the fixed-base path admits exactly the systems the whole chain
supports (signal table, adapter navigation-system mask, client
observation filter, and this merge): QZSS is deliberately not
among them yet — its rover observations would be excluded by
satexclude() anyway and could never find base counterparts */
const int system = observation.sat > 0 ? satsys(observation.sat, nullptr) : 0;
if (system != SYS_GPS && system != SYS_GAL && system != SYS_BDS)
{
continue;
}
observation.rcv = 1;
clear_unsupported_slots(observation, system);
if (has_supported_band_measurement(observation, system))
{
rover_observations.push_back(observation);
}
}
sort_and_merge(rover_observations);
if (rover_observations.size() > MAXOBS)
{
rover_observations.resize(MAXOBS);
}
/* d_obs_data and the counts are rewritten only when the base data is
actually applied (at the end of this function): every early return
below leaves the full multi-constellation rover set untouched so the
single-point fallback solves with the same satellites as a receiver
without NTRIP */
if (!fixed_base.has_observations || rover_observations.empty())
{
d_fixed_base_status = Rtklib_Fixed_Base_Status::MISSING_OBSERVATIONS;
return false;
}
if (!fixed_base.has_base_position)
{
d_fixed_base_status = Rtklib_Fixed_Base_Status::MISSING_POSITION;
return false;
}
d_fixed_base_age_s = timediff(rover_observations.front().time, fixed_base.observation_time);
if (!std::isfinite(d_fixed_base_age_s) || std::fabs(d_fixed_base_age_s) > d_conf.ntrip_max_correction_age_s)
{
d_fixed_base_status = Rtklib_Fixed_Base_Status::STALE;
return false;
}
double base_position_norm_squared = 0.0;
for (const double coordinate : fixed_base.base_position_ecef_m)
{
if (!std::isfinite(coordinate))
{
d_fixed_base_status = Rtklib_Fixed_Base_Status::INVALID_POSITION;
return false;
}
base_position_norm_squared += coordinate * coordinate;
}
if (base_position_norm_squared < 1.0e12 || base_position_norm_squared > 1.0e16)
{
d_fixed_base_status = Rtklib_Fixed_Base_Status::INVALID_POSITION;
return false;
}
std::vector<obsd_t> &base_observations = d_fixed_base_base_scratch;
base_observations.clear();
base_observations.reserve(fixed_base.observations.size());
for (auto observation : fixed_base.observations)
{
const int system = observation.sat > 0 ? satsys(observation.sat, nullptr) : 0;
if (system != SYS_GPS && system != SYS_GAL && system != SYS_BDS)
{
continue;
}
observation.rcv = 2;
clear_unsupported_slots(observation, system);
if (!has_supported_band_measurement(observation, system))
{
continue;
}
const auto rover = std::lower_bound(rover_observations.cbegin(), rover_observations.cend(), observation.sat,
[](const obsd_t &candidate, int satellite) { return candidate.sat < satellite; });
if (rover != rover_observations.cend() && rover->sat == observation.sat)
{
base_observations.push_back(observation);
}
}
sort_and_merge(base_observations);
if (base_observations.size() > MAXOBS)
{
base_observations.resize(MAXOBS);
}
d_fixed_base_common_satellites = base_observations.size();
if (d_fixed_base_common_satellites < NTRIP_MIN_COMMON_SATELLITES)
{
d_fixed_base_status = Rtklib_Fixed_Base_Status::INSUFFICIENT_COMMON_SATELLITES;
return false;
}
bool base_changed = d_fixed_base_initialized && d_fixed_base_station_id != fixed_base.station_id;
double position_delta_squared = 0.0;
for (std::size_t index = 0; index < d_fixed_base_position_ecef_m.size(); ++index)
{
const double delta = fixed_base.base_position_ecef_m[index] - d_fixed_base_position_ecef_m[index];
position_delta_squared += delta * delta;
}
base_changed = base_changed || (d_fixed_base_initialized && position_delta_squared > 1.0e-6);
const bool correction_gap = d_fixed_base_was_applied &&
d_fixed_base_last_observation_time.time != 0 &&
std::fabs(timediff(fixed_base.observation_time,
d_fixed_base_last_observation_time)) > d_conf.ntrip_max_correction_age_s;
if (base_changed || correction_gap)
{
reset_relative_filter();
}
d_fixed_base_initialized = true;
d_fixed_base_station_id = fixed_base.station_id;
d_fixed_base_position_ecef_m = fixed_base.base_position_ecef_m;
d_fixed_base_last_observation_time = fixed_base.observation_time;
for (std::size_t index = 0; index < d_fixed_base_position_ecef_m.size(); ++index)
{
d_rtk.opt.rb[index] = d_fixed_base_position_ecef_m[index];
}
/* slots beyond the entry rover count are already zero (get_PVT clears the
whole array at epoch start), so only the leftover entry-rover slots the
filtered set no longer covers need clearing */
const int previously_filled = rover_observation_count;
rover_observation_count = static_cast<int>(rover_observations.size());
std::copy(rover_observations.cbegin(), rover_observations.cend(), d_obs_data.begin());
std::copy(base_observations.cbegin(), base_observations.cend(), d_obs_data.begin() + rover_observation_count);
base_observation_count = static_cast<int>(base_observations.size());
const int used = rover_observation_count + base_observation_count;
if (used < previously_filled)
{
std::fill(d_obs_data.begin() + used, d_obs_data.begin() + previously_filled, obsd_t{});
}
d_fixed_base_status = Rtklib_Fixed_Base_Status::APPLIED;
return true;
}
/* Latest base-stream broadcast ephemeris for a satellite, or nullptr. The
records are RTKLIB-format already (RTCM MT1019/1044/1045/1046/1042), so a
satellite the rover has not finished decoding can still contribute. */
static const eph_t *find_base_ephemeris(const Ntrip_Rtcm_Snapshot *fixed_base, int sat)
{
if (fixed_base == nullptr)
{
return nullptr;
}
// the client keeps the vector sorted by satellite number
const auto candidate = std::lower_bound(
fixed_base->ephemerides.cbegin(), fixed_base->ephemerides.cend(), sat,
[](const eph_t &stored, int wanted) { return stored.sat < wanted; });
if (candidate != fixed_base->ephemerides.cend() && candidate->sat == sat)
{
return &(*candidate);
}
return nullptr;
}
bool Rtklib_Solver::substitute_base_ephemeris(const Ntrip_Rtcm_Snapshot *fixed_base,
int sat,
const Gnss_Synchro &gnss_synchro,
const std::string &band_key,
std::vector<eph_t> &eph_data,
int &valid_obs,
int glo_valid_obs)
{
const eph_t *base_ephemeris = find_base_ephemeris(fixed_base, sat);
if (base_ephemeris == nullptr)
{
return false;
}
// the substituted record's own reference time, expressed in the week
// convention the rover-decoded path uses for the system
const int system = satsys(sat, nullptr);
int week = 0;
switch (system)
{
case SYS_GAL:
time2gst(base_ephemeris->toe, &week);
break;
case SYS_BDS:
time2gpst(base_ephemeris->toe, &week);
break;
default:
// GPS/QZSS: RTCM MT1019/MT1044 carry the full GPS week
week = base_ephemeris->week;
break;
}
eph_data[valid_obs] = *base_ephemeris;
obsd_t newobs{};
if (system == SYS_BDS)
{
// HAS corrections cover GPS and Galileo only
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_synchro, week, d_rtklib_band_index.at(band_key));
}
else
{
// HAS observation corrections apply to the measurement no matter
// where the ephemeris came from: mirror the rover-decoded path
const HAS_obs_corrections *applied_has_correction = nullptr;
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_synchro,
d_has_obs_corr_map,
week,
d_rtklib_band_index.at(band_key),
&applied_has_correction,
system == SYS_GAL ? 0 : this->get_ref_gps_week());
clear_applied_has_phase_bias_discontinuity(applied_has_correction,
static_cast<int>(gnss_synchro.PRN));
}
valid_obs++;
return true;
}
bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &gnss_observables_map,
double kf_update_interval_s,
const SensorDataAggregator &sensor_data_aggregator,
bool dump_this_epoch,
const Ntrip_Rtcm_Snapshot *fixed_base)
{
std::map<int, Gnss_Synchro>::const_iterator gnss_observables_iter;
std::map<int, Gps_Ephemeris>::const_iterator gps_ephemeris_iter;
std::map<int, Gps_CNAV_Ephemeris>::const_iterator gps_cnav_ephemeris_iter;
std::map<int, Glonass_Gnav_Ephemeris>::const_iterator glonass_gnav_ephemeris_iter;
std::map<int, Beidou_Dnav_Ephemeris>::const_iterator beidou_ephemeris_iter;
const Glonass_Gnav_Utc_Model &gnav_utc = this->glonass_gnav_utc_model;
// ********************************************************************************
// ****** PREPARE THE DATA (SV EPHEMERIS AND OBSERVATIONS) ************************
// ********************************************************************************
int valid_obs = 0; // valid observations counter
int glo_valid_obs = 0; // GLONASS L1/L2 valid observations counter
d_fixed_base_status = fixed_base == nullptr ? Rtklib_Fixed_Base_Status::NOT_REQUESTED : Rtklib_Fixed_Base_Status::MISSING_OBSERVATIONS;
d_fixed_base_age_s = 0.0;
d_fixed_base_common_satellites = 0;
// Before merging, a multi-band satellite contributes several channel
// records and ephemerides. MAXOBS limits satellites, not input channels.
d_obs_data.assign(std::max(static_cast<size_t>(MAXOBS * 2), gnss_observables_map.size()), obsd_t{});
std::vector<eph_t> eph_data(std::max(static_cast<size_t>(MAXOBS), gnss_observables_map.size()));
std::vector<geph_t> geph_data(MAXOBS);
for (gnss_observables_iter = gnss_observables_map.cbegin();
gnss_observables_iter != gnss_observables_map.cend();
++gnss_observables_iter) // CHECK INCONSISTENCY when combining GLONASS + other system
{
switch (gnss_observables_iter->second.System)
{
case 'E':
{
const std::string gal_str("Galileo");
const std::string sig_(gnss_observables_iter->second.Signal, 2);
const uint32_t obs_tow = gnss_observables_iter->second.interp_TOW_ms / 1000.0;
const int prn = static_cast<int>(gnss_observables_iter->second.PRN);
Galileo_Ephemeris selected_galileo_ephemeris;
bool selected_from_reduced_ced = false;
const bool has_selected_galileo_ephemeris = select_galileo_ephemeris(
gnss_observables_iter->second.PRN, sig_, obs_tow,
selected_galileo_ephemeris, selected_from_reduced_ced);
// the do-not-use exclusion must hold whether the ephemeris
// comes from the rover or from the base stream: checking it
// only for a selected rover ephemeris would let the
// base-ephemeris substitution admit a HAS-flagged satellite
// (the GPS block below checks it unconditionally too)
if (!selected_from_reduced_ced && has_active_has_do_not_use(gal_str, prn, obs_tow))
{
break;
}
// Galileo E1
if (sig_ == "1B")
{
if (has_selected_galileo_ephemeris)
{
// convert ephemeris from GNSS-SDR class to RTKLIB structure
eph_data[valid_obs] = selected_from_reduced_ced ? eph_to_rtklib(selected_galileo_ephemeris) : eph_to_rtklib(selected_galileo_ephemeris, this->d_has_orbit_corrections_store_map[gal_str], this->d_has_clock_corrections_store_map[gal_str]);
// convert observation from GNSS-SDR class to RTKLIB structure
obsd_t newobs{};
const HAS_obs_corrections *applied_has_correction = nullptr;
if (selected_from_reduced_ced)
{
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
selected_galileo_ephemeris.WN,
d_rtklib_band_index.at(sig_));
}
else
{
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
d_has_obs_corr_map,
selected_galileo_ephemeris.WN,
d_rtklib_band_index.at(sig_),
&applied_has_correction,
false);
}
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
valid_obs++;
}
// the rover has not decoded this satellite's I/NAV yet: the
// base stream may have delivered its broadcast ephemeris over
// NTRIP (RTCM MT1045/MT1046), so the satellite contributes
// without waiting for the page decoding. E5a joins once the
// rover ephemeris is available.
else if (!substitute_base_ephemeris(fixed_base, satno(SYS_GAL, prn),
gnss_observables_iter->second, sig_,
eph_data, valid_obs, glo_valid_obs))
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
// Galileo E5
if ((sig_ == "5X") || (sig_ == "7X"))
{
if (has_selected_galileo_ephemeris)
{
bool found_E1_obs = false;
for (int i = 0; i < valid_obs; i++)
{
if (eph_data[i].sat == (static_cast<int>(gnss_observables_iter->second.PRN + NSATGPS + NSATGLO)))
{
const HAS_obs_corrections *applied_has_correction = nullptr;
if (selected_from_reduced_ced)
{
d_obs_data[i + glo_valid_obs] = insert_obs_to_rtklib(d_obs_data[i + glo_valid_obs],
gnss_observables_iter->second,
selected_galileo_ephemeris.WN,
d_rtklib_band_index.at(sig_));
}
else
{
d_obs_data[i + glo_valid_obs] = insert_obs_to_rtklib(d_obs_data[i + glo_valid_obs],
gnss_observables_iter->second,
d_has_obs_corr_map,
selected_galileo_ephemeris.WN,
d_rtklib_band_index.at(sig_),
&applied_has_correction,
false);
}
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
found_E1_obs = true;
break;
}
}
if (!found_E1_obs)
{
// insert Galileo E5 obs as new obs and also insert its ephemeris
// convert ephemeris from GNSS-SDR class to RTKLIB structure
eph_data[valid_obs] = selected_from_reduced_ced ? eph_to_rtklib(selected_galileo_ephemeris) : eph_to_rtklib(selected_galileo_ephemeris, this->d_has_orbit_corrections_store_map[gal_str], this->d_has_clock_corrections_store_map[gal_str]);
// convert observation from GNSS-SDR class to RTKLIB structure
const auto default_code_ = static_cast<unsigned char>(CODE_NONE);
obsd_t newobs = {{0, 0}, '0', '0', {}, {},
{default_code_, default_code_, default_code_},
{}, {0.0, 0.0, 0.0}, {}, {}, {}};
const HAS_obs_corrections *applied_has_correction = nullptr;
if (selected_from_reduced_ced)
{
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
selected_galileo_ephemeris.WN,
d_rtklib_band_index.at(sig_));
}
else
{
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
d_has_obs_corr_map,
selected_galileo_ephemeris.WN,
d_rtklib_band_index.at(sig_),
&applied_has_correction,
false);
}
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
valid_obs++;
}
}
else // the ephemeris are not available for this SV
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
if (sig_ == "E6" && d_conf.use_e6_for_pvt)
{
if (has_selected_galileo_ephemeris)
{
bool found_E1_obs = false;
for (int i = 0; i < valid_obs; i++)
{
if (eph_data[i].sat == (static_cast<int>(gnss_observables_iter->second.PRN + NSATGPS + NSATGLO)))
{
const HAS_obs_corrections *applied_has_correction = nullptr;
d_obs_data[i + glo_valid_obs] = insert_obs_to_rtklib(d_obs_data[i + glo_valid_obs],
gnss_observables_iter->second,
d_has_obs_corr_map,
selected_galileo_ephemeris.WN,
d_rtklib_band_index.at(sig_),
&applied_has_correction,
false);
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
found_E1_obs = true;
break;
}
}
if (!found_E1_obs)
{
// insert Galileo E6 obs as new obs and also insert its ephemeris
// convert ephemeris from GNSS-SDR class to RTKLIB structure
eph_data[valid_obs] = eph_to_rtklib(selected_galileo_ephemeris,
this->d_has_orbit_corrections_store_map[gal_str],
this->d_has_clock_corrections_store_map[gal_str]);
// convert observation from GNSS-SDR class to RTKLIB structure
const auto default_code_ = static_cast<unsigned char>(CODE_NONE);
obsd_t newobs = {{0, 0}, '0', '0', {}, {},
{default_code_, default_code_, default_code_},
{}, {0.0, 0.0, 0.0}, {}, {}, {}};
const HAS_obs_corrections *applied_has_correction = nullptr;
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
d_has_obs_corr_map,
selected_galileo_ephemeris.WN,
d_rtklib_band_index.at(sig_),
&applied_has_correction,
false);
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
valid_obs++;
}
}
else // the ephemeris are not available for this SV
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
break;
}
case 'G':
case 'J':
{
const bool is_qzss = (gnss_observables_iter->second.PRN >= 193 && gnss_observables_iter->second.PRN <= 206);
// GPS/QZSS L1
// find the ephemeris for the current SV observation. The SV PRN ID is the map key
const std::string gnss_str = is_qzss ? "QZSS" : "GPS";
const int sat_sys = is_qzss ? SYS_QZS : SYS_GPS;
const int sat = satno(sat_sys, gnss_observables_iter->second.PRN);
const int prn = static_cast<int>(gnss_observables_iter->second.PRN);
const std::string sig_(gnss_observables_iter->second.Signal, 2);
const uint32_t obs_tow = gnss_observables_iter->second.interp_TOW_ms / 1000.0;
if (!is_qzss && has_active_has_do_not_use(gnss_str, prn, obs_tow))
{
break;
}
const bool is_l1_ca = (sig_ == "1C") || (sig_ == "J1");
const bool is_l2 = (sig_ == "2S");
const bool is_l5 = (sig_ == "L5") || (sig_ == "J5");
const std::string rtklib_sig = is_qzss ? (is_l1_ca ? "J1" : (is_l5 ? "J5" : sig_)) : sig_;
if (is_l1_ca)
{
gps_ephemeris_iter = gps_ephemeris_map.find(gnss_observables_iter->second.PRN);
if (gps_ephemeris_iter != gps_ephemeris_map.cend())
{
// convert ephemeris from GNSS-SDR class to RTKLIB structure
eph_data[valid_obs] = eph_to_rtklib(gps_ephemeris_iter->second,
this->d_has_orbit_corrections_store_map[gnss_str],
this->d_has_clock_corrections_store_map[gnss_str],
this->get_ref_gps_week());
// convert observation from GNSS-SDR class to RTKLIB structure
obsd_t newobs{};
const HAS_obs_corrections *applied_has_correction = nullptr;
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
d_has_obs_corr_map,
gps_ephemeris_iter->second.WN,
d_rtklib_band_index.at(rtklib_sig),
&applied_has_correction,
this->get_ref_gps_week());
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
valid_obs++;
}
// the rover has not decoded this satellite's LNAV yet: the
// base stream may have delivered its broadcast ephemeris over
// NTRIP (RTCM MT1019, already in RTKLIB format), so the
// satellite contributes without waiting the ~30 s of subframe
// decoding
else if (!substitute_base_ephemeris(fixed_base, sat,
gnss_observables_iter->second, rtklib_sig,
eph_data, valid_obs, glo_valid_obs))
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->first;
}
}
if (is_l2 || is_l5)
{
gps_cnav_ephemeris_iter = gps_cnav_ephemeris_map.find(gnss_observables_iter->second.PRN);
if (gps_cnav_ephemeris_iter != gps_cnav_ephemeris_map.cend())
{
// 1. Find the same satellite in current GPS/QZSS observations (typically L1)
bool found_existing_obs = false;
for (int i = 0; i < valid_obs; i++)
{
if (eph_data[i].sat == sat)
{
// 2. If found, attach the L2/L5 observation to the existing observation in RTKLIB structure
// The existing entry carries the LNAV ephemeris, which has no
// inter-signal corrections: take the ISCs from CNAV so the
// dual-frequency correction in prange() can apply them.
eph_data[i].isc[0] = gps_cnav_ephemeris_iter->second.ISCL1;
eph_data[i].isc[1] = gps_cnav_ephemeris_iter->second.ISCL2;
eph_data[i].isc[2] = gps_cnav_ephemeris_iter->second.ISCL5I;
eph_data[i].isc[3] = gps_cnav_ephemeris_iter->second.ISCL5Q;
if (eph_data[i].apply_has_corrections)
{
const HAS_obs_corrections *applied_has_correction = nullptr;
d_obs_data[i + glo_valid_obs] = insert_obs_to_rtklib(d_obs_data[i + glo_valid_obs],
gnss_observables_iter->second,
d_has_obs_corr_map,
gps_cnav_ephemeris_iter->second.WN,
d_rtklib_band_index.at(rtklib_sig),
&applied_has_correction,
false);
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
}
else
{
d_obs_data[i + glo_valid_obs] = insert_obs_to_rtklib(d_obs_data[i + glo_valid_obs],
gnss_observables_iter->second,
gps_cnav_ephemeris_iter->second.WN,
d_rtklib_band_index.at(rtklib_sig));
}
found_existing_obs = true;
break;
}
}
if (!found_existing_obs)
{
// 3. If not found, insert the L2/L5 ephemeris and the observation
// convert ephemeris from GNSS-SDR class to RTKLIB structure
eph_data[valid_obs] = eph_to_rtklib(gps_cnav_ephemeris_iter->second);
// convert observation from GNSS-SDR class to RTKLIB structure
const auto default_code_ = static_cast<unsigned char>(CODE_NONE);
obsd_t newobs = {{0, 0}, '0', '0', {}, {},
{default_code_, default_code_, default_code_},
{}, {0.0, 0.0, 0.0}, {}, {}, {}};
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
gps_cnav_ephemeris_iter->second.WN,
d_rtklib_band_index.at(rtklib_sig));
valid_obs++;
}
}
else // the ephemeris are not available for this SV
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
break;
}
case 'R':
{
const std::string sig_(gnss_observables_iter->second.Signal);
// GLONASS GNAV L1
if (sig_ == "1G")
{
// 1 Glo - find the ephemeris for the current GLONASS SV observation. The SV Slot Number (PRN ID) is the map key
glonass_gnav_ephemeris_iter = glonass_gnav_ephemeris_map.find(gnss_observables_iter->second.PRN);
if (glonass_gnav_ephemeris_iter != glonass_gnav_ephemeris_map.cend())
{
// convert ephemeris from GNSS-SDR class to RTKLIB structure
geph_data[glo_valid_obs] = eph_to_rtklib(glonass_gnav_ephemeris_iter->second, gnav_utc, d_conf.glonass_strict_health);
// convert observation from GNSS-SDR class to RTKLIB structure
obsd_t newobs{};
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
glonass_gnav_ephemeris_iter->second.d_WN,
d_rtklib_band_index.at(sig_));
glo_valid_obs++;
}
else // the ephemeris are not available for this SV
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
// GLONASS GNAV L2
if (sig_ == "2G")
{
// 1 GLONASS - find the ephemeris for the current GLONASS SV observation. The SV PRN ID is the map key
glonass_gnav_ephemeris_iter = glonass_gnav_ephemeris_map.find(gnss_observables_iter->second.PRN);
if (glonass_gnav_ephemeris_iter != glonass_gnav_ephemeris_map.cend())
{
bool found_L1_obs = false;
for (int i = 0; i < glo_valid_obs; i++)
{
if (geph_data[i].sat == (static_cast<int>(gnss_observables_iter->second.PRN + NSATGPS)))
{
d_obs_data[i + valid_obs] = insert_obs_to_rtklib(d_obs_data[i + valid_obs],
gnss_observables_iter->second,
glonass_gnav_ephemeris_iter->second.d_WN,
d_rtklib_band_index.at(sig_));
found_L1_obs = true;
break;
}
}
if (!found_L1_obs)
{
// insert GLONASS GNAV L2 obs as new obs and also insert its ephemeris
// convert ephemeris from GNSS-SDR class to RTKLIB structure
geph_data[glo_valid_obs] = eph_to_rtklib(glonass_gnav_ephemeris_iter->second, gnav_utc, d_conf.glonass_strict_health);
// convert observation from GNSS-SDR class to RTKLIB structure
obsd_t newobs{};
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
glonass_gnav_ephemeris_iter->second.d_WN,
d_rtklib_band_index.at(sig_));
glo_valid_obs++;
}
}
else // the ephemeris are not available for this SV
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
break;
}
case 'C':
{
// BeiDou B1I / B1C / B3I (prefer B1C when CNAV1 + B1C obs are both present)
const std::string sig_(gnss_observables_iter->second.Signal);
const int bds_sat = static_cast<int>(gnss_observables_iter->second.PRN + NSATGPS + NSATGLO + NSATGAL + NSATQZS);
if (sig_ == "B1")
{
bool prefer_b1c = false;
for (const auto &obs_pair : gnss_observables_map)
{
if (obs_pair.second.System == 'C' &&
obs_pair.second.PRN == gnss_observables_iter->second.PRN &&
std::string(obs_pair.second.Signal, 2) == "1D" &&
beidou_cnav1_ephemeris_map.find(obs_pair.second.PRN) != beidou_cnav1_ephemeris_map.cend())
{
prefer_b1c = true;
break;
}
}
if (prefer_b1c)
{
DLOG(INFO) << "Skip B1I for PRN " << gnss_observables_iter->second.PRN
<< " (B1C present; prefer CNAV1)";
break;
}
beidou_ephemeris_iter = beidou_dnav_ephemeris_map.find(gnss_observables_iter->second.PRN);
if (beidou_ephemeris_iter != beidou_dnav_ephemeris_map.cend())
{
eph_data[valid_obs] = eph_to_rtklib(beidou_ephemeris_iter->second);
obsd_t newobs{};
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
beidou_ephemeris_iter->second.WN + BEIDOU_DNAV_BDT2GPST_WEEK_NUM_OFFSET,
d_rtklib_band_index.at(sig_));
valid_obs++;
}
else
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->first;
}
}
if (sig_ == "1D")
{
const auto cnav1_iter = beidou_cnav1_ephemeris_map.find(gnss_observables_iter->second.PRN);
if (cnav1_iter != beidou_cnav1_ephemeris_map.cend())
{
eph_data[valid_obs] = eph_to_rtklib(cnav1_iter->second);
const auto page_it = beidou_cnav1_page_data_map.find(cnav1_iter->second.PRN);
if (page_it != beidou_cnav1_page_data_map.cend())
{
eph_data[valid_obs].svh = page_it->second.common.hs;
}
obsd_t newobs{};
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
cnav1_iter->second.WN + BEIDOU_DNAV_BDT2GPST_WEEK_NUM_OFFSET,
d_rtklib_band_index.at(sig_));
valid_obs++;
}
// Base-stream broadcast ephemeris substitution (RTCM MT1042):
// use its DNAV orbit/clock while the rover B-CNAV1 decode
// completes. B1C TGD/ISC selection remains CNAV1-only.
else if (!substitute_base_ephemeris(fixed_base, bds_sat,
gnss_observables_iter->second, sig_,
eph_data, valid_obs, glo_valid_obs))
{
DLOG(INFO) << "No B-CNAV1 ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
if (sig_ == "5D")
{
const auto cnav2_iter = beidou_cnav2_ephemeris_map.find(gnss_observables_iter->second.PRN);
if (cnav2_iter != beidou_cnav2_ephemeris_map.cend() &&
cnav2_iter->second.sig_type == BDS_EPH_SOURCE_CNAV2 &&
cnav2_iter->second.sat_type != 1 &&
cnav2_iter->second.hs == 0)
{
eph_data[valid_obs] = eph_to_rtklib(cnav2_iter->second);
obsd_t newobs{};
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
cnav2_iter->second.WN + BEIDOU_DNAV_BDT2GPST_WEEK_NUM_OFFSET,
d_rtklib_band_index.at(sig_));
valid_obs++;
}
else
{
DLOG(INFO) << "No B-CNAV2 ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
// BeiDou B3: merge with DNAV/B1I only
if (sig_ == "B3")
{
beidou_ephemeris_iter = beidou_dnav_ephemeris_map.find(gnss_observables_iter->second.PRN);
if (beidou_ephemeris_iter != beidou_dnav_ephemeris_map.cend())
{
bool found_B1I_obs = false;
for (int i = 0; i < valid_obs; i++)
{
if (eph_data[i].sat == bds_sat && eph_data[i].code != BDS_EPH_SOURCE_CNAV1)
{
const unsigned char c0 = d_obs_data[i + glo_valid_obs].code[0];
if (c0 == CODE_L2I || c0 == CODE_L1I)
{
d_obs_data[i + glo_valid_obs] = insert_obs_to_rtklib(d_obs_data[i + glo_valid_obs],
gnss_observables_iter->second,
beidou_ephemeris_iter->second.WN + BEIDOU_DNAV_BDT2GPST_WEEK_NUM_OFFSET,
d_rtklib_band_index.at(sig_));
found_B1I_obs = true;
break;
}
}
}
if (!found_B1I_obs)
{
eph_data[valid_obs] = eph_to_rtklib(beidou_ephemeris_iter->second);
const auto default_code_ = static_cast<unsigned char>(CODE_NONE);
obsd_t newobs = {{0, 0}, '0', '0', {}, {},
{default_code_, default_code_, default_code_},
{}, {0.0, 0.0, 0.0}, {}, {}, {}};
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
gnss_observables_iter->second,
beidou_ephemeris_iter->second.WN + BEIDOU_DNAV_BDT2GPST_WEEK_NUM_OFFSET,
d_rtklib_band_index.at(sig_));
valid_obs++;
}
}
else
{
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
}
}
break;
}
default:
DLOG(INFO) << "Hybrid observables: Unknown GNSS";
break;
}
}
int navigation_ephemerides = valid_obs;
if (d_rtk.opt.sateph == EPHOPT_SBAS)
{
for (const auto &previous_ephemeris : gps_previous_ephemeris_map)
{
const bool is_qzss = MINPRNQZS <= previous_ephemeris.first && previous_ephemeris.first <= MAXPRNQZS;
const int satellite = satno(is_qzss ? SYS_QZS : SYS_GPS, previous_ephemeris.first);
bool satellite_is_observed = false;
for (int index = 0; index < valid_obs; ++index)
{
if (eph_data[index].sat == satellite)
{
satellite_is_observed = true;
break;
}
}
if (!satellite_is_observed)
{
continue;
}
if (navigation_ephemerides >= static_cast<int>(eph_data.size()))
{
eph_data.resize(static_cast<size_t>(navigation_ephemerides) + 1);
}
const std::string system = is_qzss ? "QZSS" : "GPS";
eph_data[navigation_ephemerides] = eph_to_rtklib(previous_ephemeris.second,
d_has_orbit_corrections_store_map[system],
d_has_clock_corrections_store_map[system],
this->get_ref_gps_week());
++navigation_ephemerides;
}
}
// **********************************************************************
// ****** SOLVE PVT******************************************************
// **********************************************************************
int rover_observation_count = merge_duplicated_rover_observations(valid_obs + glo_valid_obs);
int base_observation_count = 0;
bool fixed_base_applied = false;
if (fixed_base != nullptr)
{
const bool fixed_base_was_applied = d_fixed_base_was_applied;
fixed_base_applied = prepare_fixed_base_observations(*fixed_base, rover_observation_count, base_observation_count);
if (!fixed_base_applied && fixed_base_was_applied)
{
// Do not resume carrier-phase ambiguities after an outage.
// The temporary single-point fallback updates solution time
// but does not make the prior relative filter state valid.
reset_relative_filter();
}
d_fixed_base_was_applied = fixed_base_applied;
}
const bool use_single_fallback = fixed_base != nullptr && !fixed_base_applied && d_conf.ntrip_fallback_to_single;
const int nobs_total = rover_observation_count + base_observation_count;
this->set_valid_position(false);
if (fixed_base != nullptr && !fixed_base_applied && !d_conf.ntrip_fallback_to_single)
{
this->set_num_valid_observations(0);
return false;
}
if (rover_observation_count > 3)
{
int result = 0;
const auto sbas_time_reference = std::find_if(
gnss_observables_map.cbegin(), gnss_observables_map.cend(),
[](const std::pair<const int, Gnss_Synchro> &entry) {
return entry.second.fs > 0;
});
if (sbas_time_reference != gnss_observables_map.cend())
{
int gps_week = 0;
const double gps_tow_s = time2gpst(d_obs_data[0].time, &gps_week);
const auto &reference_observation = sbas_time_reference->second;
const double receiver_sample_stamp_s =
(static_cast<double>(reference_observation.Tracking_sample_counter) +
reference_observation.Code_phase_samples) /
static_cast<double>(reference_observation.fs);
d_sbas_corrections.update(gps_week, gps_tow_s, receiver_sample_stamp_s);
}
d_nav_data = {};
d_nav_data.eph = eph_data.data();
d_nav_data.geph = geph_data.data();
d_nav_data.n = navigation_ephemerides;
d_nav_data.ng = glo_valid_obs;
d_sbas_corrections.copy_to(d_nav_data);
if (gps_iono.valid)
{
d_nav_data.ion_gps[0] = gps_iono.alpha0;
d_nav_data.ion_gps[1] = gps_iono.alpha1;
d_nav_data.ion_gps[2] = gps_iono.alpha2;
d_nav_data.ion_gps[3] = gps_iono.alpha3;
d_nav_data.ion_gps[4] = gps_iono.beta0;
d_nav_data.ion_gps[5] = gps_iono.beta1;
d_nav_data.ion_gps[6] = gps_iono.beta2;
d_nav_data.ion_gps[7] = gps_iono.beta3;
}
if (!(gps_iono.valid) && gps_cnav_iono.valid)
{
d_nav_data.ion_gps[0] = gps_cnav_iono.alpha0;
d_nav_data.ion_gps[1] = gps_cnav_iono.alpha1;
d_nav_data.ion_gps[2] = gps_cnav_iono.alpha2;
d_nav_data.ion_gps[3] = gps_cnav_iono.alpha3;
d_nav_data.ion_gps[4] = gps_cnav_iono.beta0;
d_nav_data.ion_gps[5] = gps_cnav_iono.beta1;
d_nav_data.ion_gps[6] = gps_cnav_iono.beta2;
d_nav_data.ion_gps[7] = gps_cnav_iono.beta3;
}
if (qzss_iono.valid || qzss_cnav_iono.valid)
{
const Gps_Iono &qzss_iono_ref = qzss_iono.valid ? static_cast<const Gps_Iono &>(qzss_iono) : static_cast<const Gps_Iono &>(qzss_cnav_iono);
d_nav_data.ion_qzs[0] = qzss_iono_ref.alpha0;
d_nav_data.ion_qzs[1] = qzss_iono_ref.alpha1;
d_nav_data.ion_qzs[2] = qzss_iono_ref.alpha2;
d_nav_data.ion_qzs[3] = qzss_iono_ref.alpha3;
d_nav_data.ion_qzs[4] = qzss_iono_ref.beta0;
d_nav_data.ion_qzs[5] = qzss_iono_ref.beta1;
d_nav_data.ion_qzs[6] = qzss_iono_ref.beta2;
d_nav_data.ion_qzs[7] = qzss_iono_ref.beta3;
if (!(gps_iono.valid) && !(gps_cnav_iono.valid))
{
// Keep the GPS-interoperable QZSS Klobuchar coefficients available
// to the broadcast ionospheric model in QZSS-only configurations
d_nav_data.ion_gps[0] = qzss_iono_ref.alpha0;
d_nav_data.ion_gps[1] = qzss_iono_ref.alpha1;
d_nav_data.ion_gps[2] = qzss_iono_ref.alpha2;
d_nav_data.ion_gps[3] = qzss_iono_ref.alpha3;
d_nav_data.ion_gps[4] = qzss_iono_ref.beta0;
d_nav_data.ion_gps[5] = qzss_iono_ref.beta1;
d_nav_data.ion_gps[6] = qzss_iono_ref.beta2;
d_nav_data.ion_gps[7] = qzss_iono_ref.beta3;
}
}
if (galileo_iono.ai0 != 0.0)
{
d_nav_data.ion_gal[0] = galileo_iono.ai0;
d_nav_data.ion_gal[1] = galileo_iono.ai1;
d_nav_data.ion_gal[2] = galileo_iono.ai2;
d_nav_data.ion_gal[3] = 0.0;
}
if (beidou_dnav_iono.valid)
{
d_nav_data.ion_cmp[0] = beidou_dnav_iono.alpha0;
d_nav_data.ion_cmp[1] = beidou_dnav_iono.alpha1;
d_nav_data.ion_cmp[2] = beidou_dnav_iono.alpha2;
d_nav_data.ion_cmp[3] = beidou_dnav_iono.alpha3;
d_nav_data.ion_cmp[4] = beidou_dnav_iono.beta0;
d_nav_data.ion_cmp[5] = beidou_dnav_iono.beta1;
d_nav_data.ion_cmp[6] = beidou_dnav_iono.beta2;
d_nav_data.ion_cmp[7] = beidou_dnav_iono.beta3;
}
if (beidou_cnav1_iono.valid)
{
d_nav_data.ion_bdgim[0] = beidou_cnav1_iono.alpha1;
d_nav_data.ion_bdgim[1] = beidou_cnav1_iono.alpha2;
d_nav_data.ion_bdgim[2] = beidou_cnav1_iono.alpha3;
d_nav_data.ion_bdgim[3] = beidou_cnav1_iono.alpha4;
d_nav_data.ion_bdgim[4] = beidou_cnav1_iono.alpha5;
d_nav_data.ion_bdgim[5] = beidou_cnav1_iono.alpha6;
d_nav_data.ion_bdgim[6] = beidou_cnav1_iono.alpha7;
d_nav_data.ion_bdgim[7] = beidou_cnav1_iono.alpha8;
d_nav_data.ion_bdgim[8] = beidou_cnav1_iono.alpha9;
d_nav_data.ion_bdgim_valid = 1;
}
else
{
d_nav_data.ion_bdgim_valid = 0;
}
if (gps_utc_model.valid)
{
d_nav_data.utc_gps[0] = gps_utc_model.A0;
d_nav_data.utc_gps[1] = gps_utc_model.A1;
d_nav_data.utc_gps[2] = gps_utc_model.tot;
d_nav_data.utc_gps[3] = gps_utc_model.WN_T;
d_nav_data.leaps = gps_utc_model.DeltaT_LS;
}
if (!(gps_utc_model.valid) && gps_cnav_utc_model.valid)
{
d_nav_data.utc_gps[0] = gps_cnav_utc_model.A0;
d_nav_data.utc_gps[1] = gps_cnav_utc_model.A1;
d_nav_data.utc_gps[2] = gps_cnav_utc_model.tot;
d_nav_data.utc_gps[3] = gps_cnav_utc_model.WN_T;
d_nav_data.leaps = gps_cnav_utc_model.DeltaT_LS;
}
if (qzss_utc_model.valid || qzss_cnav_utc_model.valid)
{
const Gps_Utc_Model &qzss_utc_ref = qzss_utc_model.valid ? static_cast<const Gps_Utc_Model &>(qzss_utc_model) : static_cast<const Gps_Utc_Model &>(qzss_cnav_utc_model);
d_nav_data.utc_qzs[0] = qzss_utc_ref.A0;
d_nav_data.utc_qzs[1] = qzss_utc_ref.A1;
d_nav_data.utc_qzs[2] = qzss_utc_ref.tot;
d_nav_data.utc_qzs[3] = qzss_utc_ref.WN_T;
if (!(gps_utc_model.valid) && !(gps_cnav_utc_model.valid))
{
// QZSS time is aligned with GPS time, so its leap seconds apply
// in QZSS-only configurations
d_nav_data.leaps = qzss_utc_ref.DeltaT_LS;
}
}
if (glonass_gnav_utc_model.valid)
{
d_nav_data.utc_glo[0] = glonass_gnav_utc_model.d_tau_c; // ??
d_nav_data.utc_glo[1] = 0.0; // ??
d_nav_data.utc_glo[2] = 0.0; // ??
d_nav_data.utc_glo[3] = 0.0; // ??
}
if (galileo_utc_model.A0 != 0.0)
{
d_nav_data.utc_gal[0] = galileo_utc_model.A0;
d_nav_data.utc_gal[1] = galileo_utc_model.A1;
d_nav_data.utc_gal[2] = galileo_utc_model.tot;
d_nav_data.utc_gal[3] = galileo_utc_model.WNot;
d_nav_data.leaps = galileo_utc_model.Delta_tLS;
}
if (beidou_dnav_utc_model.valid)
{
d_nav_data.utc_cmp[0] = beidou_dnav_utc_model.A0_UTC;
d_nav_data.utc_cmp[1] = beidou_dnav_utc_model.A1_UTC;
d_nav_data.utc_cmp[2] = 0.0; // ??
d_nav_data.utc_cmp[3] = 0.0; // ??
// RTKLIB stores the GPS-UTC leap-second count, whereas the
// BeiDou message broadcasts BDT-UTC.
d_nav_data.leaps = beidou_dnav_utc_model.DeltaT_LS + BEIDOU_DNAV_BDT2GPST_LEAP_SEC_OFFSET;
}
else if (beidou_cnav1_utc_model.valid)
{
d_nav_data.utc_cmp[0] = beidou_cnav1_utc_model.A0;
d_nav_data.utc_cmp[1] = beidou_cnav1_utc_model.A1;
d_nav_data.utc_cmp[2] = static_cast<double>(beidou_cnav1_utc_model.tot);
d_nav_data.utc_cmp[3] = static_cast<double>(beidou_cnav1_utc_model.WN_t);
// RTKLIB stores the GPS-UTC leap-second count, whereas the
// BeiDou message broadcasts BDT-UTC.
d_nav_data.leaps = beidou_cnav1_utc_model.delta_t_LS + BEIDOU_DNAV_BDT2GPST_LEAP_SEC_OFFSET;
}
/* update carrier wave length using native function call in RTKlib */
for (int i = 0; i < MAXSAT; i++)
{
for (int j = 0; j < NFREQ; j++)
{
d_nav_data.lam[i][j] = satwavelen(i + 1, d_rtklib_freq_index[j], &d_nav_data);
}
}
for (int i = 0; i < nobs_total; ++i)
{
update_galileo_observation_wavelengths(d_obs_data[i]);
}
for (int k = 0; k < nobs_total; k++)
{
update_beidou_observation_wavelengths(d_obs_data[k]);
}
const int configured_positioning_mode = d_rtk.opt.mode;
if (use_single_fallback)
{
d_rtk.opt.mode = PMODE_SINGLE;
}
result = rtkpos(&d_rtk, d_obs_data.data(), nobs_total, &d_nav_data);
d_rtk.opt.mode = configured_positioning_mode;
if (fixed_base_applied && result != 0 && d_rtk.sol.stat == SOLQ_SINGLE)
{
d_fixed_base_status = Rtklib_Fixed_Base_Status::SOLVER_FALLBACK;
}
else if (fixed_base_applied && (result == 0 || d_rtk.sol.stat == SOLQ_NONE))
{
d_fixed_base_status = Rtklib_Fixed_Base_Status::SOLVER_FAILURE;
}
if (result == 0 || d_rtk.sol.stat == SOLQ_NONE)
{
if (d_rtk.neb > 0)
{
LOG(INFO) << "RTKLIB rtkpos error: " << d_rtk.errbuf;
}
d_rtk.neb = 0; // clear error buffer to avoid repeating the error message
this->set_time_offset_s(0.0); // reset rx time estimation
this->set_num_valid_observations(0);
if (d_conf.enable_pvt_kf == true)
{
d_pvt_kf.reset_Kf();
}
}
else
{
this->set_num_valid_observations(d_rtk.sol.ns); // record the number of valid satellites used by the PVT solver
pvt_sol = d_rtk.sol;
// DOP computation
unsigned int used_sats = 0;
for (unsigned int i = 0; i < MAXSAT; i++)
{
pvt_ssat[i] = d_rtk.ssat[i];
if (d_rtk.ssat[i].vs == 1)
{
used_sats++;
}
}
std::vector<double> azel(used_sats * 2);
int index_aux = 0;
for (auto &i : d_rtk.ssat)
{
if (i.vs == 1)
{
azel[2 * index_aux] = i.azel[0];
azel[2 * index_aux + 1] = i.azel[1];
index_aux++;
}
}
if (index_aux > 0)
{
dops(index_aux, azel.data(), 0.0, d_dop.data());
}
this->set_valid_position(true);
std::array<double, 4> rx_position_and_time{};
if (d_conf.enable_pvt_kf == true)
{
arma::vec p = {pvt_sol.rr[0], pvt_sol.rr[1], pvt_sol.rr[2]};
arma::vec v = {pvt_sol.rr[3], pvt_sol.rr[4], pvt_sol.rr[5]};
if (d_conf.kf_use_imu_vel)
{
v = {
sensor_data_aggregator.get_last_f32(SensorIdentifier::IMU_VEL_X).value,
sensor_data_aggregator.get_last_f32(SensorIdentifier::IMU_VEL_Y).value,
sensor_data_aggregator.get_last_f32(SensorIdentifier::IMU_VEL_Z).value};
}
if (d_pvt_kf.is_initialized() == false)
{
d_pvt_kf.init_Kf(p,
v,
kf_update_interval_s,
d_conf.measures_ecef_pos_sd_m,
d_conf.measures_ecef_vel_sd_ms,
d_conf.system_ecef_pos_sd_m,
d_conf.system_ecef_vel_sd_ms);
}
else
{
d_pvt_kf.run_Kf(p, v);
d_pvt_kf.get_pv_Kf(p, v);
pvt_sol.rr[0] = p[0]; // [m]
pvt_sol.rr[1] = p[1]; // [m]
pvt_sol.rr[2] = p[2]; // [m]
pvt_sol.rr[3] = v[0]; // [ms]
pvt_sol.rr[4] = v[1]; // [ms]
pvt_sol.rr[5] = v[2]; // [ms]
}
}
rx_position_and_time[0] = pvt_sol.rr[0]; // [m]
rx_position_and_time[1] = pvt_sol.rr[1]; // [m]
rx_position_and_time[2] = pvt_sol.rr[2]; // [m]
const bool is_ppp_solution = d_rtk.opt.mode >= PMODE_PPP_KINEMA && pvt_sol.stat == SOLQ_PPP;
if (!is_ppp_solution)
{
// SPP and relative-positioning solutions express dtr in seconds.
rx_position_and_time[3] = pvt_sol.dtr[0] + pvt_sol.dtr[2];
}
else
{
// PPP stores its estimated receiver-clock state in metres. Inter-system
// offsets retained from the point solution remain in seconds.
rx_position_and_time[3] = pvt_sol.dtr[2] + pvt_sol.dtr[0] / SPEED_OF_LIGHT_M_S;
}
this->set_rx_pos({rx_position_and_time[0], rx_position_and_time[1], rx_position_and_time[2]}); // save ECEF position for the next iteration
/* RTKLIB stamps SOLQ_PPP whenever the PPP filter runs, even on
broadcast orbits and clocks; only claim PPP when precise
products are loaded. Demoting here - after the PPP dtr-unit
handling above, which must follow the filter's internal
state - keeps the console, Monitor, dump and NMEA outputs
consistent with each other */
if (pvt_sol.stat == SOLQ_PPP && !has_precise_ephemeris())
{
pvt_sol.stat = SOLQ_SINGLE;
}
// compute Ground speed and COG
std::array<double, 3> pos{};
std::array<double, 3> enuv{};
ecef2pos(pvt_sol.rr, pos.data());
ecef2enu(pos.data(), &pvt_sol.rr[3], enuv.data());
const double ground_speed_ms = norm_rtk(enuv.data(), 2);
this->set_speed_over_ground(ground_speed_ms);
double new_cog = -9999.0; // COG not estimated due to insufficient velocity
if (ground_speed_ms >= 1.0)
{
new_cog = atan2(enuv[0], enuv[1]) * R2D;
if (new_cog < 0.0)
{
new_cog += 360.0;
}
this->set_course_over_ground(new_cog);
}
this->set_time_offset_s(rx_position_and_time[3]);
DLOG(INFO) << "RTKLIB Position at RX TOW = " << gnss_observables_map.cbegin()->second.RX_time
<< " in ECEF (X,Y,Z,t[meters]) = " << rx_position_and_time[0] << ", " << rx_position_and_time[1] << ", " << rx_position_and_time[2] << ", " << rx_position_and_time[3];
// gtime_t rtklib_utc_time = gpst2utc(pvt_sol.time); // Corrected RX Time (Non integer multiply of 1 ms of granularity)
// Uncorrected RX Time (integer multiply of 1 ms and the same observables time reported in RTCM and RINEX)
// Note: estpos() corrects the solution time only by the receiver-to-GPS clock
// state dtr[0], so only that term must be added back here. In solutions without
// GPS satellites, the receiver clock offset is absorbed by the inter-system
// states (dtr[1..3]) and pvt_sol.time already holds the uncorrected epoch.
const double dtr0_s = is_ppp_solution ? pvt_sol.dtr[0] / SPEED_OF_LIGHT_M_S : pvt_sol.dtr[0];
const gtime_t rtklib_time = timeadd(pvt_sol.time, dtr0_s); // uncorrected rx time
const gtime_t rtklib_utc_time = gpst2utc(rtklib_time);
boost::posix_time::ptime p_time = boost::posix_time::from_time_t(rtklib_utc_time.time);
p_time += boost::posix_time::microseconds(static_cast<long>(round(rtklib_utc_time.sec * 1e6))); // NOLINT(google-runtime-int)
this->set_position_UTC_time(p_time);
DLOG(INFO) << "RTKLIB Position at " << boost::posix_time::to_simple_string(p_time)
<< " is Lat = " << this->get_latitude() << " [deg], Long = " << this->get_longitude()
<< " [deg], Height= " << this->get_height() << " [m]"
<< " RX time offset= " << this->get_time_offset_s() << " [s]";
// ######## PVT MONITOR #########
// TOW
d_monitor_pvt.TOW_at_current_symbol_ms = gnss_observables_map.cbegin()->second.TOW_at_current_symbol_ms;
// WEEK
d_monitor_pvt.week = adjgpsweek(d_nav_data.eph[0].week, this->get_ref_gps_week());
// PVT GPS time
d_monitor_pvt.RX_time = gnss_observables_map.cbegin()->second.RX_time;
// User clock offset [s]
d_monitor_pvt.user_clk_offset = rx_position_and_time[3];
// ECEF POS X,Y,X [m] + ECEF VEL X,Y,X [m/s] (6 x double)
d_monitor_pvt.pos_x = pvt_sol.rr[0];
d_monitor_pvt.pos_y = pvt_sol.rr[1];
d_monitor_pvt.pos_z = pvt_sol.rr[2];
d_monitor_pvt.vel_x = pvt_sol.rr[3];
d_monitor_pvt.vel_y = pvt_sol.rr[4];
d_monitor_pvt.vel_z = pvt_sol.rr[5];
// position variance/covariance (m^2) {c_xx,c_yy,c_zz,c_xy,c_yz,c_zx} (6 x double)
d_monitor_pvt.cov_xx = pvt_sol.qr[0];
d_monitor_pvt.cov_yy = pvt_sol.qr[1];
d_monitor_pvt.cov_zz = pvt_sol.qr[2];
d_monitor_pvt.cov_xy = pvt_sol.qr[3];
d_monitor_pvt.cov_yz = pvt_sol.qr[4];
d_monitor_pvt.cov_zx = pvt_sol.qr[5];
// GEO user position Latitude [deg]
d_monitor_pvt.latitude = this->get_latitude();
// GEO user position Longitude [deg]
d_monitor_pvt.longitude = this->get_longitude();
// GEO user position Height [m]
d_monitor_pvt.height = this->get_height();
// NUMBER OF VALID SATS
d_monitor_pvt.valid_sats = pvt_sol.ns;
// RTKLIB solution status
d_monitor_pvt.solution_status = pvt_sol.stat;
// RTKLIB solution type (0:xyz-ecef,1:enu-baseline)
d_monitor_pvt.solution_type = pvt_sol.type;
// AR ratio factor for validation
d_monitor_pvt.AR_ratio_factor = pvt_sol.ratio;
// AR ratio threshold for validation
d_monitor_pvt.AR_ratio_threshold = pvt_sol.thres;
// GDOP / PDOP/ HDOP/ VDOP
d_monitor_pvt.gdop = d_dop[0];
d_monitor_pvt.pdop = d_dop[1];
d_monitor_pvt.hdop = d_dop[2];
d_monitor_pvt.vdop = d_dop[3];
// USED SATELLITES: per-satellite azimuth/elevation and which
// signal(s) contributed to this fix. Signals are listed
// individually (one entry per satellite per signal); a
// satellite whose signals were combined (e.g. Galileo E1+E5a
// iono-free combination -- see the "dual-frequency" branch of
// prange() in rtklib_pntpos.cc) gets one entry per signal, all
// flagged combined = true.
//
// pntpos() fills ssat[].azel for every observation; only
// ssat[].vs is gated by PVT.elevation_mask / RAIM FDE. A
// tracked satellite excluded from the solve is therefore
// reported with used = false rather than dropped, so the
// monitor can tell "excluded" from "not tracked".
//
// NaN az/el (corrupt ephemeris/almanac) is deliberately
// reported as-is for consumers to handle; rescode()
// already keeps NaN out of the position solve.
d_monitor_pvt.tracked_satellites.clear();
for (int sat_idx = 0; sat_idx < MAXSAT; sat_idx++)
{
const bool has_azel = (pvt_ssat[sat_idx].azel[0] != 0.0) || (pvt_ssat[sat_idx].azel[1] != 0.0);
if (!has_azel)
{
continue;
}
const bool used = pvt_ssat[sat_idx].vs != 0;
int prn = 0;
char sys_char = '?';
switch (satsys(sat_idx + 1, &prn))
{
case SYS_GPS:
sys_char = 'G';
break;
case SYS_GAL:
sys_char = 'E';
break;
case SYS_GLO:
sys_char = 'R';
break;
case SYS_BDS:
sys_char = 'C';
break;
case SYS_SBS:
sys_char = 'S';
break;
case SYS_QZS:
sys_char = 'J';
break;
default:
break;
}
std::vector<const Gnss_Synchro *> contributing_signals;
for (const auto &observable_pair : gnss_observables_map)
{
const Gnss_Synchro &synchro = observable_pair.second;
if (synchro.System == sys_char && static_cast<int>(synchro.PRN) == prn)
{
contributing_signals.push_back(&synchro);
}
}
const bool combined = contributing_signals.size() > 1;
// satazel() (rtklib_rtkcmn.cc) returns azimuth in [0, 2*pi); wrap to
// (-180, 180] deg, the convention expected downstream (monitor sky plot).
double az_deg = pvt_ssat[sat_idx].azel[0] * R2D;
if (az_deg > 180.0)
{
az_deg -= 360.0;
}
for (const Gnss_Synchro *synchro : contributing_signals)
{
Monitor_Pvt::TrackedSatelliteInfo info;
info.prn = static_cast<uint32_t>(prn);
info.system = sys_char;
info.signal = std::string(synchro->Signal, 2);
info.azimuth_deg = az_deg;
info.elevation_deg = pvt_ssat[sat_idx].azel[1] * R2D;
info.combined = combined;
info.used = used;
// Broadcast health of this signal, as reported by the
// navigation message that carries it (true when no health
// information is available). Independent of `used`.
const auto observation_tow = static_cast<uint32_t>(synchro->interp_TOW_ms / 1000.0);
bool healthy = true;
if (get_broadcast_signal_health(sys_char, static_cast<uint32_t>(prn), info.signal, observation_tow, healthy))
{
info.healthy = healthy;
}
d_monitor_pvt.tracked_satellites.push_back(info);
}
}
this->set_rx_vel({enuv[0], enuv[1], enuv[2]});
// ENU vel [m/s]
d_monitor_pvt.vel_e = enuv[0];
d_monitor_pvt.vel_n = enuv[1];
d_monitor_pvt.vel_u = enuv[2];
// Course Over Ground (cog) [deg]
d_monitor_pvt.cog = new_cog;
// Galileo HAS status: 1- HAS messages decoded and applied, 0 - HAS not available
if (d_has_obs_corr_map.empty())
{
d_monitor_pvt.galhas_status = 0;
}
else
{
d_monitor_pvt.galhas_status = 1;
}
const double clock_drift_ppm = pvt_sol.dtr[5] / SPEED_OF_LIGHT_M_S * 1e6;
this->set_clock_drift_ppm(clock_drift_ppm);
// User clock drift [ppm]
d_monitor_pvt.user_clk_drift_ppm = clock_drift_ppm;
// write UTC time string
// Use a facet to display time in a custom format (only hour and minutes).
auto *facet = new boost::posix_time::time_facet();
facet->format("%Y-%m-%dT%H:%M:%S%F");
std::stringstream stream;
stream.imbue(std::locale(std::locale::classic(), facet));
stream << p_time;
stream << 'Z';
d_monitor_pvt.utc_time = stream.str();
// ######## LOG FILE #########
if (d_flag_dump_enabled == true && dump_this_epoch == true)
{
// MULTIPLEXED FILE RECORDING - Record results to file
try
{
double tmp_double;
uint32_t tmp_uint32;
// TOW
tmp_uint32 = gnss_observables_map.cbegin()->second.TOW_at_current_symbol_ms;
d_dump_file.write(reinterpret_cast<char *>(&tmp_uint32), sizeof(uint32_t));
// WEEK
tmp_uint32 = adjgpsweek(d_nav_data.eph[0].week, this->get_ref_gps_week());
d_dump_file.write(reinterpret_cast<char *>(&tmp_uint32), sizeof(uint32_t));
// PVT GPS time
tmp_double = gnss_observables_map.cbegin()->second.RX_time;
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
// User clock offset [s]
tmp_double = rx_position_and_time[3];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
// ECEF POS X,Y,X [m] + ECEF VEL X,Y,X [m/s] (6 x double)
tmp_double = pvt_sol.rr[0];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.rr[1];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.rr[2];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.rr[3];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.rr[4];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.rr[5];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
// position variance/covariance (m^2) {c_xx,c_yy,c_zz,c_xy,c_yz,c_zx} (6 x double)
tmp_double = pvt_sol.qr[0];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.qr[1];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.qr[2];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.qr[3];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.qr[4];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
tmp_double = pvt_sol.qr[5];
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
// GEO user position Latitude [deg]
tmp_double = this->get_latitude();
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
// GEO user position Longitude [deg]
tmp_double = this->get_longitude();
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
// GEO user position Height [m]
tmp_double = this->get_height();
d_dump_file.write(reinterpret_cast<char *>(&tmp_double), sizeof(double));
// NUMBER OF VALID SATS
d_dump_file.write(reinterpret_cast<char *>(&pvt_sol.ns), sizeof(uint8_t));
// RTKLIB solution status
d_dump_file.write(reinterpret_cast<char *>(&pvt_sol.stat), sizeof(uint8_t));
// RTKLIB solution type (0:xyz-ecef,1:enu-baseline)
d_dump_file.write(reinterpret_cast<char *>(&pvt_sol.type), sizeof(uint8_t));
// AR ratio factor for validation
d_dump_file.write(reinterpret_cast<char *>(&pvt_sol.ratio), sizeof(float));
// AR ratio threshold for validation
d_dump_file.write(reinterpret_cast<char *>(&pvt_sol.thres), sizeof(float));
// GDOP / PDOP / HDOP / VDOP
d_dump_file.write(reinterpret_cast<char *>(&d_dop[0]), sizeof(double));
d_dump_file.write(reinterpret_cast<char *>(&d_dop[1]), sizeof(double));
d_dump_file.write(reinterpret_cast<char *>(&d_dop[2]), sizeof(double));
d_dump_file.write(reinterpret_cast<char *>(&d_dop[3]), sizeof(double));
}
catch (const std::ofstream::failure &e)
{
LOG(WARNING) << "Exception writing RTKLIB dump file " << e.what();
}
}
}
}
return this->is_valid_position();
}