Enable usage of Virtual Reference Stations

This commit is contained in:
Carles Fernandez
2026-08-17 09:37:25 +02:00
parent f65e43b72f
commit a936c4d2d8
7 changed files with 103 additions and 2 deletions
+5 -1
View File
@@ -112,7 +112,11 @@ All notable changes to GNSS-SDR will be documented in this file.
(`PVT.ntrip_tls_enabled=true`). It reconnects without blocking the GNU Radio
work function, filters station changes and stale corrections, redacts
credentials from RTKLIB traces, and retains an explicitly labeled single-point
fallback when configured. VRS/GGA is not yet supported.
fallback when configured. VRS and nearest-station mountpoints are supported:
the client periodically reports the rover position upstream as an NMEA GGA
sentence (`PVT.ntrip_send_gga`, enabled by default, with the cadence set by
`PVT.ntrip_gga_period_ms`, 10 s by default), starting as soon as the receiver
produces its first position solution.
- QZSS ambiguities are now resolved in their own group instead of jointly with
GPS, avoiding integer fixes across the GPS-QZSS inter-system bias, and the
RTCM 3 decoder accepts the final RTCM 3.3 BeiDou ephemeris message type 1042
+3 -1
View File
@@ -583,13 +583,15 @@ PVT.ntrip_reconnect_interval_ms=10000
PVT.ntrip_max_correction_age_s=5.0
PVT.ntrip_station_id=0
PVT.ntrip_fallback_to_single=true
PVT.ntrip_send_gga=true
PVT.ntrip_gga_period_ms=10000
\endverbatim
The caster address, mountpoint, username, password, and password-environment variable name default to empty strings; the remaining defaults are those shown above, except that <code>PVT.ntrip_client_enabled</code> defaults to <code>false</code>. Set either <code>PVT.ntrip_password</code> or <code>PVT.ntrip_password_env</code>, never both. The latter names an environment variable from which GNSS-SDR reads the password and avoids placing the password in the configuration file. Station ID <code>0</code> accepts any station; otherwise, set the expected RTCM station ID. Setting <code>PVT.ntrip_reconnect_interval_ms=0</code> disables operational reconnect attempts; the one immediate v2-to-v1 compatibility retry still runs and is not counted as an automatic reconnect.
The correction stream must provide an RTCM 3 base position message 1005 or 1006 and GPS observation messages carrying both L1 and L2, either as message 1004 or as MSM4 through MSM7 (1074 through 1077). Corrections older than <code>PVT.ntrip_max_correction_age_s</code> are not used. Enabling this client forces a separate internal single-point solver for receiver-clock correction, independently of the RTK solver. With <code>PVT.ntrip_fallback_to_single=true</code>, the receiver reports an explicitly labeled <code>SOLQ_SINGLE</code> solution while corrections are unavailable, incomplete, or stale; it does not label that fallback as an RTK solution.
This client sends an NTRIP version 2 HTTP/1.1 request by default and accepts a legacy v1 reply on that connection. If the fully-sent v2 exchange closes or times out before receiving any response bytes, or returns HTTP 400, 501, or 505, it opens one fresh connection and retries using the v1 HTTP/1.0 request. Once selected, v1 is retained for operational reconnects until the client is stopped and started again. DNS, TCP, TLS, and partial-write failures before the request is sent do not trigger a downgrade; <code>PVT.ntrip_version=1</code> forces v1 from the first connection. Setting <code>PVT.ntrip_tls_enabled=true</code> encrypts both attempts with TLS 1.2 or newer, validating the caster certificate against the system CA store and its host name. Without TLS, Basic authentication only encodes credentials; it does not encrypt them, so use a trusted network or a suitable secure tunnel, and prefer <code>PVT.ntrip_password_env</code> over a password stored in a file. VRS mountpoints requiring rover GGA messages are not supported.
This client sends an NTRIP version 2 HTTP/1.1 request by default and accepts a legacy v1 reply on that connection. If the fully-sent v2 exchange closes or times out before receiving any response bytes, or returns HTTP 400, 501, or 505, it opens one fresh connection and retries using the v1 HTTP/1.0 request. Once selected, v1 is retained for operational reconnects until the client is stopped and started again. DNS, TCP, TLS, and partial-write failures before the request is sent do not trigger a downgrade; <code>PVT.ntrip_version=1</code> forces v1 from the first connection. Setting <code>PVT.ntrip_tls_enabled=true</code> encrypts both attempts with TLS 1.2 or newer, validating the caster certificate against the system CA store and its host name. Without TLS, Basic authentication only encodes credentials; it does not encrypt them, so use a trusted network or a suitable secure tunnel, and prefer <code>PVT.ntrip_password_env</code> over a password stored in a file. VRS and nearest-station mountpoints that require rover GGA reports are supported: with <code>PVT.ntrip_send_gga=true</code> (the default), the client periodically sends an NMEA GGA sentence derived from the receiver's current position estimate, every <code>PVT.ntrip_gga_period_ms</code> (10 s by default, accepted range 1 s to 24 hours). No GGA is sent before the receiver produces its first position solution; the single-point bootstrap fix is enough for the caster to start serving the virtual station. Set <code>PVT.ntrip_send_gga=false</code> if the rover position must not be reported to the caster; physical-base mountpoints work either way.
<code>PVT.flag_rtcm_server</code> is unrelated to the NTRIP correction client. It controls an outbound raw TCP server for RTCM messages generated by GNSS-SDR; it neither connects to a caster nor supplies corrections to the RTK solver. The outbound server and inbound NTRIP client can be enabled independently.
@@ -242,6 +242,8 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration,
pvt_output_parameters.ntrip_fallback_to_single = configuration->property(role + ".ntrip_fallback_to_single", pvt_output_parameters.ntrip_fallback_to_single);
pvt_output_parameters.ntrip_tls_enabled = configuration->property(role + ".ntrip_tls_enabled", pvt_output_parameters.ntrip_tls_enabled);
pvt_output_parameters.ntrip_version = configuration->property(role + ".ntrip_version", pvt_output_parameters.ntrip_version);
pvt_output_parameters.ntrip_send_gga = configuration->property(role + ".ntrip_send_gga", pvt_output_parameters.ntrip_send_gga);
pvt_output_parameters.ntrip_gga_period_ms = configuration->property(role + ".ntrip_gga_period_ms", pvt_output_parameters.ntrip_gga_period_ms);
if (pvt_output_parameters.ntrip_version != 1 && pvt_output_parameters.ntrip_version != 2)
{
@@ -339,6 +341,11 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration,
{
throw std::invalid_argument("NTRIP timeout and reconnect intervals must be within 1000 ms and 24 hours; reconnect may also be zero");
}
if (pvt_output_parameters.ntrip_send_gga &&
(pvt_output_parameters.ntrip_gga_period_ms < 1000 || pvt_output_parameters.ntrip_gga_period_ms > max_ntrip_interval_ms))
{
throw std::invalid_argument(role + ".ntrip_gga_period_ms must be within 1000 ms and 24 hours");
}
if (!std::isfinite(pvt_output_parameters.ntrip_max_correction_age_s) || pvt_output_parameters.ntrip_max_correction_age_s <= 0.0)
{
throw std::invalid_argument(role + ".ntrip_max_correction_age_s must be a finite positive value");
@@ -685,6 +685,8 @@ rtklib_pvt_gs::rtklib_pvt_gs(uint32_t nchannels,
ntrip_config.station_id = conf_.ntrip_station_id;
ntrip_config.version = conf_.ntrip_version;
ntrip_config.tls_enabled = conf_.ntrip_tls_enabled;
ntrip_config.send_gga = conf_.ntrip_send_gga;
ntrip_config.gga_period_ms = conf_.ntrip_gga_period_ms;
const auto clear_local_credentials = [&ntrip_config]() {
const auto clear_string = [](std::string* value) {
if (value->empty())
+2
View File
@@ -76,6 +76,7 @@ public:
int32_t ntrip_timeout_ms = 10000;
int32_t ntrip_reconnect_interval_ms = 10000;
int32_t ntrip_version = 2; // 2: prefer v2 with one compatible v1 retry; 1: force legacy v1
int32_t ntrip_gga_period_ms = 10000;
uint16_t rtcm_tcp_port = 0;
uint16_t rtcm_station_id = 0;
@@ -107,6 +108,7 @@ public:
bool ntrip_client_enabled = false;
bool ntrip_fallback_to_single = true;
bool ntrip_tls_enabled = false;
bool ntrip_send_gga = true; // VRS/nearest-station casters need the rover GGA to serve corrections
bool use_e6_for_pvt = true;
bool use_has_corrections = true;
bool use_unhealthy_sats = false;
@@ -875,6 +875,12 @@ public:
return d_received_first_v2_request;
}
// safe to poll while the caster is serving
bool received_gga() const
{
return d_received_gga.load();
}
private:
void record_start_failure(const char* operation)
{
@@ -1068,6 +1074,7 @@ private:
std::chrono::steady_clock::now() +
std::chrono::milliseconds(SERVER_TIMEOUT_MS);
unsigned char discard[256];
std::string upstream;
while (!d_stop_requested.load() &&
std::chrono::steady_clock::now() < close_deadline)
{
@@ -1085,6 +1092,21 @@ private:
{
break;
}
if (count > 0 && !d_received_gga.load())
{
upstream.append(reinterpret_cast<const char*>(discard),
static_cast<std::size_t>(count));
if (upstream.find("$GPGGA") != std::string::npos)
{
d_received_gga.store(true);
}
// a GGA sentence is under 128 bytes: keep only enough
// to bridge a split across recv() boundaries
if (upstream.size() > 512)
{
upstream.erase(0, upstream.size() - 128);
}
}
}
shutdown(client_socket, SHUT_RDWR);
close(client_socket);
@@ -1129,6 +1151,7 @@ private:
bool d_server_closed_after_payload = false;
bool d_server_closed_on_accept = false;
bool d_received_first_v2_request = false;
std::atomic<bool> d_received_gga{false};
std::string d_start_failure;
int d_start_errno = 0;
};
@@ -1772,6 +1795,50 @@ TEST(NtripRtcmClientTest, AuthenticatedFragmentedStreamPublishesFixedBaseSnapsho
}
TEST(NtripRtcmClientTest, RoverGgaReachesTheCasterWhenEnabled)
{
using namespace ntrip_rtcm_client_test;
Loopback_Ntrip_Caster caster;
ASSERT_TRUE(caster.start()) << caster.start_failure() << ": "
<< std::strerror(caster.start_errno());
ASSERT_NE(0, caster.port());
Ntrip_Rtcm_Client_Config config;
config.enabled = true;
config.host = "localhost";
config.port = caster.port();
config.mountpoint = "/BASE";
config.username = TRACE_USERNAME;
config.password = TRACE_PASSWORD;
config.reconnect_interval_ms = 500;
config.timeout_ms = 1000;
config.max_age_s = 1.0;
config.station_id = TEST_STATION_ID;
config.send_gga = true;
config.gga_period_ms = 100;
Ntrip_Rtcm_Client client(config);
// A VRS caster serves no corrections until it receives the rover GGA,
// so the position must be known before the stream connects
client.update_rover_position({{4797642.0, 166322.0, 4185504.0}});
ASSERT_TRUE(client.start());
const std::chrono::steady_clock::time_point gga_deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(2000);
while (!caster.received_gga() &&
std::chrono::steady_clock::now() < gga_deadline)
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
client.stop();
caster.join();
EXPECT_TRUE(caster.accepted_client());
EXPECT_TRUE(caster.received_gga());
}
TEST(NtripRtcmClientTest, NtripV2ChunkedStreamPublishesFixedBaseSnapshot)
{
using namespace ntrip_rtcm_client_test;
@@ -189,6 +189,23 @@ TEST_F(RtklibPvtNtripConfigurationTest, RejectsInvalidAgeOrTimeoutBeforeConnecti
}
TEST_F(RtklibPvtNtripConfigurationTest, RejectsInvalidGgaPeriodOnlyWhenGgaIsEnabled)
{
using namespace rtklib_pvt_ntrip_configuration_test_detail;
std::unique_ptr<InMemoryConfiguration> bad_period = make_valid_ntrip_configuration();
bad_period->supersede_property("PVT.ntrip_gga_period_ms", "999");
expect_invalid_configuration(*bad_period, "ntrip_gga_period_ms");
// the period is not used while the GGA upload is disabled
std::unique_ptr<InMemoryConfiguration> gga_disabled = make_valid_ntrip_configuration();
gga_disabled->supersede_property("PVT.ntrip_send_gga", "false");
gga_disabled->supersede_property("PVT.ntrip_gga_period_ms", "999");
std::unique_ptr<Rtklib_Pvt> adapter;
EXPECT_NO_THROW(adapter.reset(new Rtklib_Pvt(gga_disabled.get(), ROLE, 2, 0)));
ASSERT_NE(nullptr, adapter);
}
TEST_F(RtklibPvtNtripConfigurationTest, RejectsSignalsOutsideGpsL1L2BeforeConnecting)
{
using namespace rtklib_pvt_ntrip_configuration_test_detail;