mirror of
https://github.com/gnss-sdr/gnss-sdr
synced 2026-10-13 03:01:50 +00:00
Address the findings of a second deep review of the RTK and NTRIP code
This commit is contained in:
@@ -23,9 +23,13 @@
|
||||
# GNSSSDR_GNUTLS_INCLUDE_DIR - Path to the GnuTLS include directory
|
||||
#
|
||||
# Provided function:
|
||||
# link_to_crypto_dependencies(<target>)
|
||||
# link_to_crypto_dependencies(<target> [<scope>])
|
||||
# Links <target> against the selected crypto backend and sets the
|
||||
# appropriate compile definitions:
|
||||
# appropriate compile definitions. <scope> (PUBLIC, PRIVATE or INTERFACE,
|
||||
# default PUBLIC) controls whether the backend libraries, include
|
||||
# directories and USE_* definitions propagate to the target's consumers;
|
||||
# use PRIVATE when the crypto backend is an implementation detail that no
|
||||
# public header of <target> exposes:
|
||||
# USE_OPENSSL_3=1 (OpenSSL >= 3.0.0)
|
||||
# USE_OPENSSL_111=1 (OpenSSL >= 1.1.1, < 3.0.0)
|
||||
# USE_GNUTLS_FALLBACK=1 (GnuTLS backend)
|
||||
@@ -326,10 +330,20 @@ function(link_to_crypto_dependencies target)
|
||||
)
|
||||
endif()
|
||||
|
||||
set(scope PUBLIC)
|
||||
if(ARGC GREATER 1)
|
||||
set(scope ${ARGV1})
|
||||
if(NOT scope MATCHES "^(PUBLIC|PRIVATE|INTERFACE)$")
|
||||
message(FATAL_ERROR
|
||||
"link_to_crypto_dependencies: invalid scope ${scope} (expected PUBLIC, PRIVATE or INTERFACE)"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(GNSSSDR_OPENSSL_FOUND)
|
||||
if(TARGET OpenSSL::SSL)
|
||||
target_link_libraries(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
OpenSSL::SSL
|
||||
)
|
||||
|
||||
@@ -354,19 +368,19 @@ function(link_to_crypto_dependencies target)
|
||||
if(_gnsssdr_openssl_libraries)
|
||||
list(REMOVE_DUPLICATES _gnsssdr_openssl_libraries)
|
||||
target_link_libraries(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
${_gnsssdr_openssl_libraries}
|
||||
)
|
||||
endif()
|
||||
|
||||
if(OPENSSL_INCLUDE_DIR)
|
||||
target_include_directories(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
${OPENSSL_INCLUDE_DIR}
|
||||
)
|
||||
elseif(OPENSSL_INCLUDE_DIRS)
|
||||
target_include_directories(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
${OPENSSL_INCLUDE_DIRS}
|
||||
)
|
||||
endif()
|
||||
@@ -377,12 +391,12 @@ function(link_to_crypto_dependencies target)
|
||||
if(GNSSSDR_OPENSSL_VERSION_NUMBER)
|
||||
if(NOT "${GNSSSDR_OPENSSL_VERSION_NUMBER}" VERSION_LESS "3.0.0")
|
||||
target_compile_definitions(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
USE_OPENSSL_3=1
|
||||
)
|
||||
elseif(NOT "${GNSSSDR_OPENSSL_VERSION_NUMBER}" VERSION_LESS "1.1.1")
|
||||
target_compile_definitions(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
USE_OPENSSL_111=1
|
||||
)
|
||||
endif()
|
||||
@@ -390,25 +404,25 @@ function(link_to_crypto_dependencies target)
|
||||
else()
|
||||
if(TARGET GnuTLS::GnuTLS)
|
||||
target_link_libraries(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
GnuTLS::GnuTLS
|
||||
${GNUTLS_OPENSSL_LIBRARY}
|
||||
)
|
||||
else()
|
||||
target_link_libraries(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
${GNUTLS_LIBRARIES}
|
||||
${GNUTLS_OPENSSL_LIBRARY}
|
||||
)
|
||||
|
||||
if(GNUTLS_INCLUDE_DIRS)
|
||||
target_include_directories(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
${GNUTLS_INCLUDE_DIRS}
|
||||
)
|
||||
elseif(GNSSSDR_GNUTLS_INCLUDE_DIR)
|
||||
target_include_directories(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
${GNSSSDR_GNUTLS_INCLUDE_DIR}
|
||||
)
|
||||
endif()
|
||||
@@ -442,13 +456,13 @@ function(link_to_crypto_dependencies target)
|
||||
|
||||
if(GNUTLS_DEFINITIONS)
|
||||
target_compile_options(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
${GNUTLS_DEFINITIONS}
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(${target}
|
||||
PUBLIC
|
||||
${scope}
|
||||
USE_GNUTLS_FALLBACK=1
|
||||
)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "gps_ephemeris.h" // for Gps_Ephemeris
|
||||
#include "gps_week_rollover.h" // for gps_ref_week_from_config
|
||||
#include "ntrip_rtcm_client.h" // for make_ntrip_rtcm_client_config, secure_clear_string
|
||||
#include "ntrip_rtk_signals.h" // for the NTRIP RTK signal table and derived masks
|
||||
#include "pvt_conf.h" // for Pvt_Conf
|
||||
#include "rtklib_rtkpos.h" // for rtkfree, rtkinit
|
||||
#include "signal_enabled_flags.h" // for signal_enabled_flags
|
||||
@@ -68,9 +69,7 @@ public:
|
||||
|
||||
~Ntrip_Credential_Guard()
|
||||
{
|
||||
secure_clear_string(&d_configuration->ntrip_username);
|
||||
secure_clear_string(&d_configuration->ntrip_password);
|
||||
secure_clear_string(&d_configuration->ntrip_password_env);
|
||||
secure_clear_ntrip_credentials(d_configuration);
|
||||
}
|
||||
|
||||
Ntrip_Credential_Guard(const Ntrip_Credential_Guard&) = delete;
|
||||
@@ -254,11 +253,11 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration,
|
||||
{
|
||||
throw std::invalid_argument(role + ".ntrip_password and .ntrip_password_env are mutually exclusive");
|
||||
}
|
||||
if (has_space_or_control(pvt_output_parameters.ntrip_password_env))
|
||||
if (has_space_or_control(pvt_output_parameters.ntrip_password_env.value()))
|
||||
{
|
||||
throw std::invalid_argument(role + ".ntrip_password_env must be one environment-variable name");
|
||||
}
|
||||
const char* password = std::getenv(pvt_output_parameters.ntrip_password_env.c_str());
|
||||
const char* password = std::getenv(pvt_output_parameters.ntrip_password_env.value().c_str());
|
||||
if (password == nullptr)
|
||||
{
|
||||
throw std::invalid_argument(role + ".ntrip_password_env names an environment variable that is not set");
|
||||
@@ -357,7 +356,7 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration,
|
||||
const bool ntrip_gal_e5a = signal_enabled_flags.check_any_enabled(GAL_E5a);
|
||||
const bool ntrip_bds_b1c = signal_enabled_flags.check_any_enabled(BDS_B1C);
|
||||
const bool ntrip_only_supported_signals =
|
||||
(signal_enabled_flags.flags & ~(GPS_1C | GPS_2S | GPS_L5 | GAL_1B | GAL_E5a | BDS_B1C)) == 0;
|
||||
(signal_enabled_flags.flags & ~ntrip_rtk_supported_signal_mask()) == 0;
|
||||
const bool ntrip_valid_channel_set =
|
||||
ntrip_only_supported_signals &&
|
||||
(ntrip_gps_l1 || ntrip_gal_e1 || ntrip_bds_b1c) &&
|
||||
@@ -413,10 +412,10 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration,
|
||||
}
|
||||
if (pvt_output_parameters.ntrip_client_enabled)
|
||||
{
|
||||
// The slot count is fully determined by the channel set: GPS L5
|
||||
// and Galileo E5a occupy RTKLIB's third frequency slot, GPS L2C
|
||||
// the second, and single-band sets run on the first slot alone
|
||||
const int ntrip_required_bands = (ntrip_gps_l5 || ntrip_gal_e5a) ? 3 : (ntrip_gps_l2 ? 2 : 1);
|
||||
// The slot count is fully determined by the channel set (see the
|
||||
// slot column of NTRIP_RTK_SIGNALS): single-band sets run on the
|
||||
// first slot alone
|
||||
const int ntrip_required_bands = ntrip_rtk_required_bands(signal_enabled_flags);
|
||||
if (number_of_frequencies != ntrip_required_bands)
|
||||
{
|
||||
LOG(INFO) << "NTRIP RTK: setting the number of frequency slots to " << ntrip_required_bands;
|
||||
@@ -592,9 +591,7 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration,
|
||||
}
|
||||
if (pvt_output_parameters.ntrip_client_enabled)
|
||||
{
|
||||
const int expected_navigation_system =
|
||||
(ntrip_gps_l1 ? SYS_GPS : 0) | (ntrip_gal_e1 ? SYS_GAL : 0) |
|
||||
(ntrip_bds_b1c ? SYS_BDS : 0);
|
||||
const int expected_navigation_system = ntrip_rtk_navigation_systems(signal_enabled_flags);
|
||||
if (navigation_system != expected_navigation_system)
|
||||
{
|
||||
throw std::invalid_argument(role + ".navigation_system must select exactly the constellations of the enabled RTK channels");
|
||||
@@ -798,6 +795,21 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration,
|
||||
ar_max_position_variance /* max position variance to attempt AR (m^2) */
|
||||
};
|
||||
|
||||
if (integer_ambiguity_resolution_gps > 0 &&
|
||||
(ar_filter || min_fix_sats > 0 || min_hold_sats > 0 || min_drop_sats > 0 || ar_max_position_variance > 0.0))
|
||||
{
|
||||
// These gates default ON (demo5 behavior), unlike the RTKLIB
|
||||
// PRCOPT_DEFAULT zeros: say so where the user can see why AR may
|
||||
// silently be skipped (e.g. float variance above the gate)
|
||||
LOG(INFO) << "RTK AR management gates active: ar_filter=" << (ar_filter ? "true" : "false")
|
||||
<< ", min_fix_sats=" << min_fix_sats
|
||||
<< ", min_hold_sats=" << min_hold_sats
|
||||
<< ", min_drop_sats=" << min_drop_sats
|
||||
<< ", ar_max_position_variance=" << ar_max_position_variance
|
||||
<< " m^2 (AR is skipped while the float position variance exceeds it;"
|
||||
<< " set these options to 0/false for the classic RTKLIB behavior)";
|
||||
}
|
||||
|
||||
// Outputs
|
||||
const bool default_output_enabled = configuration->property(role + ".output_enabled", true);
|
||||
pvt_output_parameters.output_enabled = default_output_enabled;
|
||||
|
||||
@@ -266,6 +266,13 @@ rtklib_pvt_gs::rtklib_pvt_gs(uint32_t nchannels,
|
||||
d_use_unhealthy_sats(conf_.use_unhealthy_sats),
|
||||
d_osnma_strict(conf_.osnma_strict)
|
||||
{
|
||||
if (conf_.ntrip_client_enabled && !conf_.enable_rx_clock_correction)
|
||||
{
|
||||
// do not override an explicit user setting silently: the timetag
|
||||
// processing path only runs when the clock correction is disabled
|
||||
std::cout << "Warning: PVT.enable_rx_clock_correction=false is overridden because the NTRIP client requires the receiver clock correction\n";
|
||||
LOG(WARNING) << "PVT.enable_rx_clock_correction=false overridden: the NTRIP client requires the receiver clock correction, so source-timetag processing is disabled";
|
||||
}
|
||||
// Send feedback message to observables block with the receiver clock offset
|
||||
this->message_port_register_out(pmt::mp("pvt_to_observables"));
|
||||
// Experimental: VLT commands from PVT to tracking channels
|
||||
@@ -2385,6 +2392,14 @@ void rtklib_pvt_gs::report_ntrip_client_status()
|
||||
{
|
||||
return;
|
||||
}
|
||||
/* the status copy locks the client's mutex and allocates for the message
|
||||
string; skip it while nothing has changed (this runs once per epoch) */
|
||||
const uint64_t state_generation = d_ntrip_client->state_generation();
|
||||
if (state_generation == d_last_ntrip_state_generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
d_last_ntrip_state_generation = state_generation;
|
||||
const Ntrip_Rtcm_Client_Status status = d_ntrip_client->status();
|
||||
const int state = static_cast<int>(status.state);
|
||||
if (state == d_last_ntrip_client_state)
|
||||
@@ -2444,7 +2459,10 @@ void rtklib_pvt_gs::report_fixed_base_status()
|
||||
LOG(WARNING) << "NTRIP base position is invalid";
|
||||
break;
|
||||
case Rtklib_Fixed_Base_Status::INSUFFICIENT_COMMON_SATELLITES:
|
||||
LOG(WARNING) << "NTRIP RTK has fewer than four common GPS satellites: "
|
||||
/* the count spans every RTK constellation; the threshold is the
|
||||
solver's, not this display layer's */
|
||||
LOG(WARNING) << "NTRIP RTK has fewer than " << Rtklib_Solver::NTRIP_MIN_COMMON_SATELLITES
|
||||
<< " common satellites: "
|
||||
<< d_user_pvt_solver->get_fixed_base_common_satellites();
|
||||
break;
|
||||
case Rtklib_Fixed_Base_Status::NOT_REQUESTED:
|
||||
@@ -2454,22 +2472,11 @@ void rtklib_pvt_gs::report_fixed_base_status()
|
||||
}
|
||||
|
||||
|
||||
int rtklib_pvt_gs::displayed_solution_status() const
|
||||
{
|
||||
const int status = d_user_pvt_solver->pvt_sol.stat;
|
||||
/* RTKLIB stamps SOLQ_PPP whenever the PPP filter runs, even on broadcast
|
||||
orbits and clocks; only claim PPP when precise products are loaded */
|
||||
if (status == SOLQ_PPP && !d_user_pvt_solver->has_precise_ephemeris())
|
||||
{
|
||||
return SOLQ_SINGLE;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
void rtklib_pvt_gs::report_solution_status()
|
||||
{
|
||||
const int status = displayed_solution_status();
|
||||
/* the solver already demotes broadcast-products PPP to SOLQ_SINGLE, so
|
||||
pvt_sol.stat is the effective status for every output */
|
||||
const int status = d_user_pvt_solver->pvt_sol.stat;
|
||||
if (status == d_last_solution_status)
|
||||
{
|
||||
return;
|
||||
@@ -2532,6 +2539,39 @@ void rtklib_pvt_gs::report_solution_status()
|
||||
}
|
||||
|
||||
|
||||
void rtklib_pvt_gs::report_solution_outage()
|
||||
{
|
||||
/* a failed solve from a labeled state must be announced and must clear
|
||||
d_last_solution_status: otherwise an outage that recovers into the same
|
||||
status (e.g. FIXED -> outage -> FIXED) would print nothing at all */
|
||||
const int previous_status = d_last_solution_status;
|
||||
if (previous_status == SOLQ_NONE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
d_last_solution_status = SOLQ_NONE;
|
||||
if (solution_status_tag(previous_status).empty())
|
||||
{
|
||||
return; /* plain single-point operation keeps the classic output */
|
||||
}
|
||||
std::ostringstream report;
|
||||
if (previous_status == SOLQ_SBAS)
|
||||
{
|
||||
report << "SBAS corrections lost: no PVT solution";
|
||||
}
|
||||
else if (previous_status == SOLQ_PPP)
|
||||
{
|
||||
report << "PPP lost: no PVT solution";
|
||||
}
|
||||
else
|
||||
{
|
||||
report << "RTK lost: no PVT solution";
|
||||
}
|
||||
std::cout << TEXT_BOLD_YELLOW << report.str() << TEXT_RESET << std::endl;
|
||||
LOG(INFO) << "Solution status change: " << report.str();
|
||||
}
|
||||
|
||||
|
||||
int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_items,
|
||||
gr_vector_void_star& output_items __attribute__((unused)))
|
||||
{
|
||||
@@ -2908,6 +2948,7 @@ int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_item
|
||||
}
|
||||
else
|
||||
{
|
||||
report_solution_outage();
|
||||
// sanity check: If the PVT solver is getting 100 consecutive errors, send a reset command to observables block
|
||||
d_pvt_errors_counter++;
|
||||
if (d_pvt_errors_counter >= 100)
|
||||
@@ -2926,12 +2967,20 @@ int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_item
|
||||
// epoch twice to the solver and duplicate the dump record
|
||||
if (flag_compute_pvt_output == true && d_enable_rx_clock_correction == true)
|
||||
{
|
||||
Ntrip_Rtcm_Snapshot fixed_base_snapshot;
|
||||
const Ntrip_Rtcm_Snapshot* fixed_base = nullptr;
|
||||
if (d_ntrip_client)
|
||||
{
|
||||
fixed_base_snapshot = d_ntrip_client->latest_snapshot(d_internal_pvt_solver->pvt_sol.time);
|
||||
fixed_base = &fixed_base_snapshot;
|
||||
/* deep-copy the snapshot only when the client's data
|
||||
actually changed; base epochs arrive at ~1 Hz while
|
||||
this path runs at the output rate */
|
||||
const uint64_t snapshot_generation = d_ntrip_client->snapshot_generation();
|
||||
if (!d_fixed_base_snapshot || snapshot_generation != d_ntrip_snapshot_generation)
|
||||
{
|
||||
d_fixed_base_snapshot.reset(new Ntrip_Rtcm_Snapshot(
|
||||
d_ntrip_client->latest_snapshot(d_internal_pvt_solver->pvt_sol.time)));
|
||||
d_ntrip_snapshot_generation = snapshot_generation;
|
||||
}
|
||||
fixed_base = d_fixed_base_snapshot.get();
|
||||
}
|
||||
flag_pvt_valid = d_user_pvt_solver->get_PVT(
|
||||
d_gnss_observables_map,
|
||||
@@ -2940,6 +2989,10 @@ int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_item
|
||||
true,
|
||||
fixed_base);
|
||||
report_fixed_base_status();
|
||||
if (!flag_pvt_valid)
|
||||
{
|
||||
report_solution_outage();
|
||||
}
|
||||
}
|
||||
|
||||
if (flag_pvt_valid == true)
|
||||
@@ -3056,7 +3109,7 @@ int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_item
|
||||
<< " [deg], Long = " << d_user_pvt_solver->get_longitude()
|
||||
<< " [deg], Height = " << d_user_pvt_solver->get_height()
|
||||
<< " [m], with GDOP = " << d_user_pvt_solver->get_gdop()
|
||||
<< solution_status_tag(displayed_solution_status());
|
||||
<< solution_status_tag(d_user_pvt_solver->pvt_sol.stat);
|
||||
std::cout << ss.str() << std::endl;
|
||||
d_end = std::chrono::system_clock::now();
|
||||
std::chrono::duration<double> elapsed_seconds = d_end - d_start;
|
||||
@@ -3155,7 +3208,7 @@ int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_item
|
||||
std::cout.setf(std::ios::fixed, std::ios::floatfield);
|
||||
auto* facet = new boost::posix_time::time_facet("%Y-%b-%d %H:%M:%S.%f %z");
|
||||
std::cout.imbue(std::locale(std::cout.getloc(), facet));
|
||||
const int solq = displayed_solution_status();
|
||||
const int solq = d_user_pvt_solver->pvt_sol.stat;
|
||||
const std::string solq_tag = solution_status_tag(solq);
|
||||
const std::string solq_txt = solq_tag.empty() ? "" : solution_status_color(solq) + solq_tag;
|
||||
std::cout
|
||||
|
||||
@@ -65,6 +65,7 @@ class Monitor_Pvt_Udp_Sink;
|
||||
class Monitor_Ephemeris_Udp_Sink;
|
||||
class Nmea_Printer;
|
||||
class Ntrip_Rtcm_Client;
|
||||
struct Ntrip_Rtcm_Snapshot;
|
||||
class Pvt_Conf;
|
||||
class Rinex_Printer;
|
||||
class Rtcm_Printer;
|
||||
@@ -168,7 +169,7 @@ private:
|
||||
|
||||
void report_solution_status();
|
||||
|
||||
int displayed_solution_status() const;
|
||||
void report_solution_outage();
|
||||
|
||||
std::map<int, Gnss_Synchro> interpolate_observables(const std::map<int, Gnss_Synchro>& observables_map_t0,
|
||||
const std::map<int, Gnss_Synchro>& observables_map_t1,
|
||||
@@ -199,6 +200,10 @@ private:
|
||||
std::unique_ptr<Gpx_Printer> d_gpx_dump;
|
||||
std::unique_ptr<Nmea_Printer> d_nmea_printer;
|
||||
std::unique_ptr<Ntrip_Rtcm_Client> d_ntrip_client;
|
||||
// Cached copy of the client's latest snapshot, refreshed only when the
|
||||
// client's snapshot generation changes (the deep copy is expensive at the
|
||||
// observables rate).
|
||||
std::unique_ptr<Ntrip_Rtcm_Snapshot> d_fixed_base_snapshot;
|
||||
std::unique_ptr<GeoJSON_Printer> d_geojson_printer;
|
||||
std::unique_ptr<Rtcm_Printer> d_rtcm_printer;
|
||||
std::unique_ptr<Monitor_Pvt_Udp_Sink> d_udp_sink_ptr;
|
||||
@@ -289,6 +294,8 @@ private:
|
||||
uint32_t d_pvt_errors_counter;
|
||||
int d_last_ntrip_client_state;
|
||||
int d_last_fixed_base_status;
|
||||
uint64_t d_last_ntrip_state_generation = 0;
|
||||
uint64_t d_ntrip_snapshot_generation = 0;
|
||||
int d_last_solution_status;
|
||||
|
||||
bool d_dump;
|
||||
|
||||
@@ -39,6 +39,8 @@ set(PVT_LIB_HEADERS
|
||||
kml_printer.h
|
||||
nmea_printer.h
|
||||
ntrip_rtcm_client.h
|
||||
ntrip_rtk_signals.h
|
||||
secure_string.h
|
||||
rinex_printer.h
|
||||
rtcm_printer.h
|
||||
rtcm.h
|
||||
|
||||
@@ -63,63 +63,40 @@ bool valid_ecef_position(const std::array<double, 3>& position)
|
||||
}
|
||||
|
||||
|
||||
/* Band membership comes from RTKLIB's own code table (obsfreqs[] via
|
||||
code2obs() in rtklib_rtkcmn.cc), so a code added there - as this branch
|
||||
itself did for B1C's CODE_L1D - is accepted here without a parallel edit.
|
||||
The caller has already filtered by satellite system, which restricts the
|
||||
codes the RTCM decoder can produce to that system's signal set. */
|
||||
bool code_in_band(unsigned char code, int band)
|
||||
{
|
||||
int freq = 0;
|
||||
code2obs(code, &freq);
|
||||
return freq == band;
|
||||
}
|
||||
|
||||
|
||||
bool supported_gps_l1_code(unsigned char code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case CODE_L1C:
|
||||
case CODE_L1P:
|
||||
case CODE_L1W:
|
||||
case CODE_L1Y:
|
||||
case CODE_L1M:
|
||||
case CODE_L1N:
|
||||
case CODE_L1S:
|
||||
case CODE_L1L:
|
||||
case CODE_L1X:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return code_in_band(code, 1);
|
||||
}
|
||||
|
||||
|
||||
bool supported_gps_l2_code(unsigned char code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case CODE_L2C:
|
||||
case CODE_L2D:
|
||||
case CODE_L2S:
|
||||
case CODE_L2L:
|
||||
case CODE_L2X:
|
||||
case CODE_L2P:
|
||||
case CODE_L2W:
|
||||
case CODE_L2Y:
|
||||
case CODE_L2M:
|
||||
case CODE_L2N:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return code_in_band(code, 2);
|
||||
}
|
||||
|
||||
|
||||
bool supported_gal_e1_code(unsigned char code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case CODE_L1A:
|
||||
case CODE_L1B:
|
||||
case CODE_L1C:
|
||||
case CODE_L1X:
|
||||
case CODE_L1Z:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return code_in_band(code, 1);
|
||||
}
|
||||
|
||||
|
||||
// Deliberately an explicit list, not a band lookup: this is the GNSS-SDR
|
||||
// prefer-B1C rule for RTKLIB's shared first slot (B1I satellites are
|
||||
// dropped), not band membership
|
||||
bool supported_bds_b1c_code(unsigned char code)
|
||||
{
|
||||
switch (code)
|
||||
@@ -134,18 +111,10 @@ bool supported_bds_b1c_code(unsigned char code)
|
||||
}
|
||||
|
||||
|
||||
// GPS L5 and Galileo E5a share the observation codes of RTKLIB's L5 band
|
||||
// GPS L5 and Galileo E5a share RTKLIB's L5/E5a band
|
||||
bool supported_l5_band_code(unsigned char code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case CODE_L5I:
|
||||
case CODE_L5Q:
|
||||
case CODE_L5X:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return code_in_band(code, 3);
|
||||
}
|
||||
|
||||
|
||||
@@ -233,8 +202,8 @@ public:
|
||||
if (d_stream.port != nullptr)
|
||||
{
|
||||
auto* ntrip = static_cast<ntrip_t*>(d_stream.port);
|
||||
std::memset(ntrip->user, 0, sizeof(ntrip->user));
|
||||
std::memset(ntrip->passwd, 0, sizeof(ntrip->passwd));
|
||||
secure_wipe(ntrip->user, sizeof(ntrip->user));
|
||||
secure_wipe(ntrip->passwd, sizeof(ntrip->passwd));
|
||||
// Keep unopened and disconnected transports explicitly invalid
|
||||
// before handing ownership back to RTKLIB's stream cleanup.
|
||||
if (ntrip->tcp != nullptr && ntrip->tcp->svr.state < 1)
|
||||
@@ -398,15 +367,23 @@ void secure_clear_string(std::string* value) noexcept
|
||||
{
|
||||
return;
|
||||
}
|
||||
volatile char* data = &(*value)[0];
|
||||
for (std::size_t i = 0; i < value->size(); ++i)
|
||||
{
|
||||
data[i] = 0;
|
||||
}
|
||||
secure_wipe(&(*value)[0], value->size());
|
||||
value->clear();
|
||||
}
|
||||
|
||||
|
||||
void secure_clear_ntrip_credentials(Pvt_Conf* conf) noexcept
|
||||
{
|
||||
if (conf == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
conf->ntrip_username.clear();
|
||||
conf->ntrip_password.clear();
|
||||
conf->ntrip_password_env.clear();
|
||||
}
|
||||
|
||||
|
||||
std::string Ntrip_Rtcm_Client_Config::validate() const
|
||||
{
|
||||
const std::string clean_mountpoint = normalized_mountpoint(mountpoint);
|
||||
@@ -499,8 +476,8 @@ Ntrip_Rtcm_Client_Config make_ntrip_rtcm_client_config(const Pvt_Conf& conf)
|
||||
config.host = conf.ntrip_caster_address;
|
||||
config.port = conf.ntrip_port;
|
||||
config.mountpoint = normalized_mountpoint(conf.ntrip_mountpoint);
|
||||
config.username = conf.ntrip_username;
|
||||
config.password = conf.ntrip_password;
|
||||
config.username = conf.ntrip_username.value();
|
||||
config.password = conf.ntrip_password.value();
|
||||
config.reconnect_interval_ms = conf.ntrip_reconnect_interval_ms;
|
||||
config.timeout_ms = conf.ntrip_timeout_ms;
|
||||
config.max_age_s = conf.ntrip_max_correction_age_s;
|
||||
@@ -644,6 +621,17 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
std::uint64_t state_generation() const noexcept
|
||||
{
|
||||
return d_state_generation.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::uint64_t snapshot_generation() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(d_output_mutex);
|
||||
return d_output_generation;
|
||||
}
|
||||
|
||||
Ntrip_Rtcm_Snapshot latest_snapshot() const
|
||||
{
|
||||
return latest_snapshot(utc2gpst(timeget()));
|
||||
@@ -657,14 +645,14 @@ public:
|
||||
result.has_observations = d_has_observations;
|
||||
result.observations = d_observations;
|
||||
result.observation_time = d_observation_time;
|
||||
result.observation_generation = d_observation_generation;
|
||||
result.generation = d_output_generation;
|
||||
result.has_base_position = d_has_base_position;
|
||||
result.base_position_ecef_m = d_base_position_ecef_m;
|
||||
result.base_position_message_type = d_base_position_message_type;
|
||||
result.station_id = d_has_observations ? d_observation_station_id : d_base_position_station_id;
|
||||
result.itrf = d_base_position_itrf;
|
||||
result.antenna_height_m = d_antenna_height_m;
|
||||
result.gps_ephemerides = d_gps_ephemerides;
|
||||
result.ephemerides = d_ephemerides;
|
||||
}
|
||||
|
||||
if (result.has_observations)
|
||||
@@ -822,6 +810,12 @@ private:
|
||||
}
|
||||
if (!complete)
|
||||
{
|
||||
// Abandon the stalled resolution so the next reconnect cycle
|
||||
// spawns a fresh lookup instead of re-waiting forever on this
|
||||
// one: a getaddrinfo() stuck on a dead DNS server would
|
||||
// otherwise wedge reconnection until receiver restart. The
|
||||
// detached thread keeps its own shared_ptr to the state.
|
||||
pending_resolution->reset();
|
||||
*failure_message = "NTRIP caster hostname resolution timed out";
|
||||
return Resolve_Result::FAILED;
|
||||
}
|
||||
@@ -1034,6 +1028,20 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
/* Evidence that the caster cannot speak NTRIP v2 on this connection and
|
||||
a fresh-connection v1 retry is warranted: it explicitly asked for v1,
|
||||
answered in v1, or dropped the request without any response bytes.
|
||||
Single home of the heuristic, shared by the connection-closed and
|
||||
timeout paths of perform_handshake() - a refinement applied to only
|
||||
one of them would make the two downgrade under different rules. */
|
||||
static bool v1_downgrade_indicated(const ntrip_t* ntrip, int requested_version)
|
||||
{
|
||||
return requested_version == NTRIP_VERSION_2 &&
|
||||
(ntrip->v1_retry_requested != 0 ||
|
||||
ntrip->version == NTRIP_VERSION_1 ||
|
||||
ntrip->response_received == 0);
|
||||
}
|
||||
|
||||
Handshake_Result perform_handshake(stream_t* stream, int requested_version)
|
||||
{
|
||||
auto* ntrip = static_cast<ntrip_t*>(stream->port);
|
||||
@@ -1069,10 +1077,7 @@ private:
|
||||
// waitntrip() call, before connected can be latched.
|
||||
if (ntrip->request_sent != 0)
|
||||
{
|
||||
if (requested_version == NTRIP_VERSION_2 &&
|
||||
(ntrip->v1_retry_requested != 0 ||
|
||||
ntrip->version == NTRIP_VERSION_1 ||
|
||||
ntrip->response_received == 0))
|
||||
if (v1_downgrade_indicated(ntrip, requested_version))
|
||||
{
|
||||
return Handshake_Result::RETRY_V1;
|
||||
}
|
||||
@@ -1103,10 +1108,7 @@ private:
|
||||
{
|
||||
return Handshake_Result::SEND_FAILED;
|
||||
}
|
||||
if (requested_version == NTRIP_VERSION_2 &&
|
||||
(ntrip->v1_retry_requested != 0 ||
|
||||
ntrip->version == NTRIP_VERSION_1 ||
|
||||
ntrip->response_received == 0))
|
||||
if (v1_downgrade_indicated(ntrip, requested_version))
|
||||
{
|
||||
return Handshake_Result::RETRY_V1;
|
||||
}
|
||||
@@ -1120,16 +1122,29 @@ private:
|
||||
std::array<unsigned char, READ_BUFFER_SIZE> buffer = {{0}};
|
||||
bool sent_gga = false;
|
||||
std::chrono::steady_clock::time_point last_gga;
|
||||
std::chrono::steady_clock::time_point last_data = std::chrono::steady_clock::now();
|
||||
|
||||
while (!d_stop_requested.load())
|
||||
{
|
||||
apply_decoder_time(decoder, applied_rover_time_generation, false);
|
||||
const int count = strread(stream, buffer.data(), static_cast<int>(buffer.size()));
|
||||
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
if (count > 0)
|
||||
{
|
||||
last_data = now;
|
||||
add_received_bytes(static_cast<std::uint64_t>(count));
|
||||
decode_bytes(decoder, buffer.data(), count);
|
||||
}
|
||||
else if (d_config.timeout_ms > 0 &&
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_data).count() >
|
||||
d_config.timeout_ms)
|
||||
{
|
||||
// The TCP-level inactivity watchdog counts writes too, so
|
||||
// the periodic GGA upload keeps feeding it while a hung
|
||||
// caster ACKs and sends nothing: enforce the timeout on
|
||||
// received-data age here so the stream reconnects
|
||||
return "correction stream inactive";
|
||||
}
|
||||
|
||||
if (ntrip == nullptr || ntrip->tcp == nullptr ||
|
||||
ntrip->tcp->svr.state != 2 || ntrip->state != 2)
|
||||
@@ -1139,7 +1154,6 @@ private:
|
||||
|
||||
if (d_config.send_gga)
|
||||
{
|
||||
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
if (!sent_gga ||
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_gga).count() >=
|
||||
d_config.gga_period_ms)
|
||||
@@ -1284,14 +1298,15 @@ private:
|
||||
d_observation_time = d_observations.front().time;
|
||||
d_observation_station_id = decoder.staid;
|
||||
d_has_observations = true;
|
||||
++d_observation_generation;
|
||||
++d_output_generation;
|
||||
}
|
||||
increment_observation_epochs();
|
||||
}
|
||||
|
||||
void store_ephemeris(const rtcm_t& decoder)
|
||||
{
|
||||
if (decoder.ephsat <= 0 || satsys(decoder.ephsat, nullptr) != SYS_GPS ||
|
||||
const int system = decoder.ephsat > 0 ? satsys(decoder.ephsat, nullptr) : 0;
|
||||
if ((system != SYS_GPS && system != SYS_GAL && system != SYS_BDS && system != SYS_QZS) ||
|
||||
decoder.nav.eph == nullptr || decoder.ephsat > decoder.nav.n)
|
||||
{
|
||||
return;
|
||||
@@ -1303,42 +1318,25 @@ private:
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(d_output_mutex);
|
||||
for (auto& d_gps_ephemeride : d_gps_ephemerides)
|
||||
++d_output_generation;
|
||||
for (auto& stored_ephemeris : d_ephemerides)
|
||||
{
|
||||
if (d_gps_ephemeride.sat == ephemeris.sat)
|
||||
if (stored_ephemeris.sat == ephemeris.sat)
|
||||
{
|
||||
d_gps_ephemeride = ephemeris;
|
||||
stored_ephemeris = ephemeris;
|
||||
return;
|
||||
}
|
||||
}
|
||||
d_gps_ephemerides.push_back(ephemeris);
|
||||
d_ephemerides.push_back(ephemeris);
|
||||
}
|
||||
|
||||
void store_base_position(const rtcm_t& decoder, int message_type)
|
||||
{
|
||||
std::array<double, 3> position = {{decoder.sta.pos[0], decoder.sta.pos[1], decoder.sta.pos[2]}};
|
||||
double geodetic_position[3] = {0.0, 0.0, 0.0};
|
||||
double delta_ecef[3] = {0.0, 0.0, 0.0};
|
||||
ecef2pos(position.data(), geodetic_position);
|
||||
if (decoder.sta.deltype != 0)
|
||||
{
|
||||
double antenna_height_enu[3] = {0.0, 0.0, decoder.sta.hgt};
|
||||
enu2ecef(geodetic_position, antenna_height_enu, delta_ecef);
|
||||
for (std::size_t i = 0; i < position.size(); ++i)
|
||||
{
|
||||
position[i] += decoder.sta.del[i] + delta_ecef[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
enu2ecef(geodetic_position, decoder.sta.del, delta_ecef);
|
||||
for (std::size_t i = 0; i < position.size(); ++i)
|
||||
{
|
||||
position[i] += delta_ecef[i];
|
||||
}
|
||||
}
|
||||
std::array<double, 3> position = {{0.0, 0.0, 0.0}};
|
||||
sta2antpos(&decoder.sta, position.data());
|
||||
|
||||
std::lock_guard<std::mutex> lock(d_output_mutex);
|
||||
++d_output_generation;
|
||||
if (d_has_observations && d_observation_station_id != decoder.staid)
|
||||
{
|
||||
invalidate_observations_locked();
|
||||
@@ -1369,10 +1367,12 @@ private:
|
||||
d_observations.clear();
|
||||
d_observation_time = gtime_t{0, 0.0};
|
||||
d_has_observations = false;
|
||||
d_observation_generation = 0;
|
||||
/* a reset is an output change too: never rewind the generation, or a
|
||||
caller could match a stale cached snapshot after restart */
|
||||
++d_output_generation;
|
||||
d_observation_station_id = 0;
|
||||
invalidate_base_position_locked();
|
||||
d_gps_ephemerides.clear();
|
||||
d_ephemerides.clear();
|
||||
}
|
||||
|
||||
void invalidate_observations_locked()
|
||||
@@ -1381,7 +1381,7 @@ private:
|
||||
d_observation_time = gtime_t{0, 0.0};
|
||||
d_has_observations = false;
|
||||
d_observation_station_id = 0;
|
||||
++d_observation_generation;
|
||||
++d_output_generation;
|
||||
}
|
||||
|
||||
void invalidate_base_position_locked()
|
||||
@@ -1434,6 +1434,7 @@ private:
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(d_status_mutex);
|
||||
d_status = Ntrip_Rtcm_Client_Status();
|
||||
d_state_generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void set_state(Ntrip_Rtcm_Client_State state, const std::string& message,
|
||||
@@ -1443,6 +1444,7 @@ private:
|
||||
d_status.state = state;
|
||||
d_status.connected = connected;
|
||||
d_status.message = message;
|
||||
d_state_generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void add_received_bytes(std::uint64_t count)
|
||||
@@ -1487,6 +1489,7 @@ private:
|
||||
|
||||
mutable std::mutex d_status_mutex;
|
||||
Ntrip_Rtcm_Client_Status d_status;
|
||||
std::atomic<std::uint64_t> d_state_generation{0};
|
||||
|
||||
mutable std::mutex d_input_mutex;
|
||||
std::array<double, 3> d_rover_position_ecef_m = {{0.0, 0.0, 0.0}};
|
||||
@@ -1499,7 +1502,7 @@ private:
|
||||
std::vector<obsd_t> d_observations;
|
||||
gtime_t d_observation_time = {0, 0.0};
|
||||
bool d_has_observations = false;
|
||||
std::uint64_t d_observation_generation = 0;
|
||||
std::uint64_t d_output_generation = 0;
|
||||
int d_observation_station_id = 0;
|
||||
std::array<double, 3> d_base_position_ecef_m = {{0.0, 0.0, 0.0}};
|
||||
bool d_has_base_position = false;
|
||||
@@ -1507,7 +1510,7 @@ private:
|
||||
int d_base_position_station_id = 0;
|
||||
int d_base_position_itrf = 0;
|
||||
double d_antenna_height_m = 0.0;
|
||||
std::vector<eph_t> d_gps_ephemerides;
|
||||
std::vector<eph_t> d_ephemerides;
|
||||
};
|
||||
|
||||
|
||||
@@ -1556,6 +1559,18 @@ Ntrip_Rtcm_Snapshot Ntrip_Rtcm_Client::latest_snapshot(const gtime_t& rover_time
|
||||
}
|
||||
|
||||
|
||||
std::uint64_t Ntrip_Rtcm_Client::snapshot_generation() const
|
||||
{
|
||||
return d_impl->snapshot_generation();
|
||||
}
|
||||
|
||||
|
||||
std::uint64_t Ntrip_Rtcm_Client::state_generation() const noexcept
|
||||
{
|
||||
return d_impl->state_generation();
|
||||
}
|
||||
|
||||
|
||||
void Ntrip_Rtcm_Client::update_rover_position(
|
||||
const std::array<double, 3>& position_ecef_m)
|
||||
{
|
||||
|
||||
@@ -32,9 +32,16 @@
|
||||
|
||||
//! Overwrites a credential string before releasing its storage, so the
|
||||
//! secret does not linger in freed heap memory. Single implementation for
|
||||
//! every holder of a Pvt_Conf / NTRIP configuration copy.
|
||||
//! every holder of a Pvt_Conf / NTRIP configuration copy (backed by the
|
||||
//! secure_wipe() primitive in rtklib_stream.h).
|
||||
void secure_clear_string(std::string* value) noexcept;
|
||||
|
||||
//! Scrubs the NTRIP credential fields of a Pvt_Conf copy ahead of its
|
||||
//! destruction (the Secure_String fields wipe themselves on release, but a
|
||||
//! long-lived copy should drop its secrets as soon as they are consumed).
|
||||
//! Single home of the credential field list.
|
||||
void secure_clear_ntrip_credentials(Pvt_Conf* conf) noexcept;
|
||||
|
||||
struct Ntrip_Rtcm_Client_Config
|
||||
{
|
||||
bool enabled = false;
|
||||
@@ -117,7 +124,9 @@ struct Ntrip_Rtcm_Snapshot
|
||||
// value, so a small future-dated base epoch is accepted as well.
|
||||
double age_s = 0.0;
|
||||
bool fresh = false;
|
||||
std::uint64_t observation_generation = 0;
|
||||
// Bumped on every output-side change (observations, base position,
|
||||
// ephemerides, reset), so equal values mean an identical snapshot.
|
||||
std::uint64_t generation = 0;
|
||||
|
||||
bool has_base_position = false;
|
||||
std::array<double, 3> base_position_ecef_m = {{0.0, 0.0, 0.0}};
|
||||
@@ -126,9 +135,10 @@ struct Ntrip_Rtcm_Snapshot
|
||||
int itrf = 0;
|
||||
double antenna_height_m = 0.0;
|
||||
|
||||
// Latest decoded GPS broadcast ephemeris for each satellite seen on the
|
||||
// correction stream. The records own no dynamic memory.
|
||||
std::vector<eph_t> gps_ephemerides;
|
||||
// Latest decoded broadcast ephemeris for each GPS, Galileo, BeiDou or
|
||||
// QZSS satellite seen on the correction stream (RTCM MT1019/1044/1045/
|
||||
// 1046/1042). The records own no dynamic memory.
|
||||
std::vector<eph_t> ephemerides;
|
||||
};
|
||||
|
||||
|
||||
@@ -164,6 +174,12 @@ public:
|
||||
Ntrip_Rtcm_Snapshot latest_snapshot() const;
|
||||
Ntrip_Rtcm_Snapshot latest_snapshot(const gtime_t& rover_time) const;
|
||||
|
||||
// Cheap change detectors so per-epoch callers can skip the deep copies:
|
||||
// snapshot_generation() matches Ntrip_Rtcm_Snapshot::generation, and
|
||||
// state_generation() changes whenever the status state/message is set.
|
||||
std::uint64_t snapshot_generation() const;
|
||||
std::uint64_t state_generation() const noexcept;
|
||||
|
||||
// Position is ECEF in metres. Invalid/non-finite positions disable GGA
|
||||
// until another valid position is supplied.
|
||||
void update_rover_position(const std::array<double, 3>& position_ecef_m);
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*!
|
||||
* \file ntrip_rtk_signals.h
|
||||
* \brief Signal table of the NTRIP RTK path
|
||||
* \author Carles Fernandez-Prades, 2026. cfernandez(at)cttc.es
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
||||
* This file is part of GNSS-SDR.
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2026 (see AUTHORS file for a list of contributors)
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#ifndef GNSS_SDR_NTRIP_RTK_SIGNALS_H
|
||||
#define GNSS_SDR_NTRIP_RTK_SIGNALS_H
|
||||
|
||||
#include "rtklib.h"
|
||||
#include "signal_enabled_flags.h"
|
||||
#include "signal_flag.h"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
/** \addtogroup PVT
|
||||
* \{ */
|
||||
/** \addtogroup PVT_libs
|
||||
* \{ */
|
||||
|
||||
/*!
|
||||
* Single home for the signal knowledge of the NTRIP RTK path: which tracked
|
||||
* signals it supports and, for each one, the constellation and the RTKLIB
|
||||
* frequency slot it occupies. The adapter's channel-set whitelist, its
|
||||
* derived slot count and navigation-system mask, and the solver's per-system
|
||||
* band mapping all derive from this table, so admitting a new signal (e.g.
|
||||
* BeiDou B3I or QZSS) starts with a one-row change here.
|
||||
*/
|
||||
struct Ntrip_Rtk_Signal
|
||||
{
|
||||
uint32_t flag; //!< signal_flag bit of the tracked channel signal
|
||||
int system; //!< RTKLIB constellation bit (SYS_XXX)
|
||||
int band_slot; //!< RTKLIB frequency slot the signal occupies (0-based)
|
||||
bool first_band; //!< entry band of its constellation (defines the navsys mask)
|
||||
};
|
||||
|
||||
|
||||
/*!
|
||||
* Keep each constellation's second bands ordered by preference: with more
|
||||
* than one enabled (a configuration the adapter rejects today),
|
||||
* ntrip_rtk_second_band_slot() picks the first match.
|
||||
*/
|
||||
inline constexpr std::array<Ntrip_Rtk_Signal, 6> NTRIP_RTK_SIGNALS = {{
|
||||
{GPS_1C, SYS_GPS, 0, true}, // GPS L1 C/A
|
||||
{GPS_2S, SYS_GPS, 1, false}, // GPS L2C
|
||||
{GPS_L5, SYS_GPS, 2, false}, // GPS L5 (shares the third slot with E5a)
|
||||
{GAL_1B, SYS_GAL, 0, true}, // Galileo E1
|
||||
{GAL_E5a, SYS_GAL, 2, false}, // Galileo E5a
|
||||
{BDS_B1C, SYS_BDS, 0, true}, // BeiDou B1C
|
||||
}};
|
||||
|
||||
|
||||
//! Bitmask of every signal_flag the NTRIP RTK path supports
|
||||
inline constexpr uint32_t ntrip_rtk_supported_signal_mask()
|
||||
{
|
||||
uint32_t mask = 0;
|
||||
for (const auto& signal : NTRIP_RTK_SIGNALS)
|
||||
{
|
||||
mask |= signal.flag;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
|
||||
//! RTKLIB navigation-system mask spanned by the enabled NTRIP RTK channels
|
||||
inline int ntrip_rtk_navigation_systems(const Signal_Enabled_Flags& enabled)
|
||||
{
|
||||
int navsys = 0;
|
||||
for (const auto& signal : NTRIP_RTK_SIGNALS)
|
||||
{
|
||||
if (signal.first_band && enabled.check_any_enabled(signal.flag))
|
||||
{
|
||||
navsys |= signal.system;
|
||||
}
|
||||
}
|
||||
return navsys;
|
||||
}
|
||||
|
||||
|
||||
//! Number of RTKLIB frequency slots the enabled NTRIP RTK channels require
|
||||
inline int ntrip_rtk_required_bands(const Signal_Enabled_Flags& enabled)
|
||||
{
|
||||
int bands = 1;
|
||||
for (const auto& signal : NTRIP_RTK_SIGNALS)
|
||||
{
|
||||
if (enabled.check_any_enabled(signal.flag) && signal.band_slot + 1 > bands)
|
||||
{
|
||||
bands = signal.band_slot + 1;
|
||||
}
|
||||
}
|
||||
return bands;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* RTKLIB slot of a satellite system's second band given the enabled channels.
|
||||
* QZSS rides the GPS bands. When no second band of the system is enabled, the
|
||||
* constellation's canonical second slot is returned (Galileo/BeiDou: 2, both
|
||||
* anticipate slot-2 signals; GPS/QZSS: 1) so unsupported slots can be cleared.
|
||||
*/
|
||||
inline int ntrip_rtk_second_band_slot(int system, const Signal_Enabled_Flags& enabled)
|
||||
{
|
||||
const int lookup_system = (system == SYS_QZS) ? SYS_GPS : system;
|
||||
for (const auto& signal : NTRIP_RTK_SIGNALS)
|
||||
{
|
||||
if (signal.system == lookup_system && !signal.first_band &&
|
||||
enabled.check_any_enabled(signal.flag))
|
||||
{
|
||||
return signal.band_slot;
|
||||
}
|
||||
}
|
||||
return (lookup_system == SYS_GAL || lookup_system == SYS_BDS) ? 2 : 1;
|
||||
}
|
||||
|
||||
|
||||
/** \} */
|
||||
/** \} */
|
||||
#endif // GNSS_SDR_NTRIP_RTK_SIGNALS_H
|
||||
@@ -17,6 +17,7 @@
|
||||
#ifndef GNSS_SDR_PVT_CONF_H
|
||||
#define GNSS_SDR_PVT_CONF_H
|
||||
|
||||
#include "secure_string.h"
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -53,9 +54,13 @@ public:
|
||||
std::string log_source_timetag_file;
|
||||
std::string ntrip_caster_address;
|
||||
std::string ntrip_mountpoint;
|
||||
std::string ntrip_username;
|
||||
std::string ntrip_password;
|
||||
std::string ntrip_password_env;
|
||||
// Self-scrubbing: every copy of this struct wipes the caster credentials
|
||||
// on release, so new holders need no manual scrub site (the explicit
|
||||
// early scrubs via secure_clear_ntrip_credentials() remain worthwhile
|
||||
// for long-lived copies)
|
||||
Secure_String ntrip_username;
|
||||
Secure_String ntrip_password;
|
||||
Secure_String ntrip_password_env;
|
||||
|
||||
uint32_t signal_enabled_flags = 0;
|
||||
uint32_t observable_interval_ms = 20;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "gnss_sdr_filesystem.h"
|
||||
#include "matlab_writter_helper.h"
|
||||
#include "ntrip_rtcm_client.h"
|
||||
#include "ntrip_rtk_signals.h"
|
||||
#include "rtklib_rtkpos.h"
|
||||
#include "signal_enabled_flags.h"
|
||||
#include <algorithm>
|
||||
@@ -69,9 +70,7 @@ Rtklib_Solver::Rtklib_Solver(const rtk_t &rtk,
|
||||
{
|
||||
// Solver instances need correction policy but never caster credentials.
|
||||
// Remove secrets from this long-lived configuration copy immediately.
|
||||
secure_clear_string(&d_conf.ntrip_username);
|
||||
secure_clear_string(&d_conf.ntrip_password);
|
||||
secure_clear_string(&d_conf.ntrip_password_env);
|
||||
secure_clear_ntrip_credentials(&d_conf);
|
||||
|
||||
// see freq index at src/algorithms/libs/rtklib/rtklib_rtkcmn.cc
|
||||
// function: satwavelen
|
||||
@@ -1473,18 +1472,11 @@ bool Rtklib_Solver::prepare_fixed_base_observations(const Ntrip_Rtcm_Snapshot &f
|
||||
int &rover_observation_count,
|
||||
int &base_observation_count)
|
||||
{
|
||||
// The dual-band pair sits in slots 0/2 for Galileo (E1/E5a shares
|
||||
// RTKLIB's L5 frequency index) and, for GPS, in slots 0/1 (L1/L2) or
|
||||
// slots 0/2 (L1/L5) depending on the configured channel pair
|
||||
// Per-constellation band-to-slot mapping, shared with the adapter's
|
||||
// channel-set validation (see NTRIP_RTK_SIGNALS in ntrip_rtk_signals.h)
|
||||
const Signal_Enabled_Flags enabled_flags(d_signal_enabled_flags);
|
||||
const int gps_second_band_slot = (!enabled_flags.check_any_enabled(GPS_2S) &&
|
||||
enabled_flags.check_any_enabled(GPS_L5))
|
||||
? 2
|
||||
: 1;
|
||||
const auto second_band_slot = [gps_second_band_slot](const obsd_t &observation) {
|
||||
// Galileo E5a and a future BeiDou B3 both live in slot 2
|
||||
const int system = satsys(observation.sat, nullptr);
|
||||
return (system == SYS_GAL || system == SYS_BDS) ? 2 : gps_second_band_slot;
|
||||
const auto second_band_slot = [&enabled_flags](const obsd_t &observation) {
|
||||
return ntrip_rtk_second_band_slot(satsys(observation.sat, nullptr), enabled_flags);
|
||||
};
|
||||
const auto clear_unsupported_slots = [&second_band_slot](obsd_t &observation) {
|
||||
const int second = second_band_slot(observation);
|
||||
@@ -1533,29 +1525,39 @@ bool Rtklib_Solver::prepare_fixed_base_observations(const Ntrip_Rtcm_Snapshot &f
|
||||
std::sort(observations.begin(), observations.end(), [](const obsd_t &left, const obsd_t &right) {
|
||||
return left.sat < right.sat;
|
||||
});
|
||||
std::vector<obsd_t> merged;
|
||||
merged.reserve(observations.size());
|
||||
for (const auto &observation : observations)
|
||||
/* 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 (!merged.empty() && merged.back().sat == observation.sat)
|
||||
if (out > 0 && observations[out - 1].sat == observations[in].sat)
|
||||
{
|
||||
merge_observation(merged.back(), observation);
|
||||
merge_observation(observations[out - 1], observations[in]);
|
||||
}
|
||||
else
|
||||
{
|
||||
merged.push_back(observation);
|
||||
if (out != in)
|
||||
{
|
||||
observations[out] = observations[in];
|
||||
}
|
||||
++out;
|
||||
}
|
||||
}
|
||||
observations.swap(merged);
|
||||
observations.resize(out);
|
||||
};
|
||||
|
||||
std::vector<obsd_t> rover_observations;
|
||||
/* 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];
|
||||
/* QZSS shares the GPS band slots and has its own AR group in the
|
||||
engine, so its rover observations enter the relative solution */
|
||||
if (observation.sat <= 0 ||
|
||||
(satsys(observation.sat, nullptr) != SYS_GPS &&
|
||||
satsys(observation.sat, nullptr) != SYS_QZS &&
|
||||
satsys(observation.sat, nullptr) != SYS_GAL &&
|
||||
satsys(observation.sat, nullptr) != SYS_BDS))
|
||||
{
|
||||
@@ -1573,10 +1575,11 @@ bool Rtklib_Solver::prepare_fixed_base_observations(const Ntrip_Rtcm_Snapshot &f
|
||||
{
|
||||
rover_observations.resize(MAXOBS);
|
||||
}
|
||||
rover_observation_count = static_cast<int>(rover_observations.size());
|
||||
base_observation_count = 0;
|
||||
d_obs_data.fill({});
|
||||
std::copy(rover_observations.cbegin(), rover_observations.cend(), d_obs_data.begin());
|
||||
/* 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())
|
||||
{
|
||||
@@ -1612,7 +1615,8 @@ bool Rtklib_Solver::prepare_fixed_base_observations(const Ntrip_Rtcm_Snapshot &f
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<obsd_t> base_observations;
|
||||
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)
|
||||
{
|
||||
@@ -1642,7 +1646,7 @@ bool Rtklib_Solver::prepare_fixed_base_observations(const Ntrip_Rtcm_Snapshot &f
|
||||
base_observations.resize(MAXOBS);
|
||||
}
|
||||
d_fixed_base_common_satellites = base_observations.size();
|
||||
if (d_fixed_base_common_satellites < 4)
|
||||
if (d_fixed_base_common_satellites < NTRIP_MIN_COMMON_SATELLITES)
|
||||
{
|
||||
d_fixed_base_status = Rtklib_Fixed_Base_Status::INSUFFICIENT_COMMON_SATELLITES;
|
||||
return false;
|
||||
@@ -1673,13 +1677,44 @@ bool Rtklib_Solver::prepare_fixed_base_observations(const Ntrip_Rtcm_Snapshot &f
|
||||
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;
|
||||
}
|
||||
for (const auto &candidate : fixed_base->ephemerides)
|
||||
{
|
||||
if (candidate.sat == sat)
|
||||
{
|
||||
return &candidate;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &gnss_observables_map,
|
||||
double kf_update_interval_s,
|
||||
const SensorDataAggregator &sensor_data_aggregator,
|
||||
@@ -1725,8 +1760,8 @@ bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &gnss_observables_
|
||||
const bool has_selected_galileo_ephemeris = select_galileo_ephemeris(
|
||||
gnss_observables_iter->second.PRN, sig_, obs_tow,
|
||||
selected_galileo_ephemeris, selected_from_reduced_ced);
|
||||
if (!has_selected_galileo_ephemeris ||
|
||||
(!selected_from_reduced_ced && has_active_has_do_not_use(gal_str, prn, obs_tow)))
|
||||
if (has_selected_galileo_ephemeris &&
|
||||
!selected_from_reduced_ced && has_active_has_do_not_use(gal_str, prn, obs_tow))
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -1760,6 +1795,31 @@ bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &gnss_observables_
|
||||
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
|
||||
valid_obs++;
|
||||
}
|
||||
else if (fixed_base != nullptr)
|
||||
{
|
||||
// the rover has not decoded this satellite's I/NAV yet, but
|
||||
// the base stream may have delivered its broadcast ephemeris
|
||||
// over NTRIP (RTCM MT1045/MT1046): use it so the satellite
|
||||
// contributes without waiting for the page decoding. E5a
|
||||
// joins once the rover ephemeris is available.
|
||||
const eph_t *base_ephemeris = find_base_ephemeris(fixed_base, satno(SYS_GAL, prn));
|
||||
if (base_ephemeris != nullptr)
|
||||
{
|
||||
int gst_week = 0;
|
||||
time2gst(base_ephemeris->toe, &gst_week);
|
||||
eph_data[valid_obs] = *base_ephemeris;
|
||||
obsd_t newobs{};
|
||||
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
|
||||
gnss_observables_iter->second,
|
||||
gst_week,
|
||||
d_rtklib_band_index.at(sig_));
|
||||
valid_obs++;
|
||||
}
|
||||
else
|
||||
{
|
||||
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
|
||||
}
|
||||
}
|
||||
else // the ephemeris are not available for this SV
|
||||
{
|
||||
DLOG(INFO) << "No ephemeris data for SV " << gnss_observables_iter->second.PRN;
|
||||
@@ -1932,22 +1992,14 @@ bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &gnss_observables_
|
||||
clear_applied_has_phase_bias_discontinuity(applied_has_correction, prn);
|
||||
valid_obs++;
|
||||
}
|
||||
else if (!is_qzss && fixed_base != nullptr)
|
||||
else if (fixed_base != nullptr)
|
||||
{
|
||||
// the rover has not decoded this satellite's LNAV yet, but
|
||||
// the base stream may have delivered its broadcast ephemeris
|
||||
// over NTRIP (RTCM MT1019, already in RTKLIB format): use it
|
||||
// so the satellite contributes without waiting the ~30 s of
|
||||
// subframe decoding
|
||||
const eph_t *base_ephemeris = nullptr;
|
||||
for (const auto &candidate : fixed_base->gps_ephemerides)
|
||||
{
|
||||
if (candidate.sat == sat)
|
||||
{
|
||||
base_ephemeris = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// over NTRIP (RTCM MT1019/MT1044, already in RTKLIB format):
|
||||
// use it so the satellite contributes without waiting the
|
||||
// ~30 s of subframe decoding
|
||||
const eph_t *base_ephemeris = find_base_ephemeris(fixed_base, sat);
|
||||
if (base_ephemeris != nullptr)
|
||||
{
|
||||
eph_data[valid_obs] = *base_ephemeris;
|
||||
@@ -2160,6 +2212,29 @@ bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &gnss_observables_
|
||||
d_rtklib_band_index.at(sig_));
|
||||
valid_obs++;
|
||||
}
|
||||
else if (fixed_base != nullptr)
|
||||
{
|
||||
// base-stream broadcast ephemeris substitution (RTCM
|
||||
// MT1042; DNAV orbit, fine for satellite positions while
|
||||
// the rover B-CNAV1 decode completes)
|
||||
const eph_t *base_ephemeris = find_base_ephemeris(fixed_base, bds_sat);
|
||||
if (base_ephemeris != nullptr)
|
||||
{
|
||||
int gps_week = 0;
|
||||
time2gpst(base_ephemeris->toe, &gps_week);
|
||||
eph_data[valid_obs] = *base_ephemeris;
|
||||
obsd_t newobs{};
|
||||
d_obs_data[valid_obs + glo_valid_obs] = insert_obs_to_rtklib(newobs,
|
||||
gnss_observables_iter->second,
|
||||
gps_week,
|
||||
d_rtklib_band_index.at(sig_));
|
||||
valid_obs++;
|
||||
}
|
||||
else
|
||||
{
|
||||
DLOG(INFO) << "No B-CNAV1 ephemeris data for SV " << gnss_observables_iter->second.PRN;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DLOG(INFO) << "No B-CNAV1 ephemeris data for SV " << gnss_observables_iter->second.PRN;
|
||||
@@ -2597,6 +2672,17 @@ bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &gnss_observables_
|
||||
}
|
||||
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
|
||||
double ground_speed_ms = 0.0;
|
||||
std::array<double, 3> pos{};
|
||||
|
||||
@@ -112,6 +112,10 @@ enum class Rtklib_Fixed_Base_Status
|
||||
class Rtklib_Solver : public Pvt_Solution
|
||||
{
|
||||
public:
|
||||
//! Minimum satellites common to rover and base (any constellation) for
|
||||
//! the fixed-base merge; the solver-owned policy the display layer quotes
|
||||
static constexpr int NTRIP_MIN_COMMON_SATELLITES = 4;
|
||||
|
||||
Rtklib_Solver(const rtk_t& rtk,
|
||||
const Pvt_Conf& conf,
|
||||
const std::string& dump_filename,
|
||||
@@ -224,6 +228,10 @@ private:
|
||||
void reset_relative_filter();
|
||||
|
||||
std::array<obsd_t, MAXOBS * 2> d_obs_data{};
|
||||
// per-epoch scratch of prepare_fixed_base_observations(); members so
|
||||
// their capacity is reused across epochs
|
||||
std::vector<obsd_t> d_fixed_base_rover_scratch;
|
||||
std::vector<obsd_t> d_fixed_base_base_scratch;
|
||||
std::array<double, 4> d_dop{};
|
||||
std::map<int, int> d_rtklib_freq_index;
|
||||
std::map<std::string, int> d_rtklib_band_index;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*!
|
||||
* \file secure_string.h
|
||||
* \brief Self-scrubbing value type for credential-bearing configuration fields
|
||||
* \author Carles Fernandez-Prades, 2026. cfernandez(at)cttc.es
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
||||
* This file is part of GNSS-SDR.
|
||||
*
|
||||
* Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors)
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#ifndef GNSS_SDR_SECURE_STRING_H
|
||||
#define GNSS_SDR_SECURE_STRING_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/** \addtogroup PVT
|
||||
* \{ */
|
||||
/** \addtogroup PVT_libs
|
||||
* \{ */
|
||||
|
||||
/* Single wipe primitive of the code base, defined in rtklib_stream.cc. It is
|
||||
* declared here directly so this header stays free of the heavy rtklib
|
||||
* includes; the compiler rejects any signature divergence in translation
|
||||
* units that also see rtklib_stream.h. */
|
||||
void secure_wipe(void* data, size_t size);
|
||||
|
||||
/*!
|
||||
* \brief Value type for credential-bearing configuration fields.
|
||||
*
|
||||
* Wipes its storage with volatile stores before every release - destruction,
|
||||
* assignment, clear() - so every copy of a configuration struct that carries
|
||||
* one scrubs itself, and a new holder of such a copy needs no manual scrub
|
||||
* site. Assign-once usage is assumed: in-place growth of the underlying
|
||||
* std::string is not protected.
|
||||
*/
|
||||
class Secure_String
|
||||
{
|
||||
public:
|
||||
Secure_String() = default;
|
||||
|
||||
// Implicit by design: a drop-in for std::string configuration fields
|
||||
Secure_String(std::string value) : d_value(std::move(value)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
|
||||
Secure_String(const char* value) : d_value(value) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
|
||||
|
||||
Secure_String(const Secure_String& other) = default;
|
||||
|
||||
Secure_String(Secure_String&& other) noexcept : d_value(std::move(other.d_value))
|
||||
{
|
||||
other.clear();
|
||||
}
|
||||
|
||||
Secure_String& operator=(const Secure_String& other)
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
clear();
|
||||
d_value = other.d_value;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Secure_String& operator=(Secure_String&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
clear();
|
||||
d_value = std::move(other.d_value);
|
||||
other.clear();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Secure_String()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
void clear() noexcept
|
||||
{
|
||||
if (!d_value.empty())
|
||||
{
|
||||
secure_wipe(&d_value[0], d_value.size());
|
||||
}
|
||||
d_value.clear();
|
||||
}
|
||||
|
||||
const std::string& value() const noexcept
|
||||
{
|
||||
return d_value;
|
||||
}
|
||||
|
||||
bool empty() const noexcept
|
||||
{
|
||||
return d_value.empty();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string d_value;
|
||||
};
|
||||
|
||||
|
||||
/** \} */
|
||||
/** \} */
|
||||
#endif // GNSS_SDR_SECURE_STRING_H
|
||||
@@ -67,19 +67,25 @@ else()
|
||||
)
|
||||
endif()
|
||||
|
||||
# Threads::Threads is PUBLIC because rtklib.h includes <pthread.h> and exposes
|
||||
# pthread_t/pthread_mutex_t members and the lock_t/rtk_lock macros to every
|
||||
# consumer, which therefore needs the -pthread compile usage requirement too.
|
||||
target_link_libraries(algorithms_libs_rtklib
|
||||
PUBLIC
|
||||
Threads::Threads
|
||||
PRIVATE
|
||||
core_system_parameters
|
||||
algorithms_libs
|
||||
Armadillo::armadillo
|
||||
LAPACK::LAPACK
|
||||
BLAS::BLAS
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
# Use the same OpenSSL-preferred/GnuTLS-fallback selection as the rest of
|
||||
# GNSS-SDR. This also propagates USE_GNUTLS_FALLBACK when appropriate.
|
||||
link_to_crypto_dependencies(algorithms_libs_rtklib)
|
||||
# GNSS-SDR. PRIVATE: the crypto backend is used only in rtklib_tls.cc and no
|
||||
# public header of this target exposes it, so consumers must not inherit the
|
||||
# backend's include directories or USE_* definitions.
|
||||
link_to_crypto_dependencies(algorithms_libs_rtklib PRIVATE)
|
||||
|
||||
if(ENABLE_GLOG_AND_GFLAGS)
|
||||
target_link_libraries(algorithms_libs_rtklib PRIVATE Gflags::gflags Glog::glog)
|
||||
|
||||
@@ -300,6 +300,31 @@ const double EFACT_BDS = 1.0; //!< error factor: BeiDou
|
||||
const double EFACT_IRN = 1.5; //!< error factor: IRNSS
|
||||
const double EFACT_SBS = 3.0; //!< error factor: SBAS
|
||||
|
||||
//! Measurement error factor of a navigation system (single home of the
|
||||
//! sys-to-EFACT mapping used by the single-point, relative and PPP error
|
||||
//! models)
|
||||
inline double sysefact(int sys)
|
||||
{
|
||||
switch (sys)
|
||||
{
|
||||
case SYS_GLO:
|
||||
return EFACT_GLO;
|
||||
case SYS_GAL:
|
||||
return EFACT_GAL;
|
||||
case SYS_SBS:
|
||||
return EFACT_SBS;
|
||||
case SYS_QZS:
|
||||
return EFACT_QZS;
|
||||
case SYS_BDS:
|
||||
return EFACT_BDS;
|
||||
case SYS_IRN:
|
||||
return EFACT_IRN;
|
||||
case SYS_GPS:
|
||||
default:
|
||||
return EFACT_GPS;
|
||||
}
|
||||
}
|
||||
|
||||
const int MAXEXFILE = 1024; //!< max number of expanded files
|
||||
const double MAXSBSAGEF = 30.0; //!< max age of SBAS fast correction (s)
|
||||
const double MAXSBSAGEL = 1800.0; //!< max age of SBAS long term corr (s)
|
||||
@@ -1024,7 +1049,11 @@ typedef struct
|
||||
degraded-visibility scenarios */
|
||||
/* AR management options from the demo5 RTKLIB fork. Value-initialized (all zeros)
|
||||
they reproduce the classic behavior: no AR filtering, no minimum-satellite gates,
|
||||
no satellite exclusion cycling, no position-variance gate */
|
||||
no satellite exclusion cycling, no position-variance gate. PRCOPT_DEFAULT keeps
|
||||
them at zero; note that the GNSS-SDR PVT adapter enables the demo5 defaults
|
||||
instead (PVT.ar_filter=true, min_fix_sats=4, min_hold_sats=5, min_drop_sats=10,
|
||||
ar_max_position_variance=0.25) - set those configuration options to zero/false
|
||||
to recover the classic behavior */
|
||||
int arfilter; /* AR filtering to reject newly-added sats on AR ratio degradation (0:off,1:on) */
|
||||
int minfixsats; /* min number of sats to fix integer ambiguities (0:no limit) */
|
||||
int minholdsats; /* min number of sats to hold integer ambiguities (0:no limit) */
|
||||
|
||||
@@ -100,33 +100,9 @@ static int used_frequency_slot(const obsd_t *obs)
|
||||
|
||||
double varerr(const prcopt_t *opt, const obsd_t *obs, double el, int sys)
|
||||
{
|
||||
double fact = 1.0;
|
||||
double fact = sysefact(sys);
|
||||
double varr;
|
||||
|
||||
switch (sys)
|
||||
{
|
||||
case SYS_GPS:
|
||||
fact *= EFACT_GPS;
|
||||
break;
|
||||
case SYS_GLO:
|
||||
fact *= EFACT_GLO;
|
||||
break;
|
||||
case SYS_GAL:
|
||||
fact *= EFACT_GAL;
|
||||
break;
|
||||
case SYS_SBS:
|
||||
fact *= EFACT_SBS;
|
||||
break;
|
||||
case SYS_QZS:
|
||||
fact *= EFACT_QZS;
|
||||
break;
|
||||
case SYS_BDS:
|
||||
fact *= EFACT_BDS;
|
||||
break;
|
||||
default:
|
||||
fact *= EFACT_GPS;
|
||||
break;
|
||||
}
|
||||
if (el < VARERR_MIN_EL)
|
||||
{
|
||||
el = VARERR_MIN_EL;
|
||||
@@ -341,8 +317,12 @@ double prange(const obsd_t *obs, const nav_t *nav, const double *azel,
|
||||
}
|
||||
else if (obs->code[2] != CODE_NONE)
|
||||
{
|
||||
i = 2;
|
||||
j = 2; /* B3I-only */
|
||||
/* B3I-only: same index pattern as L5-only (empty first
|
||||
band), so the second-band branch applies the B3I rules -
|
||||
no TGD (DNAV timing reference) and the broadcast iono,
|
||||
modeled at B1I, scaled by gamma13 via iono_scale */
|
||||
i = 0;
|
||||
j = 2;
|
||||
}
|
||||
}
|
||||
else if (sys == SYS_GPS || sys == SYS_GLO || sys == SYS_QZS)
|
||||
|
||||
@@ -918,7 +918,7 @@ double varerr(int sat __attribute__((unused)), int sys, double el, int type, con
|
||||
{
|
||||
fact *= opt->eratio[0];
|
||||
}
|
||||
fact *= sys == SYS_GLO ? EFACT_GLO : (sys == SYS_SBS ? EFACT_SBS : EFACT_GPS);
|
||||
fact *= sysefact(sys);
|
||||
if (opt->ionoopt == IONOOPT_IFLC)
|
||||
{
|
||||
fact *= 3.0;
|
||||
|
||||
@@ -2353,6 +2353,48 @@ void enu2ecef(const double *pos, const double *e, double *r)
|
||||
}
|
||||
|
||||
|
||||
/* antenna phase-center position of a station record ---------------------------
|
||||
* apply the antenna delta of an RTCM 1005/1006 station record to its stored
|
||||
* reference-point position. Single home of this geodesy: an inconsistency
|
||||
* between consumers would directly bias the RTK baseline
|
||||
* args : sta_t *sta I station parameters (pos, del, deltype, hgt)
|
||||
* double *rr O antenna phase-center position ecef (m)
|
||||
* notes : deltype 0: del is an e/n/u offset;
|
||||
* deltype 1: del is an ecef x/y/z offset and hgt an additional up
|
||||
* offset
|
||||
*-----------------------------------------------------------------------------*/
|
||||
void sta2antpos(const sta_t *sta, double *rr)
|
||||
{
|
||||
double pos[3];
|
||||
double del[3] = {0.0, 0.0, 0.0};
|
||||
double dr[3];
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
rr[i] = sta->pos[i];
|
||||
}
|
||||
ecef2pos(rr, pos);
|
||||
if (sta->deltype)
|
||||
{ /* xyz + height */
|
||||
del[2] = sta->hgt;
|
||||
enu2ecef(pos, del, dr);
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
rr[i] += sta->del[i] + dr[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{ /* enu */
|
||||
enu2ecef(pos, sta->del, dr);
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
rr[i] += dr[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* transform covariance to local tangental coordinate --------------------------
|
||||
* transform ecef covariance to local tangental coordinate
|
||||
* args : double *pos I geodetic position {lat, lon} (rad)
|
||||
|
||||
@@ -199,6 +199,8 @@ void pos2ecef(const double *pos, double *r);
|
||||
void xyz2enu(const double *pos, double *E);
|
||||
void ecef2enu(const double *pos, const double *r, double *e);
|
||||
void enu2ecef(const double *pos, const double *e, double *r);
|
||||
|
||||
void sta2antpos(const sta_t *sta, double *rr);
|
||||
void covenu(const double *pos, const double *P, double *Q);
|
||||
void covecef(const double *pos, const double *Q, double *P);
|
||||
void ast_args(double t, double *f);
|
||||
|
||||
@@ -617,30 +617,7 @@ double varerr(int sat __attribute((unused)), int sys, double el,
|
||||
fact = opt->eratio[0];
|
||||
}
|
||||
/* adjust variance for constellation */
|
||||
switch (sys)
|
||||
{
|
||||
case SYS_GPS:
|
||||
fact *= EFACT_GPS;
|
||||
break;
|
||||
case SYS_GLO:
|
||||
fact *= EFACT_GLO;
|
||||
break;
|
||||
case SYS_GAL:
|
||||
fact *= EFACT_GAL;
|
||||
break;
|
||||
case SYS_SBS:
|
||||
fact *= EFACT_SBS;
|
||||
break;
|
||||
case SYS_QZS:
|
||||
fact *= EFACT_QZS;
|
||||
break;
|
||||
case SYS_BDS:
|
||||
fact *= EFACT_BDS;
|
||||
break;
|
||||
default:
|
||||
fact *= EFACT_GPS;
|
||||
break;
|
||||
}
|
||||
fact *= sysefact(sys);
|
||||
a = fact * opt->err[1];
|
||||
b = fact * opt->err[2];
|
||||
}
|
||||
@@ -1176,7 +1153,11 @@ void detslp_dop(rtk_t *rtk, const obsd_t *obs, const int *ix, int ns, int rcv,
|
||||
sat = obs[ix[i]].sat;
|
||||
for (f = 0; f < nf; f++)
|
||||
{
|
||||
if (dt[i][f] == 0.0)
|
||||
/* skip entries whose difference was never computed (guards
|
||||
above): dt alone is set before the DTTOL and wavelength
|
||||
checks, so testing it would evaluate those entries as
|
||||
dopdif = 0 against the median (demo5 tests dopdif) */
|
||||
if (dopdif[i][f] == 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1813,10 +1794,16 @@ int ddres(rtk_t *rtk, const obsd_t *obs, const nav_t *nav, double dt, const doub
|
||||
{
|
||||
frq = f % nf;
|
||||
|
||||
/* choose the minimum-variance satellite without a slip as
|
||||
the reference (demo5: better than highest elevation in
|
||||
urban conditions, where SNR is the better quality proxy) */
|
||||
/* choose the minimum-variance satellite as the reference
|
||||
(demo5: better than highest elevation in urban
|
||||
conditions, where SNR is the better quality proxy),
|
||||
preferring one without a slip and with a valid lock;
|
||||
one pass tracks the best clean and the best overall
|
||||
candidate, and the overall one is used only if no
|
||||
clean reference exists */
|
||||
minvar = 0.0;
|
||||
double minvar_any = 0.0;
|
||||
int ref_any = -1;
|
||||
for (i = -1, j = 0; j < ns; j++)
|
||||
{
|
||||
sysj = rtk->ssat[sat[j] - 1].sys;
|
||||
@@ -1828,47 +1815,29 @@ int ddres(rtk_t *rtk, const obsd_t *obs, const nav_t *nav, double dt, const doub
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (rtk->ssat[sat[j] - 1].slip[frq] & 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (rtk->ssat[sat[j] - 1].lock[frq] < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
refvar = varerr(sat[j], sysj, azel[1 + iu[j] * 2],
|
||||
rtk->ssat[sat[j] - 1].snr_rover[frq], rtk->ssat[sat[j] - 1].snr_base[frq],
|
||||
bl, dt, f, opt, &obs[iu[j]]);
|
||||
if (ref_any < 0 || refvar < minvar_any)
|
||||
{
|
||||
ref_any = j;
|
||||
minvar_any = refvar;
|
||||
}
|
||||
if ((rtk->ssat[sat[j] - 1].slip[frq] & 1) ||
|
||||
rtk->ssat[sat[j] - 1].lock[frq] < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (i < 0 || refvar < minvar)
|
||||
{
|
||||
i = j;
|
||||
minvar = refvar;
|
||||
}
|
||||
}
|
||||
/* fall back to a slipped satellite only if no clean reference exists */
|
||||
if (i < 0)
|
||||
{
|
||||
minvar = 0.0;
|
||||
for (j = 0; j < ns; j++)
|
||||
{
|
||||
sysj = rtk->ssat[sat[j] - 1].sys;
|
||||
if (!test_sys(sysj, m) || sysj == SYS_SBS)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!validobs(iu[j], ir[j], f, nf, y))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
refvar = varerr(sat[j], sysj, azel[1 + iu[j] * 2],
|
||||
rtk->ssat[sat[j] - 1].snr_rover[frq], rtk->ssat[sat[j] - 1].snr_base[frq],
|
||||
bl, dt, f, opt, &obs[iu[j]]);
|
||||
if (i < 0 || refvar < minvar)
|
||||
{
|
||||
i = j;
|
||||
minvar = refvar;
|
||||
}
|
||||
}
|
||||
{ /* fall back to a slipped/unlocked reference */
|
||||
i = ref_any;
|
||||
minvar = minvar_any;
|
||||
}
|
||||
if (i < 0)
|
||||
{
|
||||
@@ -2022,10 +1991,10 @@ int ddres(rtk_t *rtk, const obsd_t *obs, const nav_t *nav, double dt, const doub
|
||||
sat[i], sat[j], f < nf ? "L" : "P", f % nf + 1, v[nv]);
|
||||
continue;
|
||||
}
|
||||
/* single-differenced measurement error variances */
|
||||
Ri[nv] = varerr(sat[i], sysi, azel[1 + iu[i] * 2],
|
||||
rtk->ssat[sat[i] - 1].snr_rover[frq], rtk->ssat[sat[i] - 1].snr_base[frq],
|
||||
bl, dt, f, opt, &obs[iu[i]]);
|
||||
/* single-differenced measurement error variances;
|
||||
the reference variance was already computed by
|
||||
the selection loop and is invariant in j */
|
||||
Ri[nv] = minvar;
|
||||
Rj[nv] = varerr(sat[j], sysj, azel[1 + iu[j] * 2],
|
||||
rtk->ssat[sat[j] - 1].snr_rover[frq], rtk->ssat[sat[j] - 1].snr_base[frq],
|
||||
bl, dt, f, opt, &obs[iu[j]]);
|
||||
@@ -2196,7 +2165,7 @@ double intpres(gtime_t time, const obsd_t *obs, int n, const nav_t *nav,
|
||||
/* single to double-difference transformation matrix (D') --------------------*/
|
||||
/* index of single-to-double-difference transformation (demo5/RTKLIB 2.4.3 style):
|
||||
returns pairs of phase-bias state indices {ref, target} instead of a D matrix */
|
||||
int ddidx(rtk_t *rtk, int *ix, int gps, int glo, int sbs)
|
||||
int ddidx(rtk_t *rtk, int *ix, int glo, int sbs)
|
||||
{
|
||||
int i;
|
||||
int j;
|
||||
@@ -2209,7 +2178,7 @@ int ddidx(rtk_t *rtk, int *ix, int gps, int glo, int sbs)
|
||||
int nf = NF_RTK(&rtk->opt);
|
||||
int nofix;
|
||||
|
||||
trace(3, "ddidx: gps=%d glo=%d/%d sbs=%d\n", gps, glo, rtk->opt.glomodear, sbs);
|
||||
trace(3, "ddidx: glo=%d/%d sbs=%d\n", glo, rtk->opt.glomodear, sbs);
|
||||
|
||||
/* clear fix flag for all sats (1=float, 2=fix) */
|
||||
for (i = 0; i < MAXSAT; i++)
|
||||
@@ -2221,8 +2190,10 @@ int ddidx(rtk_t *rtk, int *ix, int gps, int glo, int sbs)
|
||||
}
|
||||
for (m = 0; m < 5; m++)
|
||||
{ /* m=0:gps/sbs, 1:glo, 2:gal, 3:bds, 4:qzs */
|
||||
/* skip if ambiguity resolution turned off for this sys */
|
||||
nofix = (m == 0 && gps == 0) || (m == 1 && glo == 0) || (m == 3 && rtk->opt.bdsmodear == 0);
|
||||
/* skip if ambiguity resolution turned off for this sys
|
||||
(GPS AR is never disabled here: ARMODE_OFF is handled upstream
|
||||
in manage_amb_LAMBDA) */
|
||||
nofix = (m == 1 && glo == 0) || (m == 3 && rtk->opt.bdsmodear == 0);
|
||||
|
||||
/* step through freqs */
|
||||
for (f = 0, k = na; f < nf; f++, k += MAXSAT)
|
||||
@@ -2429,13 +2400,33 @@ void holdamb(rtk_t *rtk, const double *xa)
|
||||
}
|
||||
|
||||
|
||||
/* count satellites with an ambiguity flagged for AR (fix == 2) on at least
|
||||
one frequency; sat == nullptr scans the whole satellite table ------------*/
|
||||
static int count_fixed_sats(const rtk_t *rtk, const int *sat, int ns, int nf)
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < ns; i++)
|
||||
{
|
||||
const int index = (sat != nullptr ? sat[i] : i + 1) - 1;
|
||||
for (int f = 0; f < nf; f++)
|
||||
{
|
||||
if (rtk->ssat[index].fix[f] == 2)
|
||||
{
|
||||
count++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
/* resolve integer ambiguity by LAMBDA ---------------------------------------*/
|
||||
int resamb_LAMBDA(rtk_t *rtk, double *bias, double *xa, int gps, int glo, int sbs)
|
||||
int resamb_LAMBDA(rtk_t *rtk, double *bias, double *xa, int glo, int sbs)
|
||||
{
|
||||
prcopt_t *opt = &rtk->opt;
|
||||
int i;
|
||||
int j;
|
||||
int f;
|
||||
int ns;
|
||||
int nb;
|
||||
int nb1;
|
||||
@@ -2461,21 +2452,10 @@ int resamb_LAMBDA(rtk_t *rtk, double *bias, double *xa, int gps, int glo, int sb
|
||||
/* index of single to double-difference transformation matrix (D'),
|
||||
used to translate phase biases to double differences */
|
||||
ix = imat(nx, 2);
|
||||
nb = ddidx(rtk, ix, gps, glo, sbs);
|
||||
nb = ddidx(rtk, ix, glo, sbs);
|
||||
|
||||
/* count unique sats used for AR on at least one frequency */
|
||||
ns = 0;
|
||||
for (i = 0; i < MAXSAT; i++)
|
||||
{
|
||||
for (f = 0; f < NF_RTK(opt); f++)
|
||||
{
|
||||
if (rtk->ssat[i].fix[f] == 2)
|
||||
{
|
||||
ns++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ns = count_fixed_sats(rtk, nullptr, MAXSAT, NF_RTK(opt));
|
||||
if (nb <= 0 || ns < opt->minfixsats)
|
||||
{
|
||||
errmsg(rtk, "not enough valid sats for AR: nb=%d ns=%d min=%d\n",
|
||||
@@ -2610,6 +2590,8 @@ int resamb_LAMBDA(rtk_t *rtk, double *bias, double *xa, int gps, int glo, int sb
|
||||
else
|
||||
{
|
||||
errmsg(rtk, "lambda error (info=%d)\n", info);
|
||||
nb = 0; /* do not report the pair count as fixed ambiguities: bias[]
|
||||
and xa were never written (demo5 zeroes nb here too) */
|
||||
}
|
||||
free(ix);
|
||||
free(y);
|
||||
@@ -2633,12 +2615,9 @@ int manage_amb_LAMBDA(rtk_t *rtk, double *bias, double *xa, const int *sat,
|
||||
int f;
|
||||
int lockc[NFREQ];
|
||||
int excsat = 0;
|
||||
int nbsat = 0;
|
||||
int gps1;
|
||||
int nbsat;
|
||||
int glo1;
|
||||
int sbas1;
|
||||
int glo2;
|
||||
int sbas2;
|
||||
int nb;
|
||||
int rerun;
|
||||
int dly;
|
||||
@@ -2668,17 +2647,7 @@ int manage_amb_LAMBDA(rtk_t *rtk, double *bias, double *xa, const int *sat,
|
||||
return 0;
|
||||
}
|
||||
/* if no fix on previous sample and enough sats, cycle the excluded sat */
|
||||
for (i = 0; i < ns; i++)
|
||||
{
|
||||
for (f = 0; f < nf; f++)
|
||||
{
|
||||
if (rtk->ssat[sat[i] - 1].fix[f] == 2)
|
||||
{
|
||||
nbsat++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
nbsat = count_fixed_sats(rtk, sat, ns, nf);
|
||||
if (rtk->opt.mindropsats > 0 && rtk->sol.prev_ratio2 < rtk->sol.thres &&
|
||||
nbsat >= rtk->opt.mindropsats)
|
||||
{
|
||||
@@ -2729,8 +2698,6 @@ int manage_amb_LAMBDA(rtk_t *rtk, double *bias, double *xa, const int *sat,
|
||||
rtk->excsat = excsat;
|
||||
}
|
||||
|
||||
/* for the initial ambiguity resolution attempt, include all enabled sats */
|
||||
gps1 = 1;
|
||||
/* enable GLO AR only if configured and, for fix-and-hold, once hold has been acquired */
|
||||
glo1 = ((rtk->opt.navsys & SYS_GLO) != 0 && rtk->opt.glomodear != 0 &&
|
||||
(rtk->opt.glomodear != 3 || rtk->holdamb))
|
||||
@@ -2739,7 +2706,7 @@ int manage_amb_LAMBDA(rtk_t *rtk, double *bias, double *xa, const int *sat,
|
||||
sbas1 = (rtk->opt.navsys & SYS_GLO) != 0 ? glo1 : (((rtk->opt.navsys & SYS_SBS) != 0) ? 1 : 0);
|
||||
|
||||
/* first attempt to resolve ambiguities */
|
||||
nb = resamb_LAMBDA(rtk, bias, xa, gps1, glo1, sbas1);
|
||||
nb = resamb_LAMBDA(rtk, bias, xa, glo1, sbas1);
|
||||
ratio1 = rtk->sol.ratio;
|
||||
|
||||
/* reject bad satellites if AR filtering enabled */
|
||||
@@ -2778,18 +2745,15 @@ int manage_amb_LAMBDA(rtk_t *rtk, double *bias, double *xa, const int *sat,
|
||||
if (rerun)
|
||||
{
|
||||
trace(3, "rerun AR with new sats removed\n");
|
||||
nb = resamb_LAMBDA(rtk, bias, xa, gps1, glo1, sbas1);
|
||||
nb = resamb_LAMBDA(rtk, bias, xa, glo1, sbas1);
|
||||
}
|
||||
}
|
||||
|
||||
/* if fix-and-hold glomodear enabled, re-run AR with GLO/SBS disabled if it differs */
|
||||
if ((rtk->opt.navsys & SYS_GLO) != 0 && rtk->opt.glomodear == 3 && rtk->sol.ratio < rtk->sol.thres)
|
||||
/* if fix-and-hold glomodear enabled, re-run AR with GLO/SBS disabled if it was enabled */
|
||||
if ((rtk->opt.navsys & SYS_GLO) != 0 && rtk->opt.glomodear == 3 && rtk->sol.ratio < rtk->sol.thres &&
|
||||
glo1 != 0)
|
||||
{
|
||||
glo2 = sbas2 = 0;
|
||||
if (glo1 != glo2)
|
||||
{
|
||||
nb = resamb_LAMBDA(rtk, bias, xa, 1, glo2, sbas2);
|
||||
}
|
||||
nb = resamb_LAMBDA(rtk, bias, xa, 0, 0);
|
||||
}
|
||||
/* restore the excluded sat if still no fix or no significant increase in AR ratio */
|
||||
if (excsat != 0 && (rtk->sol.ratio < rtk->sol.thres) &&
|
||||
@@ -2976,12 +2940,13 @@ int relpos(rtk_t *rtk, const obsd_t *obs, int nu, int nr,
|
||||
tracemat(4, rtk->x, 1, NR_RTK(opt), 13, 4);
|
||||
|
||||
xp = mat(rtk->nx, 1);
|
||||
Pp = zeros(rtk->nx, rtk->nx);
|
||||
Pp = mat(rtk->nx, rtk->nx);
|
||||
xa = mat(rtk->nx, 1);
|
||||
matcpy(xp, rtk->x, rtk->nx, 1);
|
||||
/* the first ddres call reads Pp to detect freshly initialized phase
|
||||
biases (innovation-threshold opening), so it must see the real
|
||||
covariance, not zeros (demo5) */
|
||||
covariance (demo5); this copy also fully initializes the mat()
|
||||
allocation */
|
||||
matcpy(Pp, rtk->P, rtk->nx, rtk->nx);
|
||||
|
||||
ny = ns * nf * 2 + 2;
|
||||
@@ -3009,8 +2974,13 @@ int relpos(rtk_t *rtk, const obsd_t *obs, int nu, int nr,
|
||||
stat = SOLQ_NONE;
|
||||
break;
|
||||
}
|
||||
/* kalman filter measurement update */
|
||||
matcpy(Pp, rtk->P, rtk->nx, rtk->nx);
|
||||
/* kalman filter measurement update (on the first iteration Pp
|
||||
already holds rtk->P from the pre-loop copy and nothing has
|
||||
written either since) */
|
||||
if (i > 0)
|
||||
{
|
||||
matcpy(Pp, rtk->P, rtk->nx, rtk->nx);
|
||||
}
|
||||
if ((info = filter(xp, Pp, H, v, R, rtk->nx, nv)))
|
||||
{
|
||||
errmsg(rtk, "filter error (info=%d)\n", info);
|
||||
|
||||
@@ -156,13 +156,13 @@ double intpres(gtime_t time, const obsd_t *obs, int n, const nav_t *nav,
|
||||
rtk_t *rtk, double *y);
|
||||
|
||||
|
||||
int ddidx(rtk_t *rtk, int *ix, int gps, int glo, int sbs);
|
||||
int ddidx(rtk_t *rtk, int *ix, int glo, int sbs);
|
||||
|
||||
void restamb(rtk_t *rtk, const double *bias, int nb, double *xa);
|
||||
|
||||
void holdamb(rtk_t *rtk, const double *xa);
|
||||
|
||||
int resamb_LAMBDA(rtk_t *rtk, double *bias, double *xa, int gps, int glo, int sbs);
|
||||
int resamb_LAMBDA(rtk_t *rtk, double *bias, double *xa, int glo, int sbs);
|
||||
|
||||
int manage_amb_LAMBDA(rtk_t *rtk, double *bias, double *xa, const int *sat,
|
||||
int nf, int ns);
|
||||
|
||||
@@ -175,9 +175,6 @@ void updatesvr(rtksvr_t *svr, int ret, obs_t *obs, nav_t *nav, int sat,
|
||||
geph_t *geph2;
|
||||
geph_t *geph3;
|
||||
// gtime_t tof;
|
||||
double pos[3];
|
||||
double del[3] = {0};
|
||||
double dr[3];
|
||||
int i;
|
||||
int n = 0;
|
||||
int prn;
|
||||
@@ -302,29 +299,8 @@ void updatesvr(rtksvr_t *svr, int ret, obs_t *obs, nav_t *nav, int sat,
|
||||
{ /* antenna position parameters */
|
||||
if (svr->rtk.opt.refpos == 4 && index == 1)
|
||||
{
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
svr->rtk.rb[i] = svr->rtcm[1].sta.pos[i];
|
||||
}
|
||||
/* antenna delta */
|
||||
ecef2pos(svr->rtk.rb, pos);
|
||||
if (svr->rtcm[1].sta.deltype)
|
||||
{ /* xyz */
|
||||
del[2] = svr->rtcm[1].sta.hgt;
|
||||
enu2ecef(pos, del, dr);
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
svr->rtk.rb[i] += svr->rtcm[1].sta.del[i] + dr[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{ /* enu */
|
||||
enu2ecef(pos, svr->rtcm[1].sta.del, dr);
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
svr->rtk.rb[i] += dr[i];
|
||||
}
|
||||
}
|
||||
sta2antpos(&svr->rtcm[1].sta, svr->rtk.rb);
|
||||
}
|
||||
svr->nmsg[index][4]++;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <fcntl.h>
|
||||
#include <memory>
|
||||
#include <netdb.h>
|
||||
@@ -57,6 +56,7 @@
|
||||
#include <sys/time.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
@@ -361,19 +361,14 @@ void closefile_(file_t *file)
|
||||
}
|
||||
|
||||
|
||||
/* open file (path=filepath[::T[::+<off>][::x<speed>]][::S=swapintv]) --------*/
|
||||
file_t *openfile(std::string const &path, int mode, char *msg)
|
||||
/* Split a stream path at the literal "::" separators, preserving empty
|
||||
* tokens. Plain string search is used instead of std::regex, which is not
|
||||
* functional in GCC < 4.9. Single tokenizer for every "::"-delimited stream
|
||||
* path (file options, NTRIP client options): these paths carry credentials,
|
||||
* so the index arithmetic lives in exactly one place. */
|
||||
static std::vector<std::string> split_double_colon(const std::string &path)
|
||||
{
|
||||
tracet(3, "openfile: path=%s mode=%d\n", path.data(), mode);
|
||||
|
||||
if ((mode & (STR_MODE_R | STR_MODE_W)) == 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Split the string at the literal "::" separators. Plain string search is
|
||||
// used instead of std::regex, which is not functional in GCC < 4.9
|
||||
std::deque<std::string> tokens;
|
||||
std::vector<std::string> tokens;
|
||||
std::string::size_type token_start = 0;
|
||||
for (;;)
|
||||
{
|
||||
@@ -386,19 +381,31 @@ file_t *openfile(std::string const &path, int mode, char *msg)
|
||||
tokens.push_back(path.substr(token_start, separator - token_start));
|
||||
token_start = separator + 2;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
||||
/* open file (path=filepath[::T[::+<off>][::x<speed>]][::S=swapintv]) --------*/
|
||||
file_t *openfile(std::string const &path, int mode, char *msg)
|
||||
{
|
||||
tracet(3, "openfile: path=%s mode=%d\n", path.data(), mode);
|
||||
|
||||
if ((mode & (STR_MODE_R | STR_MODE_W)) == 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<std::string> tokens = split_double_colon(path);
|
||||
|
||||
auto file = std::make_unique<file_t>();
|
||||
|
||||
file->mode = mode;
|
||||
file->path = tokens.front(); // first token is the path
|
||||
tokens.pop_front();
|
||||
|
||||
|
||||
/* file options */
|
||||
while (!tokens.empty())
|
||||
for (std::size_t token_index = 1; token_index < tokens.size(); ++token_index)
|
||||
{
|
||||
auto tag = tokens.front();
|
||||
tokens.pop_front();
|
||||
const std::string &tag = tokens[token_index];
|
||||
|
||||
// edge case that may not be possible, but I don't want to test for right now
|
||||
if (tag.empty()) continue; // NOLINT(readability-braces-around-statements)
|
||||
@@ -1454,6 +1461,12 @@ int readtcpcli(tcpcli_t *tcpcli, unsigned char *buff, int n, char *msg)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (n <= 0)
|
||||
{
|
||||
/* recv() returns 0 for a zero-length request too: without this
|
||||
guard a full caller buffer would be misread as a peer close */
|
||||
return 0;
|
||||
}
|
||||
|
||||
nr = recv_nb(tcpcli->svr.sock, buff, n);
|
||||
if (nr == RECV_WOULD_BLOCK)
|
||||
@@ -1557,6 +1570,20 @@ int encbase64(char *str, const unsigned char *byte, int n)
|
||||
}
|
||||
|
||||
|
||||
void secure_wipe(void *data, size_t size)
|
||||
{
|
||||
if (data == nullptr || size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
volatile auto *bytes = static_cast<volatile unsigned char *>(data);
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
{
|
||||
bytes[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
Rtklib_Tls_Client *ntrip_tls(ntrip_t *ntrip)
|
||||
@@ -1566,13 +1593,8 @@ Rtklib_Tls_Client *ntrip_tls(ntrip_t *ntrip)
|
||||
|
||||
void ntrip_clear_request(ntrip_t *ntrip)
|
||||
{
|
||||
/* Requests can contain caster credentials. Use volatile stores so clearing
|
||||
* the persistent retry buffer is not optimized away. */
|
||||
volatile unsigned char *request = ntrip->request_buff;
|
||||
for (std::size_t i = 0; i < sizeof(ntrip->request_buff); ++i)
|
||||
{
|
||||
request[i] = 0;
|
||||
}
|
||||
/* Requests can contain caster credentials */
|
||||
secure_wipe(ntrip->request_buff, sizeof(ntrip->request_buff));
|
||||
ntrip->request_length = 0;
|
||||
ntrip->request_offset = 0;
|
||||
}
|
||||
@@ -2093,24 +2115,14 @@ int reject_ntrip_v2_response(ntrip_t *ntrip, char *msg,
|
||||
bool parse_ntrip_client_options(const char *path, std::string &clean_path,
|
||||
int &version, int &tls_enabled, char *msg)
|
||||
{
|
||||
clean_path = path;
|
||||
version = NTRIP_VERSION_2;
|
||||
tls_enabled = 0;
|
||||
|
||||
const std::string::size_type first = clean_path.find("::");
|
||||
if (first == std::string::npos)
|
||||
const std::vector<std::string> tokens = split_double_colon(path);
|
||||
clean_path = tokens.front();
|
||||
for (std::size_t token_index = 1; token_index < tokens.size(); ++token_index)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string options = clean_path.substr(first + 2);
|
||||
clean_path.resize(first);
|
||||
std::string::size_type begin = 0;
|
||||
while (begin <= options.size())
|
||||
{
|
||||
const std::string::size_type end = options.find("::", begin);
|
||||
std::string option = options.substr(begin,
|
||||
end == std::string::npos ? std::string::npos : end - begin);
|
||||
std::string option = tokens[token_index];
|
||||
std::transform(option.begin(), option.end(), option.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
||||
|
||||
@@ -2135,12 +2147,6 @@ bool parse_ntrip_client_options(const char *path, std::string &clean_path,
|
||||
std::snprintf(msg, MAXSTRMSG, "unknown NTRIP option");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (end == std::string::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
begin = end + 2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -2462,15 +2468,16 @@ int rspntrip_c(ntrip_t *ntrip, char *msg)
|
||||
{
|
||||
ntrip->buff[128] = '\0';
|
||||
}
|
||||
std::strncpy(msg, p, MAXSTRMSG);
|
||||
std::snprintf(msg, MAXSTRMSG, "%s", p);
|
||||
tracet(1, "rspntrip_s: %s nb=%d\n", msg, ntrip->nb);
|
||||
ntrip->nb = 0;
|
||||
ntrip->buff[0] = '\0';
|
||||
ntrip->state = 0;
|
||||
ntrip_disconnect(ntrip, ntrip->tcp->tirecon);
|
||||
}
|
||||
else if (ntrip->nb >= NTRIP_MAXRSP)
|
||||
{ /* buffer overflow */
|
||||
else if (ntrip->nb >= NTRIP_MAXRSP - 1)
|
||||
{ /* buffer overflow (reads are capped at NTRIP_MAXRSP - nb - 1, so the
|
||||
buffer is effectively full one byte before NTRIP_MAXRSP) */
|
||||
std::snprintf(msg, MAXSTRMSG, "response overflow");
|
||||
tracet(1, "rspntrip_s: response overflow nb=%d\n", ntrip->nb);
|
||||
ntrip->nb = 0;
|
||||
@@ -2505,6 +2512,16 @@ int waitntrip(ntrip_t *ntrip, char *msg)
|
||||
/* A new connection must send the complete request, never just the
|
||||
* suffix left from a short write on the previous socket. */
|
||||
ntrip_clear_request(ntrip);
|
||||
/* The response buffer and the chunked-transfer parser state belong
|
||||
* to the previous connection too: leftover stream bytes ahead of
|
||||
* the new HTTP response would make rspntrip_c parse garbage (and
|
||||
* spuriously downgrade a v2 session to v1), and a stale chunked
|
||||
* flag would de-frame a response that is not chunked. */
|
||||
ntrip->nb = 0;
|
||||
ntrip->buff[0] = '\0';
|
||||
ntrip->chunked = 0;
|
||||
ntrip->chunk_state = 0;
|
||||
ntrip->chunk_remaining = 0;
|
||||
ntrip_reset_tls(ntrip);
|
||||
}
|
||||
|
||||
@@ -2525,6 +2542,19 @@ int waitntrip(ntrip_t *ntrip, char *msg)
|
||||
}
|
||||
if (ntrip->state == 1)
|
||||
{ /* read response */
|
||||
if (ntrip->nb >= NTRIP_MAXRSP - 1)
|
||||
{ /* the buffer filled without a recognizable status line: report
|
||||
overflow instead of issuing a zero-length read (recv() would
|
||||
return 0 and be misread as a peer disconnect; the TLS reader
|
||||
would return 0 forever and stall until an external timeout) */
|
||||
std::snprintf(msg, MAXSTRMSG, "response overflow");
|
||||
tracet(1, "waitntrip: response overflow nb=%d\n", ntrip->nb);
|
||||
ntrip->nb = 0;
|
||||
ntrip->buff[0] = '\0';
|
||||
ntrip->state = 0;
|
||||
ntrip_disconnect(ntrip, ntrip->tcp->tirecon);
|
||||
return 0;
|
||||
}
|
||||
p = reinterpret_cast<char *>(ntrip->buff) + ntrip->nb;
|
||||
if ((n = ntrip_read_transport(ntrip, reinterpret_cast<unsigned char *>(p), NTRIP_MAXRSP - ntrip->nb - 1, msg)) == 0)
|
||||
{
|
||||
@@ -2612,14 +2642,7 @@ ntrip_t *openntrip(const char *path, int type, char *msg)
|
||||
return nullptr;
|
||||
}
|
||||
std::string s_aux = "http://"s + std::string(tpath);
|
||||
int n = s_aux.length();
|
||||
if (n < 256)
|
||||
{
|
||||
for (int k = 0; k < n; k++)
|
||||
{
|
||||
ntrip->url[k] = s_aux[k];
|
||||
}
|
||||
}
|
||||
std::snprintf(ntrip->url, sizeof(ntrip->url), "%s", s_aux.c_str());
|
||||
std::strncpy(tpath, proxyaddr, MAXSTRPATH);
|
||||
}
|
||||
/* open tcp client stream */
|
||||
@@ -2676,14 +2699,15 @@ int ntripsethost(ntrip_t *ntrip, const char *hostname, char *msg)
|
||||
std::snprintf(ntrip->host, sizeof(ntrip->host), "%s", hostname);
|
||||
if (ntrip->tls_enabled)
|
||||
{
|
||||
auto *tls = new (std::nothrow) Rtklib_Tls_Client(hostname);
|
||||
if (!tls || !tls->initialize(msg))
|
||||
/* retarget the existing context instead of building a second one:
|
||||
the SSL context and CA store are host-independent, and the
|
||||
hostname is consumed when the handshake starts */
|
||||
Rtklib_Tls_Client *tls = ntrip_tls(ntrip);
|
||||
if (!tls || !tls->set_hostname(hostname))
|
||||
{
|
||||
delete tls;
|
||||
std::snprintf(msg, MAXSTRMSG, "TLS host name update failed");
|
||||
return 0;
|
||||
}
|
||||
delete ntrip_tls(ntrip);
|
||||
ntrip->tls_ctx = tls;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
#define FTP_TIMEOUT 30 /* ftp/http timeout (s) */
|
||||
|
||||
|
||||
/* Overwrite credential-bearing memory with volatile stores so the wipe is not
|
||||
* optimized away (a memset before free is a legal elision target). Single
|
||||
* wipe primitive behind every credential scrub in the code base. */
|
||||
void secure_wipe(void *data, size_t size);
|
||||
|
||||
serial_t *openserial(const char *path, int mode, char *msg);
|
||||
|
||||
void closeserial(serial_t *serial);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_GNUTLS_FALLBACK
|
||||
#include <gnutls/gnutls.h>
|
||||
@@ -226,12 +227,13 @@ public:
|
||||
#endif
|
||||
|
||||
// SSL_write() demands a retry after WANT_READ/WANT_WRITE to present
|
||||
// the very same buffer and length, but the periodic GGA upload builds
|
||||
// a fresh sentence on a fresh stack buffer each cycle: without these
|
||||
// modes a backpressured GGA send is followed by a fatal
|
||||
// SSL_R_BAD_WRITE_RETRY and a full NTRIP disconnect. With them a
|
||||
// moved or shortened buffer is legal and partial writes report the
|
||||
// bytes actually consumed, like a plain socket send
|
||||
// the very same buffer and length. These modes make a moved buffer
|
||||
// legal and let partial writes report the bytes actually consumed,
|
||||
// but ssl3_write_pending() still rejects a retry SHORTER than the
|
||||
// pending record with SSL_R_BAD_WRITE_RETRY, and an equal-or-longer
|
||||
// retry silently transmits the cached record instead of the new data:
|
||||
// write() below therefore keeps its own copy of a backpressured
|
||||
// buffer and flushes exactly that copy before accepting new data
|
||||
SSL_CTX_set_mode(d_context,
|
||||
SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
|
||||
SSL_CTX_set_verify(d_context, SSL_VERIFY_PEER, nullptr);
|
||||
@@ -262,6 +264,16 @@ public:
|
||||
#endif
|
||||
}
|
||||
|
||||
bool set_hostname(const std::string &hostname)
|
||||
{
|
||||
if (hostname.empty() || d_session != nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
d_hostname = hostname;
|
||||
return true;
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
#ifdef USE_GNUTLS_FALLBACK
|
||||
@@ -277,6 +289,7 @@ public:
|
||||
SSL_free(d_session);
|
||||
d_session = nullptr;
|
||||
}
|
||||
d_pending_record.clear();
|
||||
#endif
|
||||
d_established = false;
|
||||
}
|
||||
@@ -540,6 +553,31 @@ public:
|
||||
set_tls_msg(msg, "TLS signal protection failed");
|
||||
return -1;
|
||||
}
|
||||
// OpenSSL keeps the record pending after WANT_READ/WANT_WRITE and
|
||||
// rejects any retry shorter than it (SSL_R_BAD_WRITE_RETRY), even
|
||||
// with SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER; an equal-or-longer retry
|
||||
// silently transmits the cached record in place of the new data.
|
||||
// Flush the stored copy of the backpressured buffer first: a stable-
|
||||
// buffer retry (the handshake path) resends its own data exactly
|
||||
// once, and a fresh GGA retry costs at most one stale-by-a-period
|
||||
// sentence, with the report clamped to the caller's length
|
||||
if (!d_pending_record.empty())
|
||||
{
|
||||
const int pending = static_cast<int>(d_pending_record.size());
|
||||
const int flushed = SSL_write(d_session, d_pending_record.data(), pending);
|
||||
if (flushed <= 0)
|
||||
{
|
||||
const int flush_error = SSL_get_error(d_session, flushed);
|
||||
if (flush_error == SSL_ERROR_WANT_READ || flush_error == SSL_ERROR_WANT_WRITE)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
set_tls_msg(msg, "OpenSSL send failed");
|
||||
return -1;
|
||||
}
|
||||
d_pending_record.clear();
|
||||
return flushed > n ? n : flushed;
|
||||
}
|
||||
const int ret = SSL_write(d_session, buff, n);
|
||||
if (ret > 0)
|
||||
{
|
||||
@@ -548,6 +586,7 @@ public:
|
||||
const int ssl_error = SSL_get_error(d_session, ret);
|
||||
if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE)
|
||||
{
|
||||
d_pending_record.assign(buff, buff + n);
|
||||
return 0;
|
||||
}
|
||||
set_tls_msg(msg, "OpenSSL send failed");
|
||||
@@ -565,6 +604,7 @@ private:
|
||||
#else
|
||||
SSL_CTX *d_context = nullptr;
|
||||
SSL *d_session = nullptr;
|
||||
std::vector<unsigned char> d_pending_record;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -590,6 +630,12 @@ void Rtklib_Tls_Client::reset()
|
||||
}
|
||||
|
||||
|
||||
bool Rtklib_Tls_Client::set_hostname(const std::string &hostname)
|
||||
{
|
||||
return d_impl->set_hostname(hostname);
|
||||
}
|
||||
|
||||
|
||||
int Rtklib_Tls_Client::handshake(socket_t sock, char *msg)
|
||||
{
|
||||
return d_impl->handshake(sock, msg);
|
||||
|
||||
@@ -35,6 +35,12 @@ public:
|
||||
bool initialize(char *msg);
|
||||
void reset();
|
||||
|
||||
// Replace the host name used for SNI and certificate verification. The
|
||||
// SSL context and CA store are host-independent, so an initialized client
|
||||
// can be retargeted without rebuilding them. Fails (returns false) once a
|
||||
// TLS session exists: the hostname is consumed when the handshake starts.
|
||||
bool set_hostname(const std::string &hostname);
|
||||
|
||||
// Return 1 when the TLS session is established, 0 while a non-blocking
|
||||
// handshake is in progress, and -1 on a fatal error.
|
||||
int handshake(socket_t sock, char *msg);
|
||||
|
||||
@@ -134,7 +134,11 @@ target_include_directories(core_libs
|
||||
|
||||
# links to the appropriate library and defines
|
||||
# USE_GNUTLS_FALLBACK, USE_OPENSSL_3, or USE_OPENSSL_111 accordingly.
|
||||
link_to_crypto_dependencies(core_libs)
|
||||
# PUBLIC scope (the default) is required here: the public header
|
||||
# gnss_crypto.h includes the backend headers and switches the class
|
||||
# definition on those macros, so every consumer must see the same
|
||||
# defines and include directories.
|
||||
link_to_crypto_dependencies(core_libs PUBLIC)
|
||||
|
||||
if(USE_GENERIC_LAMBDAS)
|
||||
set(has_generic_lambdas HAS_GENERIC_LAMBDA=1)
|
||||
|
||||
@@ -48,7 +48,10 @@ endif()
|
||||
|
||||
# links to the appropriate library and defines
|
||||
# USE_GNUTLS_FALLBACK, USE_OPENSSL_3, or USE_OPENSSL_111 accordingly.
|
||||
link_to_crypto_dependencies(core_libs_supl)
|
||||
# PUBLIC scope (the default) is required here: the public header supl.h
|
||||
# includes the backend headers under those macros, so every consumer must
|
||||
# see the same defines and include directories.
|
||||
link_to_crypto_dependencies(core_libs_supl PUBLIC)
|
||||
|
||||
target_include_directories(core_libs_supl
|
||||
PUBLIC
|
||||
|
||||
@@ -720,6 +720,9 @@ if(ENABLE_UNIT_TESTING)
|
||||
system_testing_lib
|
||||
core_receiver
|
||||
)
|
||||
# rtklib_tls_test.cc drives the crypto backend directly (test TLS server),
|
||||
# so link it explicitly instead of relying on transitive propagation
|
||||
link_to_crypto_dependencies(run_tests PRIVATE)
|
||||
if(ENABLE_GLOG_AND_GFLAGS)
|
||||
target_link_libraries(run_tests PRIVATE Gflags::gflags Glog::glog)
|
||||
target_compile_definitions(run_tests PRIVATE -DUSE_GLOG_AND_GFLAGS=1)
|
||||
|
||||
@@ -167,15 +167,19 @@ private:
|
||||
#include "unit-tests/signal-processing-blocks/observables/observables_phase_continuity_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/osnma/gnss_crypto_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/osnma/osnma_msg_receiver_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/bds_tgd_iono_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/geohash_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/gps_tgd_isc_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/nmea_printer_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/ntrip_rtcm_client_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/rtcm_printer_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/rtcm_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/rtklib_detslp_dop_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/rtklib_fixed_base_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/rtklib_pvt_ntrip_configuration_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/rtklib_tls_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/rtklib_udpos_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/sbas_rtklib_corrections_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/pvt/serdes_monitor_pvt_test.cc"
|
||||
#include "unit-tests/signal-processing-blocks/resampler/direct_resampler_conditioner_cc_test.cc"
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/*!
|
||||
* \file bds_tgd_iono_test.cc
|
||||
* \brief Tests for BeiDou B1I/B3I group-delay and ionospheric handling in SPP
|
||||
* \author Carles Fernandez-Prades, 2026. cfernandez(at)cttc.es
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
||||
* This file is part of GNSS-SDR.
|
||||
*
|
||||
* Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors)
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include "rtklib_pntpos.h"
|
||||
#include "rtklib_rtkcmn.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double TGD1_S = 5.0e-9;
|
||||
constexpr double B1I_WAVELENGTH_M = SPEED_OF_LIGHT_M_S / FREQ1_BDS;
|
||||
constexpr double B3I_WAVELENGTH_M = SPEED_OF_LIGHT_M_S / FREQ3_BDS;
|
||||
|
||||
void set_bds_test_ephemeris(nav_t& nav, eph_t& eph, int sat)
|
||||
{
|
||||
eph.sat = sat;
|
||||
eph.tgd[0] = TGD1_S; // TGD1 (B1I), the B3I-referenced clock needs none
|
||||
nav.n = 1;
|
||||
nav.eph = &eph;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST(BdsTgdIonoTest, SingleFrequencyB1IAppliesMinusTgd1)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_BDS, 6);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_bds_test_ephemeris(nav, eph, sat);
|
||||
|
||||
nav.lam[sat - 1][0] = B1I_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][2] = B3I_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[0] = 24000000.0;
|
||||
obs.code[0] = CODE_L2I;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
EXPECT_NEAR(obs.P[0] - SPEED_OF_LIGHT_M_S * TGD1_S, corrected_pseudorange, 1.0e-6);
|
||||
EXPECT_DOUBLE_EQ(1.0, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(BdsTgdIonoTest, SingleFrequencyB3IHasNoTgdAndScalesIonoByGamma13)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_BDS, 6);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_bds_test_ephemeris(nav, eph, sat);
|
||||
|
||||
nav.lam[sat - 1][0] = B1I_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][2] = B3I_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[2] = 24000000.0;
|
||||
obs.code[2] = CODE_L6I;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
// B3I is the clock reference signal: no group-delay correction
|
||||
EXPECT_NEAR(obs.P[2], corrected_pseudorange, 1.0e-6);
|
||||
const double gamma13 = std::pow(B3I_WAVELENGTH_M / B1I_WAVELENGTH_M, 2.0);
|
||||
EXPECT_DOUBLE_EQ(gamma13, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(BdsTgdIonoTest, DualFrequencyB1IB3IRemovesTgd1BeforeCombining)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_BDS, 6);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_bds_test_ephemeris(nav, eph, sat);
|
||||
|
||||
nav.lam[sat - 1][0] = B1I_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][2] = B3I_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[0] = 24000000.0;
|
||||
obs.P[2] = 24000006.0;
|
||||
obs.code[0] = CODE_L2I;
|
||||
obs.code[2] = CODE_L6I;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
// The DNAV clock is referenced to B3I, so the iono-free combination must
|
||||
// use the TGD1-corrected B1I pseudorange:
|
||||
// PC = (gamma13*(P1 - c*TGD1) - P2) / (gamma13 - 1)
|
||||
const double gamma13 = std::pow(B3I_WAVELENGTH_M / B1I_WAVELENGTH_M, 2.0);
|
||||
const double p1_corr = obs.P[0] - SPEED_OF_LIGHT_M_S * TGD1_S;
|
||||
const double expected = (gamma13 * p1_corr - obs.P[2]) / (gamma13 - 1.0);
|
||||
EXPECT_NEAR(expected, corrected_pseudorange, 1.0e-6);
|
||||
EXPECT_DOUBLE_EQ(0.0, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(BdsTgdIonoTest, DualFrequencyB1IB3IIonoFreeModeAlsoRemovesTgd1)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_BDS, 6);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_bds_test_ephemeris(nav, eph, sat);
|
||||
|
||||
nav.lam[sat - 1][0] = B1I_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][2] = B3I_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[0] = 24000000.0;
|
||||
obs.P[2] = 24000006.0;
|
||||
obs.code[0] = CODE_L2I;
|
||||
obs.code[2] = CODE_L6I;
|
||||
options.ionoopt = IONOOPT_IFLC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
const double gamma13 = std::pow(B3I_WAVELENGTH_M / B1I_WAVELENGTH_M, 2.0);
|
||||
const double p1_corr = obs.P[0] - SPEED_OF_LIGHT_M_S * TGD1_S;
|
||||
const double expected = (gamma13 * p1_corr - obs.P[2]) / (gamma13 - 1.0);
|
||||
EXPECT_NEAR(expected, corrected_pseudorange, 1.0e-6);
|
||||
EXPECT_DOUBLE_EQ(0.0, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(BdsTgdIonoTest, IonocorrPrefersBdsKlobucharForBdsSatellites)
|
||||
{
|
||||
nav_t nav{};
|
||||
const double ep[6] = {2026, 7, 31, 12, 0, 0};
|
||||
const gtime_t t = epoch2time(ep);
|
||||
const double pos[3] = {0.7, 2.0, 100.0};
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
|
||||
const double ion_gps[8] = {0.1118e-7, -0.7451e-8, -0.5961e-7, 0.1192e-6,
|
||||
0.1167e6, -0.2294e6, -0.1311e6, 0.1049e7};
|
||||
const double ion_bds[8] = {0.2235e-7, -0.1490e-7, -0.1192e-6, 0.2384e-6,
|
||||
0.1290e6, -0.1966e6, -0.2621e6, 0.8389e6};
|
||||
for (int k = 0; k < 8; k++)
|
||||
{
|
||||
nav.ion_gps[k] = ion_gps[k];
|
||||
nav.ion_cmp[k] = ion_bds[k];
|
||||
}
|
||||
|
||||
double ion = 0.0;
|
||||
double var = 0.0;
|
||||
|
||||
// GPS satellite: GPS coefficients, unscaled
|
||||
ASSERT_EQ(1, ionocorr(t, &nav, satno(SYS_GPS, 1), pos, azel, IONOOPT_BRDC, &ion, &var));
|
||||
EXPECT_NEAR(ionmodel(t, nav.ion_gps, pos, azel), ion, 1.0e-9);
|
||||
|
||||
// BDS satellite: BDS coefficients (B1I-referenced), expressed at GPS L1
|
||||
ASSERT_EQ(1, ionocorr(t, &nav, satno(SYS_BDS, 6), pos, azel, IONOOPT_BRDC, &ion, &var));
|
||||
const double expected_bds = ionmodel(t, nav.ion_cmp, pos, azel) * std::pow(FREQ1_BDS / FREQ1, 2.0);
|
||||
EXPECT_NEAR(expected_bds, ion, 1.0e-9);
|
||||
EXPECT_GT(ion, 0.0);
|
||||
|
||||
// BDS satellite without BDS iono: falls back to the GPS coefficients
|
||||
for (int k = 0; k < 8; k++)
|
||||
{
|
||||
nav.ion_cmp[k] = 0.0;
|
||||
}
|
||||
ASSERT_EQ(1, ionocorr(t, &nav, satno(SYS_BDS, 6), pos, azel, IONOOPT_BRDC, &ion, &var));
|
||||
EXPECT_NEAR(ionmodel(t, nav.ion_gps, pos, azel), ion, 1.0e-9);
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/*!
|
||||
* \file gps_tgd_isc_test.cc
|
||||
* \brief Tests for GPS TGD / inter-signal correction application in prange()
|
||||
* \author Carles Fernandez-Prades, 2026. cfernandez(at)cttc.es
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
||||
* This file is part of GNSS-SDR.
|
||||
*
|
||||
* Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors)
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include "rtklib_pntpos.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double TGD_S = 1.0e-8;
|
||||
constexpr double ISC_L1CA_S = -2.0e-9;
|
||||
constexpr double ISC_L2C_S = -3.0e-9;
|
||||
constexpr double ISC_L5I5_S = -4.0e-9;
|
||||
constexpr double L1_WAVELENGTH_M = SPEED_OF_LIGHT_M_S / FREQ1;
|
||||
constexpr double L2_WAVELENGTH_M = SPEED_OF_LIGHT_M_S / FREQ2;
|
||||
constexpr double L5_WAVELENGTH_M = SPEED_OF_LIGHT_M_S / FREQ5;
|
||||
|
||||
void set_gps_test_ephemeris(nav_t& nav, eph_t& eph, int sat)
|
||||
{
|
||||
eph.sat = sat;
|
||||
eph.tgd[0] = TGD_S;
|
||||
eph.isc[0] = ISC_L1CA_S;
|
||||
eph.isc[1] = ISC_L2C_S;
|
||||
eph.isc[2] = ISC_L5I5_S;
|
||||
nav.n = 1;
|
||||
nav.eph = &eph;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST(GpsTgdIscTest, SingleFrequencyL1AppliesMinusTgd)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_GPS, 1);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_gps_test_ephemeris(nav, eph, sat);
|
||||
|
||||
nav.lam[sat - 1][0] = L1_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][1] = L2_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[0] = 24000000.0;
|
||||
obs.code[0] = CODE_L1C;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
// IS-GPS-200 20.3.3.3.3.2: (dtSV)_L1 = dtSV - TGD => PC = P1 - c*TGD
|
||||
EXPECT_NEAR(obs.P[0] - SPEED_OF_LIGHT_M_S * TGD_S, corrected_pseudorange, 1.0e-6);
|
||||
EXPECT_DOUBLE_EQ(1.0, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(GpsTgdIscTest, SingleFrequencyL1ConvertsP1P2DcbToTgdEquivalent)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_GPS, 1);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_gps_test_ephemeris(nav, eph, sat);
|
||||
|
||||
constexpr double dcb_p1_p2_m = 3.0;
|
||||
nav.cbias[sat - 1][0] = dcb_p1_p2_m;
|
||||
nav.lam[sat - 1][0] = L1_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][1] = L2_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[0] = 24000000.0;
|
||||
obs.code[0] = CODE_L1C;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
// A raw P1-P2 DCB (here 3 m) takes precedence over the broadcast TGD and
|
||||
// must be converted to its TGD equivalent: TGD = DCB / (1 - gamma12),
|
||||
// so PC = P1 - DCB / (1 - gamma12), matching upstream RTKLIB
|
||||
const double gamma12 = std::pow(L2_WAVELENGTH_M / L1_WAVELENGTH_M, 2.0);
|
||||
const double expected = obs.P[0] - dcb_p1_p2_m / (1.0 - gamma12);
|
||||
EXPECT_NEAR(expected, corrected_pseudorange, 1.0e-6);
|
||||
}
|
||||
|
||||
|
||||
TEST(GpsTgdIscTest, DualFrequencyL1L2AppliesMinusTgdToL1Pseudorange)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_GPS, 1);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_gps_test_ephemeris(nav, eph, sat);
|
||||
|
||||
nav.lam[sat - 1][0] = L1_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][1] = L2_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[0] = 24000000.0;
|
||||
obs.P[1] = 24000005.0;
|
||||
obs.code[0] = CODE_L1C;
|
||||
obs.code[1] = CODE_L2S;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
// With a single-frequency iono model, the L1 C/A pseudorange referenced to
|
||||
// the LNAV clock takes the same correction as the L1-only case: P1 - c*TGD.
|
||||
// A '+' sign here doubles the group-delay error (up to ~8 m).
|
||||
EXPECT_NEAR(obs.P[0] - SPEED_OF_LIGHT_M_S * TGD_S, corrected_pseudorange, 1.0e-6);
|
||||
// The measurement is L1-referenced, so the modeled iono delay applies in full
|
||||
EXPECT_DOUBLE_EQ(1.0, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(GpsTgdIscTest, SingleFrequencyL5ScalesModeledIonoByGamma15)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_GPS, 1);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_gps_test_ephemeris(nav, eph, sat);
|
||||
|
||||
nav.lam[sat - 1][0] = L1_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][2] = L5_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[2] = 24000000.0;
|
||||
obs.code[2] = CODE_L5X;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
// The modeled L1 iono delay must be scaled by gamma15 = (f_L1/f_L5)^2 for
|
||||
// an L5-only pseudorange (the PC group-delay value itself is not asserted
|
||||
// here; its sign convention is exercised by the CNAV correction tests)
|
||||
const double gamma15 = std::pow(L5_WAVELENGTH_M / L1_WAVELENGTH_M, 2.0);
|
||||
EXPECT_NE(0.0, corrected_pseudorange);
|
||||
EXPECT_DOUBLE_EQ(gamma15, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(GpsTgdIscTest, DualFrequencyL1L5UsesIscL1caOnGammaWeightedTerm)
|
||||
{
|
||||
nav_t nav{};
|
||||
eph_t eph{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_GPS, 1);
|
||||
ASSERT_GT(sat, 0);
|
||||
set_gps_test_ephemeris(nav, eph, sat);
|
||||
|
||||
nav.lam[sat - 1][0] = L1_WAVELENGTH_M;
|
||||
nav.lam[sat - 1][2] = L5_WAVELENGTH_M;
|
||||
obs.sat = sat;
|
||||
obs.P[0] = 24000000.0;
|
||||
obs.P[2] = 24000008.0;
|
||||
obs.code[0] = CODE_L1C;
|
||||
obs.code[2] = CODE_L5X;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
// IS-GPS-705 20.3.3.3.1.2.2 dual-frequency (L1 C/A, L5 I5) correction:
|
||||
// PC = (P5 + c*ISC_L5I5 - gamma15*(P1 + c*ISC_L1CA)) / (1 - gamma15) - c*TGD
|
||||
const double gamma15 = std::pow(L5_WAVELENGTH_M / L1_WAVELENGTH_M, 2.0);
|
||||
const double p1_corr = obs.P[0] + SPEED_OF_LIGHT_M_S * ISC_L1CA_S;
|
||||
const double p5_corr = obs.P[2] + SPEED_OF_LIGHT_M_S * ISC_L5I5_S;
|
||||
const double expected = (p5_corr - gamma15 * p1_corr) / (1.0 - gamma15) - SPEED_OF_LIGHT_M_S * TGD_S;
|
||||
|
||||
EXPECT_NEAR(expected, corrected_pseudorange, 1.0e-6);
|
||||
// Iono-free combination: the modeled iono delay must not be applied on top
|
||||
EXPECT_DOUBLE_EQ(0.0, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(GlonassDcbTest, SingleFrequencyL2AppliesP1P2DcbOnce)
|
||||
{
|
||||
nav_t nav{};
|
||||
obsd_t obs{};
|
||||
prcopt_t options{};
|
||||
const int sat = satno(SYS_GLO, 1);
|
||||
ASSERT_GT(sat, 0);
|
||||
|
||||
constexpr double dcb_p1_p2_m = 3.0;
|
||||
const double l1_wavelength_m = SPEED_OF_LIGHT_M_S / FREQ1_GLO;
|
||||
const double l2_wavelength_m = SPEED_OF_LIGHT_M_S / FREQ2_GLO;
|
||||
nav.cbias[sat - 1][0] = dcb_p1_p2_m;
|
||||
nav.lam[sat - 1][0] = l1_wavelength_m;
|
||||
nav.lam[sat - 1][1] = l2_wavelength_m;
|
||||
obs.sat = sat;
|
||||
obs.P[1] = 24000000.0;
|
||||
obs.code[1] = CODE_L2C;
|
||||
options.ionoopt = IONOOPT_BRDC;
|
||||
|
||||
const double azel[2] = {0.0, 1.0};
|
||||
double variance = 0.0;
|
||||
double iono_scale = -1.0;
|
||||
const double corrected_pseudorange = prange(&obs, &nav, azel, 0, &options, &variance, &iono_scale);
|
||||
|
||||
const double gamma12 = std::pow(l2_wavelength_m / l1_wavelength_m, 2.0);
|
||||
const double expected = obs.P[1] - gamma12 * dcb_p1_p2_m / (1.0 - gamma12);
|
||||
EXPECT_NEAR(expected, corrected_pseudorange, 1.0e-6);
|
||||
EXPECT_DOUBLE_EQ(gamma12, iono_scale);
|
||||
}
|
||||
|
||||
|
||||
TEST(SppVarerrTest, IonoFreeCombinationAmplifiesVarianceByUpstreamFactorNine)
|
||||
{
|
||||
// the ionosphere-free noise amplification is (f1^2/(f1^2-f2^2))^2 +
|
||||
// (f2^2/(f1^2-f2^2))^2, approximately 8.9 for GPS L1/L2, which upstream
|
||||
// RTKLIB rounds to 3^2 = 9; rescode's dual-frequency fallback applies the
|
||||
// same factor, so this pins the canonical value
|
||||
prcopt_t opt{};
|
||||
opt.err[1] = 0.003;
|
||||
opt.err[2] = 0.003;
|
||||
opt.eratio[0] = 100.0;
|
||||
|
||||
obsd_t obs{};
|
||||
const double elevation_rad = 45.0 * D2R;
|
||||
|
||||
opt.ionoopt = IONOOPT_OFF;
|
||||
const double variance_single = varerr(&opt, &obs, elevation_rad, SYS_GPS);
|
||||
ASSERT_GT(variance_single, 0.0);
|
||||
|
||||
opt.ionoopt = IONOOPT_IFLC;
|
||||
const double variance_iono_free = varerr(&opt, &obs, elevation_rad, SYS_GPS);
|
||||
|
||||
EXPECT_DOUBLE_EQ(9.0, variance_iono_free / variance_single);
|
||||
}
|
||||
|
||||
|
||||
TEST(SppVarerrTest, SnrWeightingFollowsTheMeasurementSlot)
|
||||
{
|
||||
// GNSS-SDR places single-band L2C/L5/E5a observations in slots 1/2 with
|
||||
// slot 0 empty: the SNR term must read the slot that carries the
|
||||
// pseudorange, or those satellites are weighted as if their SNR were zero
|
||||
prcopt_t opt{};
|
||||
opt.err[1] = 0.003;
|
||||
opt.err[2] = 0.003;
|
||||
opt.err[5] = 52.0; // error_snr_max (dB-Hz)
|
||||
opt.err[6] = 0.005; // error_factor_snr (m)
|
||||
opt.eratio[0] = 100.0;
|
||||
opt.ionoopt = IONOOPT_OFF;
|
||||
const double elevation_rad = 45.0 * D2R;
|
||||
const unsigned char snr_45dbhz = 180; // in 0.25 dB-Hz units
|
||||
|
||||
obsd_t slot0_observation{};
|
||||
slot0_observation.P[0] = 22.0e6;
|
||||
slot0_observation.SNR[0] = snr_45dbhz;
|
||||
|
||||
obsd_t slot2_observation{}; // an L5/E5a-only observation lives in slot 2
|
||||
slot2_observation.P[2] = 22.0e6;
|
||||
slot2_observation.SNR[2] = snr_45dbhz;
|
||||
|
||||
const double variance_slot0 = varerr(&opt, &slot0_observation, elevation_rad, SYS_GPS);
|
||||
const double variance_slot2 = varerr(&opt, &slot2_observation, elevation_rad, SYS_GPS);
|
||||
EXPECT_DOUBLE_EQ(variance_slot0, variance_slot2);
|
||||
|
||||
obsd_t zero_snr_observation{};
|
||||
zero_snr_observation.P[0] = 22.0e6;
|
||||
const double variance_zero_snr = varerr(&opt, &zero_snr_observation, elevation_rad, SYS_GPS);
|
||||
EXPECT_GT(variance_zero_snr, 100.0 * variance_slot2);
|
||||
}
|
||||
@@ -1999,7 +1999,8 @@ TEST(NtripRtcmClientTest, AuthenticatedFragmentedStreamPublishesFixedBaseSnapsho
|
||||
EXPECT_GT(single_band_observation->P[0], 0.0);
|
||||
EXPECT_TRUE(snapshot.fresh);
|
||||
EXPECT_NEAR(0.0, snapshot.age_s, 1e-9);
|
||||
EXPECT_GT(snapshot.observation_generation, 0U);
|
||||
EXPECT_GT(snapshot.generation, 0U);
|
||||
EXPECT_EQ(snapshot.generation, client.snapshot_generation());
|
||||
|
||||
const Ntrip_Rtcm_Snapshot slightly_future_snapshot =
|
||||
client.latest_snapshot(timeadd(rover_time, -0.5));
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*!
|
||||
* \file rtklib_detslp_dop_test.cc
|
||||
* \brief Unit tests for the doppler / phase-difference cycle-slip detector
|
||||
* \author Carles Fernandez-Prades, 2026. cfernandez(at)cttc.es
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
||||
* This file is part of GNSS-SDR.
|
||||
*
|
||||
* Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors)
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include "rtklib.h"
|
||||
#include "rtklib_rtkcmn.h"
|
||||
#include "rtklib_rtkpos.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double LAMBDA_L1 = SPEED_OF_LIGHT_M_S / 1575.42e6;
|
||||
constexpr double PHASE_RATE_CYCLES_S = 1000.0;
|
||||
constexpr int NUM_SATS = 4;
|
||||
|
||||
struct DetslpDopFixture
|
||||
{
|
||||
DetslpDopFixture()
|
||||
: rtk(std::make_unique<rtk_t>()),
|
||||
nav(std::make_unique<nav_t>())
|
||||
{
|
||||
*rtk = {};
|
||||
*nav = {};
|
||||
rtk->opt.nf = 1;
|
||||
|
||||
const double ep[] = {2026, 8, 10, 0, 0, 0.0};
|
||||
t_prev = epoch2time(ep);
|
||||
t_now = timeadd(t_prev, 1.0);
|
||||
|
||||
for (int i = 0; i < NUM_SATS; i++)
|
||||
{
|
||||
ix[i] = i;
|
||||
obs[i] = {};
|
||||
obs[i].sat = i + 1;
|
||||
obs[i].time = t_now;
|
||||
/* doppler consistent with the phase advance on every satellite */
|
||||
obs[i].L[0] = 100.0 + PHASE_RATE_CYCLES_S;
|
||||
obs[i].D[0] = -PHASE_RATE_CYCLES_S;
|
||||
rtk->ssat[i].ph[0][0] = 100.0;
|
||||
rtk->ssat[i].pt[0][0] = t_prev;
|
||||
nav->lam[i][0] = LAMBDA_L1;
|
||||
}
|
||||
}
|
||||
std::unique_ptr<rtk_t> rtk;
|
||||
std::unique_ptr<nav_t> nav;
|
||||
obsd_t obs[NUM_SATS]{};
|
||||
int ix[NUM_SATS]{};
|
||||
gtime_t t_prev{};
|
||||
gtime_t t_now{};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST(RtklibDetslpDopTest, FlagsOnlyTheSatelliteWithInconsistentPhase)
|
||||
{
|
||||
DetslpDopFixture f;
|
||||
/* 25-cycle slip on the last satellite: phase jumps, doppler does not */
|
||||
f.obs[NUM_SATS - 1].L[0] += 25.0;
|
||||
|
||||
/* threshold 0 disables the test */
|
||||
f.rtk->opt.thresdop = 0.0;
|
||||
detslp_dop(f.rtk.get(), f.obs, f.ix, NUM_SATS, 1, f.nav.get());
|
||||
for (int i = 0; i < NUM_SATS; i++)
|
||||
{
|
||||
EXPECT_EQ(f.rtk->ssat[i].slip[0] & 1, 0) << "sat " << i + 1;
|
||||
}
|
||||
|
||||
f.rtk->opt.thresdop = 1.0;
|
||||
detslp_dop(f.rtk.get(), f.obs, f.ix, NUM_SATS, 1, f.nav.get());
|
||||
for (int i = 0; i < NUM_SATS - 1; i++)
|
||||
{
|
||||
EXPECT_EQ(f.rtk->ssat[i].slip[0] & 1, 0) << "sat " << i + 1;
|
||||
}
|
||||
EXPECT_EQ(f.rtk->ssat[NUM_SATS - 1].slip[0] & 1, 1);
|
||||
}
|
||||
|
||||
|
||||
TEST(RtklibDetslpDopTest, ImmuneToCommonReceiverClockJump)
|
||||
{
|
||||
DetslpDopFixture f;
|
||||
/* a receiver clock jump advances every carrier phase by the same amount;
|
||||
the median common range-rate error must absorb it without false slips */
|
||||
for (int i = 0; i < NUM_SATS; i++)
|
||||
{
|
||||
f.obs[i].L[0] += 2000.0;
|
||||
}
|
||||
|
||||
f.rtk->opt.thresdop = 1.0;
|
||||
detslp_dop(f.rtk.get(), f.obs, f.ix, NUM_SATS, 1, f.nav.get());
|
||||
for (int i = 0; i < NUM_SATS; i++)
|
||||
{
|
||||
EXPECT_EQ(f.rtk->ssat[i].slip[0] & 1, 0) << "sat " << i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(RtklibDetslpDopTest, DetectsSlipOnTopOfClockJump)
|
||||
{
|
||||
DetslpDopFixture f;
|
||||
for (int i = 0; i < NUM_SATS; i++)
|
||||
{
|
||||
f.obs[i].L[0] += 2000.0;
|
||||
}
|
||||
f.obs[0].L[0] += 25.0; /* slip on the first satellite, on top of the jump */
|
||||
|
||||
f.rtk->opt.thresdop = 1.0;
|
||||
detslp_dop(f.rtk.get(), f.obs, f.ix, NUM_SATS, 1, f.nav.get());
|
||||
EXPECT_EQ(f.rtk->ssat[0].slip[0] & 1, 1);
|
||||
for (int i = 1; i < NUM_SATS; i++)
|
||||
{
|
||||
EXPECT_EQ(f.rtk->ssat[i].slip[0] & 1, 0) << "sat " << i + 1;
|
||||
}
|
||||
}
|
||||
@@ -1437,7 +1437,7 @@ TEST_F(RtklibFixedBaseTest, BaseStreamEphemerisSubstitutesForUndecodedRoverEphem
|
||||
satellites, false, true);
|
||||
solver->gps_ephemeris_map.erase(static_cast<int>(undecoded_prn));
|
||||
solver->gps_cnav_ephemeris_map.erase(static_cast<int>(undecoded_prn));
|
||||
epoch.base_snapshot.gps_ephemerides.push_back(
|
||||
epoch.base_snapshot.ephemerides.push_back(
|
||||
eph_to_rtklib(make_relative_ephemeris(undecoded_prn)));
|
||||
|
||||
SCOPED_TRACE(::testing::Message() << "epoch=" << epoch_index);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "in_memory_configuration.h"
|
||||
#include "rtklib_pvt.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -354,3 +355,38 @@ TEST_F(RtklibPvtNtripConfigurationTest, RejectsInvalidPasswordConfigurationBefor
|
||||
duplicate_password_source->set_property("PVT.ntrip_password_env", "UNUSED_NTRIP_PASSWORD");
|
||||
expect_invalid_configuration(*duplicate_password_source, "mutually exclusive");
|
||||
}
|
||||
|
||||
|
||||
TEST_F(RtklibPvtNtripConfigurationTest, ResolvesThePasswordFromTheNamedEnvironmentVariable)
|
||||
{
|
||||
using namespace rtklib_pvt_ntrip_configuration_test_detail;
|
||||
ASSERT_EQ(0, setenv("GNSS_SDR_TEST_NTRIP_PASSWORD", "secret-from-env", 1));
|
||||
|
||||
// Without a username, a resolved password must trigger the
|
||||
// username-is-required rule: the throw proves the environment value
|
||||
// actually materialized as the password
|
||||
std::unique_ptr<InMemoryConfiguration> env_password_no_username = make_valid_ntrip_configuration();
|
||||
env_password_no_username->set_property("PVT.ntrip_password_env", "GNSS_SDR_TEST_NTRIP_PASSWORD");
|
||||
expect_invalid_configuration(*env_password_no_username, "username is required");
|
||||
|
||||
std::unique_ptr<InMemoryConfiguration> env_password = make_valid_ntrip_configuration();
|
||||
env_password->set_property("PVT.ntrip_username", "user");
|
||||
env_password->set_property("PVT.ntrip_password_env", "GNSS_SDR_TEST_NTRIP_PASSWORD");
|
||||
std::unique_ptr<Rtklib_Pvt> adapter;
|
||||
EXPECT_NO_THROW(adapter.reset(new Rtklib_Pvt(env_password.get(), ROLE, 2, 0)));
|
||||
ASSERT_NE(nullptr, adapter);
|
||||
|
||||
ASSERT_EQ(0, unsetenv("GNSS_SDR_TEST_NTRIP_PASSWORD"));
|
||||
}
|
||||
|
||||
|
||||
TEST_F(RtklibPvtNtripConfigurationTest, RejectsAPasswordEnvironmentVariableThatIsNotSet)
|
||||
{
|
||||
using namespace rtklib_pvt_ntrip_configuration_test_detail;
|
||||
ASSERT_EQ(0, unsetenv("GNSS_SDR_TEST_NTRIP_PASSWORD_UNSET"));
|
||||
|
||||
std::unique_ptr<InMemoryConfiguration> unset_variable = make_valid_ntrip_configuration();
|
||||
unset_variable->set_property("PVT.ntrip_username", "user");
|
||||
unset_variable->set_property("PVT.ntrip_password_env", "GNSS_SDR_TEST_NTRIP_PASSWORD_UNSET");
|
||||
expect_invalid_configuration(*unset_variable, "not set");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/*!
|
||||
* \file rtklib_udpos_test.cc
|
||||
* \brief This file implements tests for the temporal update of the position,
|
||||
* velocity and acceleration states of the RTK filter.
|
||||
* \author Carles Fernandez-Prades, 2026. cfernandez(at)cttc.es
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
||||
* This file is part of GNSS-SDR.
|
||||
*
|
||||
* Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors)
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include "rtklib_rtkpos.h"
|
||||
#include "rtklib_rtksvr.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
namespace rtklib_udpos_test_detail
|
||||
{
|
||||
// a plausible ECEF position, so that the filter is not reinitialized
|
||||
constexpr double POSITION_ECEF_M[3] = {4600000.0, 700000.0, 4370000.0};
|
||||
constexpr double VELOCITY_ECEF_MPS[3] = {3.0, -2.0, 1.0};
|
||||
constexpr double ACCELERATION_ECEF_MPS2[3] = {0.4, -0.3, 0.2};
|
||||
|
||||
|
||||
prcopt_t dynamic_options()
|
||||
{
|
||||
prcopt_t options = PRCOPT_DEFAULT;
|
||||
options.mode = PMODE_KINEMA;
|
||||
options.nf = 1;
|
||||
options.navsys = SYS_GPS;
|
||||
options.dynamics = 1;
|
||||
options.armaxposvar = 0.25;
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
class Rtk_State
|
||||
{
|
||||
public:
|
||||
explicit Rtk_State(const prcopt_t& options)
|
||||
{
|
||||
rtkinit(&d_rtk, &options);
|
||||
}
|
||||
|
||||
~Rtk_State()
|
||||
{
|
||||
rtkfree(&d_rtk);
|
||||
}
|
||||
|
||||
Rtk_State(const Rtk_State&) = delete;
|
||||
Rtk_State& operator=(const Rtk_State&) = delete;
|
||||
|
||||
//! Puts the filter in a converged state moving with a constant acceleration
|
||||
void set_dynamic_state(double position_variance_m2)
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
d_rtk.x[i] = POSITION_ECEF_M[i];
|
||||
d_rtk.x[i + 3] = VELOCITY_ECEF_MPS[i];
|
||||
d_rtk.x[i + 6] = ACCELERATION_ECEF_MPS2[i];
|
||||
d_rtk.sol.rr[i] = POSITION_ECEF_M[i];
|
||||
d_rtk.sol.rr[i + 3] = VELOCITY_ECEF_MPS[i];
|
||||
}
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
d_rtk.P[i + i * d_rtk.nx] = position_variance_m2;
|
||||
}
|
||||
}
|
||||
|
||||
rtk_t& rtk() { return d_rtk; }
|
||||
|
||||
private:
|
||||
rtk_t d_rtk{};
|
||||
};
|
||||
} // namespace rtklib_udpos_test_detail
|
||||
|
||||
|
||||
TEST(RtklibUdposTest, AccelerationDrivesPositionOnceTheFilterHasConverged)
|
||||
{
|
||||
using namespace rtklib_udpos_test_detail;
|
||||
Rtk_State state(dynamic_options());
|
||||
state.set_dynamic_state(0.01); // well below armaxposvar
|
||||
|
||||
const double tt = 0.5;
|
||||
udpos(&state.rtk(), tt);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
const double expected_position = POSITION_ECEF_M[i] +
|
||||
VELOCITY_ECEF_MPS[i] * tt +
|
||||
0.5 * ACCELERATION_ECEF_MPS2[i] * tt * tt;
|
||||
const double expected_velocity = VELOCITY_ECEF_MPS[i] + ACCELERATION_ECEF_MPS2[i] * tt;
|
||||
EXPECT_NEAR(expected_position, state.rtk().x[i], 1e-9) << "axis " << i;
|
||||
EXPECT_NEAR(expected_velocity, state.rtk().x[i + 3], 1e-9) << "axis " << i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(RtklibUdposTest, AccelerationIsKeptOutOfPositionWhileTheVarianceIsLarge)
|
||||
{
|
||||
using namespace rtklib_udpos_test_detail;
|
||||
prcopt_t options = dynamic_options();
|
||||
Rtk_State state(options);
|
||||
state.set_dynamic_state(10.0 * options.armaxposvar);
|
||||
|
||||
const double tt = 0.5;
|
||||
udpos(&state.rtk(), tt);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
// velocity still propagates, the acceleration term does not
|
||||
const double expected_position = POSITION_ECEF_M[i] + VELOCITY_ECEF_MPS[i] * tt;
|
||||
EXPECT_NEAR(expected_position, state.rtk().x[i], 1e-9) << "axis " << i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(RtklibUdposTest, TheAccelerationGateCanBeDisabled)
|
||||
{
|
||||
using namespace rtklib_udpos_test_detail;
|
||||
prcopt_t options = dynamic_options();
|
||||
options.armaxposvar = 0.0; // no gate
|
||||
Rtk_State state(options);
|
||||
// large, but below the variance that makes the filter reset the position
|
||||
state.set_dynamic_state(100.0);
|
||||
|
||||
const double tt = 0.5;
|
||||
udpos(&state.rtk(), tt);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
const double expected_position = POSITION_ECEF_M[i] +
|
||||
VELOCITY_ECEF_MPS[i] * tt +
|
||||
0.5 * ACCELERATION_ECEF_MPS2[i] * tt * tt;
|
||||
EXPECT_NEAR(expected_position, state.rtk().x[i], 1e-9) << "axis " << i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(RtklibUdposTest, AccelerationProcessNoiseGrowsWithTheElapsedTime)
|
||||
{
|
||||
using namespace rtklib_udpos_test_detail;
|
||||
const prcopt_t options = dynamic_options();
|
||||
|
||||
const auto acceleration_variance_after = [&options](double tt) {
|
||||
Rtk_State state(options);
|
||||
state.set_dynamic_state(0.01);
|
||||
const double before = state.rtk().P[6 + 6 * state.rtk().nx];
|
||||
udpos(&state.rtk(), tt);
|
||||
return state.rtk().P[6 + 6 * state.rtk().nx] - before;
|
||||
};
|
||||
|
||||
const double growth_half_second = acceleration_variance_after(0.5);
|
||||
const double growth_one_second = acceleration_variance_after(1.0);
|
||||
|
||||
EXPECT_GT(growth_half_second, 0.0);
|
||||
EXPECT_NEAR(2.0, growth_one_second / growth_half_second, 1e-6);
|
||||
}
|
||||
|
||||
|
||||
TEST(RtklibUdposTest, ProcessNoiseStaysPositiveWhenTimeGoesBackwards)
|
||||
{
|
||||
using namespace rtklib_udpos_test_detail;
|
||||
const prcopt_t options = dynamic_options();
|
||||
|
||||
Rtk_State forward(options);
|
||||
forward.set_dynamic_state(0.01);
|
||||
const double before = forward.rtk().P[6 + 6 * forward.rtk().nx];
|
||||
udpos(&forward.rtk(), 1.0);
|
||||
const double forward_growth = forward.rtk().P[6 + 6 * forward.rtk().nx] - before;
|
||||
|
||||
Rtk_State backward(options);
|
||||
backward.set_dynamic_state(0.01);
|
||||
udpos(&backward.rtk(), -1.0);
|
||||
const double backward_growth = backward.rtk().P[6 + 6 * backward.rtk().nx] - before;
|
||||
|
||||
// a negative time step must not remove process noise from the filter. The
|
||||
// two are not bit-identical because the noise is rotated to ECEF at the
|
||||
// propagated position, which differs between the two directions
|
||||
EXPECT_GT(backward_growth, 0.0);
|
||||
EXPECT_NEAR(1.0, backward_growth / forward_growth, 1e-5);
|
||||
}
|
||||
|
||||
|
||||
TEST(RtklibUdposTest, IonosphericProcessNoiseStaysPositiveWhenTimeGoesBackwards)
|
||||
{
|
||||
using namespace rtklib_udpos_test_detail;
|
||||
prcopt_t options = dynamic_options();
|
||||
options.ionoopt = IONOOPT_EST;
|
||||
Rtk_State state(options);
|
||||
|
||||
// an already initialized ionospheric state, so that udion() adds process
|
||||
// noise to it instead of initializing it
|
||||
const int satellite = 1;
|
||||
const int index = II_RTK(satellite, &state.rtk().opt);
|
||||
state.rtk().x[index] = 1.0;
|
||||
state.rtk().P[index + index * state.rtk().nx] = 0.1;
|
||||
state.rtk().ssat[satellite - 1].azel[1] = 45.0 * D2R;
|
||||
|
||||
const double baseline_m = 10000.0;
|
||||
const double variance_before = state.rtk().P[index + index * state.rtk().nx];
|
||||
udion(&state.rtk(), -1.0, baseline_m, &satellite, 1);
|
||||
|
||||
// a negative time step must not remove process noise from the filter
|
||||
EXPECT_GT(state.rtk().P[index + index * state.rtk().nx], variance_before);
|
||||
}
|
||||
|
||||
|
||||
TEST(RtklibUdposTest, AnImplausiblePositionStateIsReinitialized)
|
||||
{
|
||||
using namespace rtklib_udpos_test_detail;
|
||||
Rtk_State state(dynamic_options());
|
||||
state.set_dynamic_state(0.01);
|
||||
|
||||
// a leftover state that is not a position on the Earth: the filter would
|
||||
// never converge from here
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
state.rtk().x[i] = 1000.0;
|
||||
}
|
||||
|
||||
udpos(&state.rtk(), 1.0);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
// reinitialized from the single-point solution, then propagated
|
||||
EXPECT_GT(std::fabs(state.rtk().x[i]), 1000.0) << "axis " << i;
|
||||
EXPECT_NEAR(POSITION_ECEF_M[i], state.rtk().x[i], 10.0) << "axis " << i;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user