diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 56ccb0d1b..106a85ac1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/docs/doxygen/other/main_page.dox b/docs/doxygen/other/main_page.dox index 8af296d7c..c4921a3f5 100644 --- a/docs/doxygen/other/main_page.dox +++ b/docs/doxygen/other/main_page.dox @@ -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 PVT.ntrip_client_enabled defaults to false. Set either PVT.ntrip_password or PVT.ntrip_password_env, 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 0 accepts any station; otherwise, set the expected RTCM station ID. Setting PVT.ntrip_reconnect_interval_ms=0 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 PVT.ntrip_max_correction_age_s are not used. Enabling this client forces a separate internal single-point solver for receiver-clock correction, independently of the RTK solver. With PVT.ntrip_fallback_to_single=true, the receiver reports an explicitly labeled SOLQ_SINGLE 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; PVT.ntrip_version=1 forces v1 from the first connection. Setting PVT.ntrip_tls_enabled=true 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 PVT.ntrip_password_env 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; PVT.ntrip_version=1 forces v1 from the first connection. Setting PVT.ntrip_tls_enabled=true 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 PVT.ntrip_password_env over a password stored in a file. VRS and nearest-station mountpoints that require rover GGA reports are supported: with PVT.ntrip_send_gga=true (the default), the client periodically sends an NMEA GGA sentence derived from the receiver's current position estimate, every PVT.ntrip_gga_period_ms (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 PVT.ntrip_send_gga=false if the rover position must not be reported to the caster; physical-base mountpoints work either way. PVT.flag_rtcm_server 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. diff --git a/src/algorithms/PVT/adapters/rtklib_pvt.cc b/src/algorithms/PVT/adapters/rtklib_pvt.cc index 14424b84f..b3da7c6ba 100644 --- a/src/algorithms/PVT/adapters/rtklib_pvt.cc +++ b/src/algorithms/PVT/adapters/rtklib_pvt.cc @@ -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"); diff --git a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc index bc609e3d4..85fd9e672 100644 --- a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc +++ b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc @@ -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()) diff --git a/src/algorithms/PVT/libs/pvt_conf.h b/src/algorithms/PVT/libs/pvt_conf.h index 1c7240e96..3837a9faa 100644 --- a/src/algorithms/PVT/libs/pvt_conf.h +++ b/src/algorithms/PVT/libs/pvt_conf.h @@ -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; diff --git a/tests/unit-tests/signal-processing-blocks/pvt/ntrip_rtcm_client_test.cc b/tests/unit-tests/signal-processing-blocks/pvt/ntrip_rtcm_client_test.cc index 0400ddba5..8ef9542db 100644 --- a/tests/unit-tests/signal-processing-blocks/pvt/ntrip_rtcm_client_test.cc +++ b/tests/unit-tests/signal-processing-blocks/pvt/ntrip_rtcm_client_test.cc @@ -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(discard), + static_cast(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 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; diff --git a/tests/unit-tests/signal-processing-blocks/pvt/rtklib_pvt_ntrip_configuration_test.cc b/tests/unit-tests/signal-processing-blocks/pvt/rtklib_pvt_ntrip_configuration_test.cc index 3bf2ee619..7097536ee 100644 --- a/tests/unit-tests/signal-processing-blocks/pvt/rtklib_pvt_ntrip_configuration_test.cc +++ b/tests/unit-tests/signal-processing-blocks/pvt/rtklib_pvt_ntrip_configuration_test.cc @@ -189,6 +189,23 @@ TEST_F(RtklibPvtNtripConfigurationTest, RejectsInvalidAgeOrTimeoutBeforeConnecti } +TEST_F(RtklibPvtNtripConfigurationTest, RejectsInvalidGgaPeriodOnlyWhenGgaIsEnabled) +{ + using namespace rtklib_pvt_ntrip_configuration_test_detail; + std::unique_ptr 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 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 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;