From e457b924bd2c523e8119bd2b86ce35782a55f9e9 Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Thu, 11 Jun 2026 20:58:48 +0200 Subject: [PATCH] Implement Glonass handling of new leap second (KP word) --- src/core/system_parameters/GLONASS_L1_L2_CA.h | 1 + .../glonass_gnav_ephemeris.cc | 5 +- .../glonass_gnav_ephemeris.h | 58 ++++--- .../glonass_gnav_navigation_message.cc | 109 +++++++++++- .../glonass_gnav_navigation_message.h | 8 + .../glonass_gnav_utc_model.h | 20 +++ .../glonass_gnav_ephemeris_test.cc | 159 +++++++++--------- .../glonass_gnav_nav_message_test.cc | 85 ++++++++++ 8 files changed, 335 insertions(+), 110 deletions(-) diff --git a/src/core/system_parameters/GLONASS_L1_L2_CA.h b/src/core/system_parameters/GLONASS_L1_L2_CA.h index 6896e6a2e..ff6efc890 100644 --- a/src/core/system_parameters/GLONASS_L1_L2_CA.h +++ b/src/core/system_parameters/GLONASS_L1_L2_CA.h @@ -337,6 +337,7 @@ const std::vector> H_N_A({{72, 5}}); // STRING 14 FRAME 5 const std::vector> B1({{6, 11}}); const std::vector> B2({{17, 10}}); +const std::vector> KP({{27, 2}}); /** \} */ diff --git a/src/core/system_parameters/glonass_gnav_ephemeris.cc b/src/core/system_parameters/glonass_gnav_ephemeris.cc index 795196568..014bf68f5 100644 --- a/src/core/system_parameters/glonass_gnav_ephemeris.cc +++ b/src/core/system_parameters/glonass_gnav_ephemeris.cc @@ -146,8 +146,9 @@ void Glonass_Gnav_Ephemeris::glot_to_gpst(double tod_offset, double glot2utc_cor // Compute the arithmetic modules to wrap around range *tow = total_sec - GLONASS_GNAV_SECONDS_PER_WEEK * floor(total_sec / GLONASS_GNAV_SECONDS_PER_WEEK); - // Perform corrections from fractional seconds - *tow += glot2utc_corr + glot2gpst_corr; + // Perform corrections from fractional seconds, plus the leap second + // announced by the KP word that is not yet in the leap second table + *tow += glot2utc_corr + glot2gpst_corr + d_kp_leap_correction_s; while (*tow >= GLONASS_GNAV_SECONDS_PER_WEEK) { *tow -= GLONASS_GNAV_SECONDS_PER_WEEK; diff --git a/src/core/system_parameters/glonass_gnav_ephemeris.h b/src/core/system_parameters/glonass_gnav_ephemeris.h index e10430324..dd207c9e2 100644 --- a/src/core/system_parameters/glonass_gnav_ephemeris.h +++ b/src/core/system_parameters/glonass_gnav_ephemeris.h @@ -87,6 +87,7 @@ public: double d_TOW{}; //!< GLONASST IN GPST seconds of week int32_t d_WN{}; //!< GLONASST IN GPST week number of the start of frame double d_tod{}; //!< Time of Day since ephemeris where decoded + double d_kp_leap_correction_s{}; //!< UTC leap second announced by the KP word and already in effect, but not yet in the receiver's leap second table [s] /*! * \brief Sets (\a d_satClkDrift)and returns the clock drift in seconds according to the User Algorithm for SV Clock Correction @@ -134,34 +135,35 @@ public: archive& BOOST_SERIALIZATION_NVP(i_satellite_freq_channel); //!< SV PRN frequency channel number archive& BOOST_SERIALIZATION_NVP(PRN); archive& BOOST_SERIALIZATION_NVP(i_satellite_slot_number); - archive& BOOST_SERIALIZATION_NVP(d_m); //!< String number within frame [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_t_k); //!< Time referenced to the beginning of the frame within the current day [hours, minutes, seconds] - archive& BOOST_SERIALIZATION_NVP(d_t_b); //!< Index of a time interval within current day according to UTC(SU) + 03 hours 00 min. [minutes] - archive& BOOST_SERIALIZATION_NVP(d_M); //!< Type of satellite transmitting navigation signal [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_gamma_n); //!< Relative deviation of predicted carrier frequency value of n- satellite from nominal value at the instant tb [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_tau_n); //!< Correction to the nth satellite time (tn) relative to GLONASS time (te) - archive& BOOST_SERIALIZATION_NVP(d_Xn); //!< Earth-fixed coordinate x of the satellite in PZ-90.02 coordinate system [km]. - archive& BOOST_SERIALIZATION_NVP(d_Yn); //!< Earth-fixed coordinate y of the satellite in PZ-90.02 coordinate system [km] - archive& BOOST_SERIALIZATION_NVP(d_Zn); //!< Earth-fixed coordinate z of the satellite in PZ-90.02 coordinate system [km] - archive& BOOST_SERIALIZATION_NVP(d_VXn); //!< Earth-fixed velocity coordinate x of the satellite in PZ-90.02 coordinate system [km/s] - archive& BOOST_SERIALIZATION_NVP(d_VYn); //!< Earth-fixed velocity coordinate y of the satellite in PZ-90.02 coordinate system [km/s] - archive& BOOST_SERIALIZATION_NVP(d_VZn); //!< Earth-fixed velocity coordinate z of the satellite in PZ-90.02 coordinate system [km/s] - archive& BOOST_SERIALIZATION_NVP(d_AXn); //!< Earth-fixed acceleration coordinate x of the satellite in PZ-90.02 coordinate system [km/s^2] - archive& BOOST_SERIALIZATION_NVP(d_AYn); //!< Earth-fixed acceleration coordinate y of the satellite in PZ-90.02 coordinate system [km/s^2] - archive& BOOST_SERIALIZATION_NVP(d_AZn); //!< Earth-fixed acceleration coordinate z of the satellite in PZ-90.02 coordinate system [km/s^2] - archive& BOOST_SERIALIZATION_NVP(d_B_n); //!< Health flag [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_P); //!< Technological parameter of control segment, indication the satellite operation mode in respect of time parameters [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_N_T); //!< Current date, calendar number of day within four-year interval starting from the 1-st of January in a leap year [days] - archive& BOOST_SERIALIZATION_NVP(d_F_T); //!< Parameter that provides the predicted satellite user range accuracy at time tb [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_n); //!< Index of the satellite transmitting given navigation signal. It corresponds to a slot number within GLONASS constellation - archive& BOOST_SERIALIZATION_NVP(d_Delta_tau_n); //!< Time difference between navigation RF signal transmitted in L2 sub- band and aviation RF signal transmitted in L1 sub-band by nth satellite. [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_E_n); //!< Characterises "age" of a current information [days] - archive& BOOST_SERIALIZATION_NVP(d_P_1); //!< Flag of the immediate data updating. - archive& BOOST_SERIALIZATION_NVP(d_P_2); //!< Flag of oddness ("1") or evenness ("0") of the value of (tb) [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_P_3); //!< Flag indicating a number of satellites for which almanac is transmitted within given frame: "1" corresponds to 5 satellites and "0" corresponds to 4 satellites [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_P_4); //!< Flag to show that ephemeris parameters are present. "1" indicates that updated ephemeris or frequency/time parameters have been uploaded by the control segment [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_l3rd_n); //!< Health flag for nth satellite; ln = 0 indicates the n-th satellite is helthy, ln = 1 indicates malfunction of this nth satellite [dimensionless] - archive& BOOST_SERIALIZATION_NVP(d_l5th_n); //!< Health flag for nth satellite; ln = 0 indicates the n-th satellite is helthy, ln = 1 indicates malfunction of this nth satellite [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_m); //!< String number within frame [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_t_k); //!< Time referenced to the beginning of the frame within the current day [hours, minutes, seconds] + archive& BOOST_SERIALIZATION_NVP(d_t_b); //!< Index of a time interval within current day according to UTC(SU) + 03 hours 00 min. [minutes] + archive& BOOST_SERIALIZATION_NVP(d_M); //!< Type of satellite transmitting navigation signal [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_gamma_n); //!< Relative deviation of predicted carrier frequency value of n- satellite from nominal value at the instant tb [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_tau_n); //!< Correction to the nth satellite time (tn) relative to GLONASS time (te) + archive& BOOST_SERIALIZATION_NVP(d_Xn); //!< Earth-fixed coordinate x of the satellite in PZ-90.02 coordinate system [km]. + archive& BOOST_SERIALIZATION_NVP(d_Yn); //!< Earth-fixed coordinate y of the satellite in PZ-90.02 coordinate system [km] + archive& BOOST_SERIALIZATION_NVP(d_Zn); //!< Earth-fixed coordinate z of the satellite in PZ-90.02 coordinate system [km] + archive& BOOST_SERIALIZATION_NVP(d_VXn); //!< Earth-fixed velocity coordinate x of the satellite in PZ-90.02 coordinate system [km/s] + archive& BOOST_SERIALIZATION_NVP(d_VYn); //!< Earth-fixed velocity coordinate y of the satellite in PZ-90.02 coordinate system [km/s] + archive& BOOST_SERIALIZATION_NVP(d_VZn); //!< Earth-fixed velocity coordinate z of the satellite in PZ-90.02 coordinate system [km/s] + archive& BOOST_SERIALIZATION_NVP(d_AXn); //!< Earth-fixed acceleration coordinate x of the satellite in PZ-90.02 coordinate system [km/s^2] + archive& BOOST_SERIALIZATION_NVP(d_AYn); //!< Earth-fixed acceleration coordinate y of the satellite in PZ-90.02 coordinate system [km/s^2] + archive& BOOST_SERIALIZATION_NVP(d_AZn); //!< Earth-fixed acceleration coordinate z of the satellite in PZ-90.02 coordinate system [km/s^2] + archive& BOOST_SERIALIZATION_NVP(d_B_n); //!< Health flag [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_P); //!< Technological parameter of control segment, indication the satellite operation mode in respect of time parameters [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_N_T); //!< Current date, calendar number of day within four-year interval starting from the 1-st of January in a leap year [days] + archive& BOOST_SERIALIZATION_NVP(d_F_T); //!< Parameter that provides the predicted satellite user range accuracy at time tb [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_n); //!< Index of the satellite transmitting given navigation signal. It corresponds to a slot number within GLONASS constellation + archive& BOOST_SERIALIZATION_NVP(d_Delta_tau_n); //!< Time difference between navigation RF signal transmitted in L2 sub- band and aviation RF signal transmitted in L1 sub-band by nth satellite. [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_E_n); //!< Characterises "age" of a current information [days] + archive& BOOST_SERIALIZATION_NVP(d_P_1); //!< Flag of the immediate data updating. + archive& BOOST_SERIALIZATION_NVP(d_P_2); //!< Flag of oddness ("1") or evenness ("0") of the value of (tb) [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_P_3); //!< Flag indicating a number of satellites for which almanac is transmitted within given frame: "1" corresponds to 5 satellites and "0" corresponds to 4 satellites [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_P_4); //!< Flag to show that ephemeris parameters are present. "1" indicates that updated ephemeris or frequency/time parameters have been uploaded by the control segment [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_l3rd_n); //!< Health flag for nth satellite; ln = 0 indicates the n-th satellite is helthy, ln = 1 indicates malfunction of this nth satellite [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_l5th_n); //!< Health flag for nth satellite; ln = 0 indicates the n-th satellite is helthy, ln = 1 indicates malfunction of this nth satellite [dimensionless] + archive& BOOST_SERIALIZATION_NVP(d_kp_leap_correction_s); //!< UTC leap second announced by the KP word and already in effect [s] } private: diff --git a/src/core/system_parameters/glonass_gnav_navigation_message.cc b/src/core/system_parameters/glonass_gnav_navigation_message.cc index c0a88f148..7be00c87a 100644 --- a/src/core/system_parameters/glonass_gnav_navigation_message.cc +++ b/src/core/system_parameters/glonass_gnav_navigation_message.cc @@ -18,9 +18,15 @@ #include "glonass_gnav_navigation_message.h" #include "MATH_CONSTANTS.h" // for TWO_N20, TWO_N30, TWO_N14, TWO_N15, TWO_N18 +#include "display.h" #include "gnss_satellite.h" -#include // for size_t +#include +#include +#include // for size_t +#include // for std::cout +#include #include // for operator<< +#include #if USE_GLOG_AND_GFLAGS #include @@ -29,6 +35,25 @@ #endif +namespace +{ +// The KP word is decoded independently by every GLONASS channel, and +// re-decoded on every superframe: print leap second announcements at the +// terminal only once per event +void print_kp_leap_second_message(const std::string& message) +{ + static std::mutex kp_print_mutex; + static std::string last_message; + std::lock_guard lock(kp_print_mutex); + if (message != last_message) + { + last_message = message; + std::cout << TEXT_RED << message << TEXT_RESET << '\n'; + } +} +} // namespace + + bool Glonass_Gnav_Navigation_Message::CRC_test(std::bitset& bits) const { uint32_t sum_bits = 0; @@ -188,6 +213,84 @@ int64_t Glonass_Gnav_Navigation_Message::read_navigation_signed(const std::bitse } +void Glonass_Gnav_Navigation_Message::update_kp_leap_second_state() +{ + // d_yr (from string 5) and d_t_k (from string 1) are needed to place the + // announcement in the calendar; a correction already in effect is kept + if (gnav_ephemeris.d_kp_leap_correction_s != 0.0 || gnav_ephemeris.d_yr < 1996.0) + { + return; + } + const int32_t announced = gnav_utc_model.announced_leap_second(); + if (announced == 0) + { + // no correction planned, or a previously announced one was cancelled + if (d_kp_announced_leap_s != 0) + { + const std::string msg = "GLONASS GNAV: KP no longer announces a UTC leap second; the correction scheduled for " + + boost::posix_time::to_simple_string(d_kp_leap_event_utc) + " UTC is cancelled"; + LOG(INFO) << msg; + print_kp_leap_second_message(msg); + } + d_kp_announced_leap_s = 0; + return; + } + + // Current UTC epoch from the broadcast date and the frame time + const boost::posix_time::ptime utc_now = gnav_ephemeris.glot_to_utc(gnav_ephemeris.d_t_k, 0.0); + const boost::gregorian::date utc_date = utc_now.date(); + const uint16_t quarter_first_month = (((utc_date.month() - 1) / 3) * 3) + 1; // 1, 4, 7, 10 + const boost::gregorian::date quarter_start(utc_date.year(), quarter_first_month, 1); + + if (utc_now - boost::posix_time::ptime(quarter_start) < boost::posix_time::hours(24)) + { + // KP still announces a correction right after a quarter boundary, so + // the leap second has just been applied to UTC and GLONASS time + apply_kp_leap_second(announced); + } + else + { + // Schedule the correction for the end of the current quarter + const boost::posix_time::ptime event_utc(quarter_start + boost::gregorian::months(3)); + const bool is_new_announcement = (d_kp_announced_leap_s != announced) || (d_kp_leap_event_utc != event_utc); + d_kp_announced_leap_s = announced; + d_kp_leap_event_utc = event_utc; + if (is_new_announcement) + { + const std::string sign = (announced > 0) ? "+1" : "-1"; + const std::string msg = "GLONASS GNAV: KP announces a UTC leap second of " + sign + " s at " + + boost::posix_time::to_simple_string(event_utc) + + " UTC. GNSS-SDR results obtained after that epoch may be wrong unless the software is updated"; + LOG(INFO) << msg; + print_kp_leap_second_message(msg); + } + } +} + + +void Glonass_Gnav_Navigation_Message::check_kp_leap_event() +{ + if (d_kp_announced_leap_s == 0 || gnav_ephemeris.d_kp_leap_correction_s != 0.0) + { + return; + } + const boost::posix_time::ptime utc_now = gnav_ephemeris.glot_to_utc(gnav_ephemeris.d_t_k + 10, 0.0); + if (utc_now >= d_kp_leap_event_utc) + { + apply_kp_leap_second(d_kp_announced_leap_s); + } +} + + +void Glonass_Gnav_Navigation_Message::apply_kp_leap_second(int32_t leap_s) +{ + gnav_ephemeris.d_kp_leap_correction_s = static_cast(leap_s); + d_kp_announced_leap_s = 0; + LOG(INFO) << "GLONASS GNAV: applying the UTC leap second of " << ((leap_s > 0) ? "+1" : "-1") + << " s announced by KP to the GLONASS to GPS time conversion"; +} + + void Glonass_Gnav_Navigation_Message::update_almanac_satellite_info() { Glonass_Gnav_Almanac& alm = gnav_almanac[i_alm_satellite_slot_number - 1]; @@ -377,6 +480,8 @@ int32_t Glonass_Gnav_Navigation_Message::string_decoder(const std::string& frame // 3). Set TOW once the year has been defined, it helps with leap second determination if (flag_ephemeris_str_1 == true) { + // Apply a leap second announced by the KP word if its epoch has been reached + check_kp_leap_event(); gnav_ephemeris.glot_to_gpst(gnav_ephemeris.d_t_k + 10, gnav_utc_model.d_tau_c, gnav_utc_model.d_tau_gps, &gnav_ephemeris.d_WN, &gnav_ephemeris.d_TOW); flag_TOW_set = true; flag_TOW_new = true; @@ -563,6 +668,8 @@ int32_t Glonass_Gnav_Navigation_Message::string_decoder(const std::string& frame // B1 and B2 are sign-magnitude values (GLONASS ICD Table 4.9) gnav_utc_model.d_B1 = static_cast(read_navigation_signed(string_bits, B1)) * TWO_N10; gnav_utc_model.d_B2 = static_cast(read_navigation_signed(string_bits, B2)) * TWO_N16; + gnav_utc_model.d_KP = static_cast(read_navigation_unsigned(string_bits, KP)); + update_kp_leap_second_state(); } else if (d_frame_ID != 0) { diff --git a/src/core/system_parameters/glonass_gnav_navigation_message.h b/src/core/system_parameters/glonass_gnav_navigation_message.h index 5663659a6..1d9041ab2 100644 --- a/src/core/system_parameters/glonass_gnav_navigation_message.h +++ b/src/core/system_parameters/glonass_gnav_navigation_message.h @@ -25,6 +25,7 @@ #include "glonass_gnav_almanac.h" #include "glonass_gnav_ephemeris.h" #include "glonass_gnav_utc_model.h" +#include #include #include #include @@ -178,6 +179,11 @@ private: int64_t read_navigation_signed(const std::bitset& bits, const std::vector>& parameter) const; bool read_navigation_bool(const std::bitset& bits, const std::vector>& parameter) const; void update_almanac_satellite_info(); + void update_kp_leap_second_state(); + void check_kp_leap_event(); + void apply_kp_leap_second(int32_t leap_s); + + boost::posix_time::ptime d_kp_leap_event_utc; // UTC epoch of the leap second event announced by KP, while armed Glonass_Gnav_Ephemeris gnav_ephemeris{}; // Ephemeris information decoded Glonass_Gnav_Utc_Model gnav_utc_model{}; // UTC model information @@ -190,6 +196,8 @@ private: uint32_t d_string_ID{}; uint32_t i_alm_satellite_slot_number{}; // SV Orbit Slot Number + int32_t d_kp_announced_leap_s{}; // UTC correction announced by KP (+1 or -1 s), 0 if not armed + bool flag_CRC_test{}; bool flag_update_slot_number{}; diff --git a/src/core/system_parameters/glonass_gnav_utc_model.h b/src/core/system_parameters/glonass_gnav_utc_model.h index 2885d552d..d6069f352 100644 --- a/src/core/system_parameters/glonass_gnav_utc_model.h +++ b/src/core/system_parameters/glonass_gnav_utc_model.h @@ -50,6 +50,7 @@ public: double d_N_A{}; //!< Calendar day number within the four-year period beginning since the leap year for Almanac data [days] double d_B1{}; //!< Coefficient to determine DeltaUT1 [s] double d_B2{}; //!< Coefficient to determine DeltaUT1 [s/msd] + double d_KP{}; //!< Notification of forthcoming leap second correction of UTC (GLONASS ICD Table 4.12) [dimensionless] /*! * \brief Computes the Coordinated Universal Time (UTC) and @@ -57,6 +58,24 @@ public: */ double utc_time(double glonass_time_corrected) const; + /*! + * \brief Returns the UTC leap second correction announced by the KP word + * for the end of the current quarter: +1 s, -1 s, or 0 if no correction + * is planned (GLONASS ICD Edition 5.1, Table 4.12) + */ + inline int32_t announced_leap_second() const + { + if (static_cast(d_KP) == 1) + { + return 1; + } + if (static_cast(d_KP) == 3) + { + return -1; + } + return 0; + } + template /*! * \brief Serialize is a boost standard method to be called by the boost XML @@ -74,6 +93,7 @@ public: archive& BOOST_SERIALIZATION_NVP(d_N_A); archive& BOOST_SERIALIZATION_NVP(d_B1); archive& BOOST_SERIALIZATION_NVP(d_B2); + archive& BOOST_SERIALIZATION_NVP(d_KP); } }; diff --git a/tests/unit-tests/system-parameters/glonass_gnav_ephemeris_test.cc b/tests/unit-tests/system-parameters/glonass_gnav_ephemeris_test.cc index 328d2b791..a838bf071 100644 --- a/tests/unit-tests/system-parameters/glonass_gnav_ephemeris_test.cc +++ b/tests/unit-tests/system-parameters/glonass_gnav_ephemeris_test.cc @@ -47,26 +47,19 @@ TEST(GlonassGnavEphemerisTest, ComputeGlonassTime) { Glonass_Gnav_Ephemeris gnav_eph; gnav_eph.d_yr = 2016; - gnav_eph.d_N_T = 367; - boost::posix_time::time_duration t(0, 0, 7560); - boost::gregorian::date d(gnav_eph.d_yr, 1, 1); - boost::gregorian::days d2(gnav_eph.d_N_T); - d = d + d2; + gnav_eph.d_N_T = 367; // 2016-01-02 in the GLONASS day scale - boost::gregorian::date expected_gdate; - boost::posix_time::time_duration expected_gtime; - - boost::posix_time::ptime gtime = gnav_eph.compute_GLONASS_time(7560); - expected_gdate = gtime.date(); - expected_gtime = gtime.time_of_day(); + const boost::posix_time::ptime gtime = gnav_eph.compute_GLONASS_time(7560); + const boost::gregorian::date gdate = gtime.date(); + const boost::posix_time::time_duration gtod = gtime.time_of_day(); // Perform assertions of decoded fields - ASSERT_TRUE(expected_gdate.year() - d.year() < FLT_EPSILON); - ASSERT_TRUE(expected_gdate.month() - d.month() < FLT_EPSILON); - ASSERT_TRUE(expected_gdate.day() - d.day() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.hours() - t.hours() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.minutes() - t.minutes() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.seconds() - t.seconds() < FLT_EPSILON); + EXPECT_EQ(gdate.year(), 2016); + EXPECT_EQ(gdate.month(), 1); + EXPECT_EQ(gdate.day(), 2); + EXPECT_EQ(gtod.hours(), 2); + EXPECT_EQ(gtod.minutes(), 6); + EXPECT_EQ(gtod.seconds(), 0); } @@ -74,26 +67,19 @@ TEST(GlonassGnavEphemerisTest, ComputeGlonassTime2019) { Glonass_Gnav_Ephemeris gnav_eph; gnav_eph.d_yr = 2019; - gnav_eph.d_N_T = 1366; - boost::posix_time::time_duration t(0, 0, 7560); - boost::gregorian::date d(gnav_eph.d_yr, 1, 1); - boost::gregorian::days d2(gnav_eph.d_N_T); - d = d + d2; + gnav_eph.d_N_T = 1366; // 2019-09-27 in the GLONASS day scale - boost::gregorian::date expected_gdate; - boost::posix_time::time_duration expected_gtime; - - boost::posix_time::ptime gtime = gnav_eph.compute_GLONASS_time(7560); - expected_gdate = gtime.date(); - expected_gtime = gtime.time_of_day(); + const boost::posix_time::ptime gtime = gnav_eph.compute_GLONASS_time(7560); + const boost::gregorian::date gdate = gtime.date(); + const boost::posix_time::time_duration gtod = gtime.time_of_day(); // Perform assertions of decoded fields - ASSERT_TRUE(expected_gdate.year() - d.year() < FLT_EPSILON); - ASSERT_TRUE(expected_gdate.month() - d.month() < FLT_EPSILON); - ASSERT_TRUE(expected_gdate.day() - d.day() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.hours() - t.hours() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.minutes() - t.minutes() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.seconds() - t.seconds() < FLT_EPSILON); + EXPECT_EQ(gdate.year(), 2019); + EXPECT_EQ(gdate.month(), 9); + EXPECT_EQ(gdate.day(), 27); + EXPECT_EQ(gtod.hours(), 2); + EXPECT_EQ(gtod.minutes(), 6); + EXPECT_EQ(gtod.seconds(), 0); } @@ -101,26 +87,19 @@ TEST(GlonassGnavEphemerisTest, ComputeGlonassTime2020) { Glonass_Gnav_Ephemeris gnav_eph; gnav_eph.d_yr = 2020; - gnav_eph.d_N_T = 62; - boost::posix_time::time_duration t(0, 0, 7560); - boost::gregorian::date d(gnav_eph.d_yr, 1, 1); - boost::gregorian::days d2(gnav_eph.d_N_T); - d = d + d2; + gnav_eph.d_N_T = 62; // 2020-03-02, a Monday - boost::gregorian::date expected_gdate; - boost::posix_time::time_duration expected_gtime; - - boost::posix_time::ptime gtime = gnav_eph.compute_GLONASS_time(7560); - expected_gdate = gtime.date(); - expected_gtime = gtime.time_of_day(); + const boost::posix_time::ptime gtime = gnav_eph.compute_GLONASS_time(7560); + const boost::gregorian::date gdate = gtime.date(); + const boost::posix_time::time_duration gtod = gtime.time_of_day(); // Perform assertions of decoded fields - ASSERT_TRUE(expected_gdate.year() - d.year() < FLT_EPSILON); - ASSERT_TRUE(expected_gdate.month() - d.month() < FLT_EPSILON); - ASSERT_TRUE(expected_gdate.day() - d.day() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.hours() - t.hours() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.minutes() - t.minutes() < FLT_EPSILON); - ASSERT_TRUE(expected_gtime.seconds() - t.seconds() < FLT_EPSILON); + EXPECT_EQ(gdate.year(), 2020); + EXPECT_EQ(gdate.month(), 3); + EXPECT_EQ(gdate.day(), 2); + EXPECT_EQ(gtod.hours(), 2); + EXPECT_EQ(gtod.minutes(), 6); + EXPECT_EQ(gtod.seconds(), 0); } @@ -132,21 +111,21 @@ TEST(GlonassGnavEphemerisTest, ConvertGlonassT2GpsT1) { Glonass_Gnav_Ephemeris gnav_eph; gnav_eph.d_yr = 2004; - gnav_eph.d_N_T = 366 + 28; + gnav_eph.d_N_T = 366 + 28; // 2004-01-29 in the GLONASS day scale, a Thursday double glo2utc = 3600 * 3; double tod = 48600; - int week = 0.0; + int week = 0; double tow = 0.0; double true_leap_sec = 13; - double true_week = 1307; - double true_tow = 480600 + true_leap_sec; + int true_week = 1255; + double true_tow = 345600 + true_leap_sec + tod; gnav_eph.glot_to_gpst(tod + glo2utc, 0.0, 0.0, &week, &tow); // Perform assertions of decoded fields - ASSERT_TRUE(week - true_week < FLT_EPSILON); - ASSERT_TRUE(tow - true_tow < FLT_EPSILON); + EXPECT_EQ(week, true_week); + EXPECT_DOUBLE_EQ(tow, true_tow); } @@ -158,21 +137,21 @@ TEST(GlonassGnavEphemerisTest, ConvertGlonassT2GpsT2) { Glonass_Gnav_Ephemeris gnav_eph; gnav_eph.d_yr = 2016; - gnav_eph.d_N_T = 268; + gnav_eph.d_N_T = 268; // 2016-09-24, a Saturday double glo2utc = 3600 * 3; double tod = 7560; - int week = 0.0; + int week = 0; double tow = 0.0; double true_leap_sec = 17; - double true_week = 1915; + int true_week = 1915; double true_tow = 518400 + true_leap_sec + tod; gnav_eph.glot_to_gpst(tod + glo2utc, 0.0, 0.0, &week, &tow); // Perform assertions of decoded fields - ASSERT_TRUE(week - true_week < FLT_EPSILON); - ASSERT_TRUE(tow - true_tow < FLT_EPSILON); + EXPECT_EQ(week, true_week); + EXPECT_DOUBLE_EQ(tow, true_tow); } @@ -184,21 +163,21 @@ TEST(GlonassGnavEphemerisTest, ConvertGlonassT2GpsT3) { Glonass_Gnav_Ephemeris gnav_eph; gnav_eph.d_yr = 2016; - gnav_eph.d_N_T = 62; + gnav_eph.d_N_T = 62; // 2016-03-02, a Wednesday double glo2utc = 3600 * 3; double tod = 7560; - int week = 0.0; + int week = 0; double tow = 0.0; double true_leap_sec = 17; - double true_week = 1886; + int true_week = 1886; double true_tow = 259200 + true_leap_sec + tod; gnav_eph.glot_to_gpst(tod + glo2utc, 0.0, 0.0, &week, &tow); // Perform assertions of decoded fields - ASSERT_TRUE(week - true_week < FLT_EPSILON); - ASSERT_TRUE(tow - true_tow < FLT_EPSILON); + EXPECT_EQ(week, true_week); + EXPECT_DOUBLE_EQ(tow, true_tow); } @@ -206,21 +185,21 @@ TEST(GlonassGnavEphemerisTest, ConvertGlonassT2GpsT4) { Glonass_Gnav_Ephemeris gnav_eph; gnav_eph.d_yr = 2019; - gnav_eph.d_N_T = 1366; + gnav_eph.d_N_T = 1366; // 2019-09-27 in the GLONASS day scale, a Friday double glo2utc = 3600 * 3; double tod = 7560; - int week = 0.0; + int week = 0; double tow = 0.0; double true_leap_sec = 18; - double true_week = 2072; - double true_tow = 447120 + true_leap_sec + tod; + int true_week = 2072; + double true_tow = 432000 + true_leap_sec + tod; gnav_eph.glot_to_gpst(tod + glo2utc, 0.0, 0.0, &week, &tow); // Perform assertions of decoded fields - ASSERT_TRUE(week - true_week < FLT_EPSILON); - ASSERT_TRUE(tow - true_tow < FLT_EPSILON); + EXPECT_EQ(week, true_week); + EXPECT_DOUBLE_EQ(tow, true_tow); } @@ -228,21 +207,21 @@ TEST(GlonassGnavEphemerisTest, ConvertGlonassT2GpsT5) { Glonass_Gnav_Ephemeris gnav_eph; gnav_eph.d_yr = 2020; - gnav_eph.d_N_T = 62; + gnav_eph.d_N_T = 62; // 2020-03-02, a Monday double glo2utc = 3600 * 3; double tod = 7560; - int week = 0.0; + int week = 0; double tow = 0.0; double true_leap_sec = 18; - double true_week = 2095; - double true_tow = 259200 + true_leap_sec + tod; + int true_week = 2095; + double true_tow = 86400 + true_leap_sec + tod; gnav_eph.glot_to_gpst(tod + glo2utc, 0.0, 0.0, &week, &tow); // Perform assertions of decoded fields - ASSERT_TRUE(week - true_week < FLT_EPSILON); - ASSERT_TRUE(tow - true_tow < FLT_EPSILON); + EXPECT_EQ(week, true_week); + EXPECT_DOUBLE_EQ(tow, true_tow); } @@ -269,6 +248,28 @@ TEST(GlonassGnavEphemerisTest, ConvertGlonassT2GpsT6) } +/*! + * \brief Testing conversion from GLONASST to GPST when a leap second announced + * by the KP word is in effect but not yet in the receiver's leap second table + */ +TEST(GlonassGnavEphemerisTest, ConvertGlonassT2GpsTKpLeapSecond) +{ + Glonass_Gnav_Ephemeris gnav_eph; + gnav_eph.d_yr = 2019; + gnav_eph.d_N_T = 1366; // 2019-09-27, a Friday + gnav_eph.d_kp_leap_correction_s = 1.0; + + int week = 0; + double tow = 0.0; + + // UTC tod 02:06:00 -> GPST tow = 5 days + 7560 s + 18 s + 1 s announced leap + gnav_eph.glot_to_gpst(7560 + 10800, 0.0, 0.0, &week, &tow); + + EXPECT_EQ(week, 2072); + EXPECT_DOUBLE_EQ(tow, 432000.0 + 7560.0 + 18.0 + 1.0); +} + + /*! * \brief Testing the GLONASST to UTC(SU) conversion of the UTC model: * tUTC = tGLO + tau_c - 3 hrs (GLONASS ICD Edition 5.1, Section 4.5) diff --git a/tests/unit-tests/system-parameters/glonass_gnav_nav_message_test.cc b/tests/unit-tests/system-parameters/glonass_gnav_nav_message_test.cc index a8b122188..259d509d4 100644 --- a/tests/unit-tests/system-parameters/glonass_gnav_nav_message_test.cc +++ b/tests/unit-tests/system-parameters/glonass_gnav_nav_message_test.cc @@ -299,3 +299,88 @@ TEST(GlonassGnavNavigationMessageTest, String14RequiresKnownFrame) EXPECT_DOUBLE_EQ(gnav_nav_message.get_almanac(10).d_n_A, 0.0); } + + +/*! + * \brief KP announces a UTC leap second correction at the end of the current + * quarter: 01 = +1 s, 11 = -1 s (GLONASS ICD Edition 5.1, Table 4.12) + */ +TEST(GlonassGnavNavigationMessageTest, KpAnnouncedLeapSecondMapping) +{ + Glonass_Gnav_Utc_Model gnav_utc_model; + + gnav_utc_model.d_KP = 0.0; + EXPECT_EQ(gnav_utc_model.announced_leap_second(), 0); + gnav_utc_model.d_KP = 1.0; + EXPECT_EQ(gnav_utc_model.announced_leap_second(), 1); + gnav_utc_model.d_KP = 2.0; + EXPECT_EQ(gnav_utc_model.announced_leap_second(), 0); + gnav_utc_model.d_KP = 3.0; + EXPECT_EQ(gnav_utc_model.announced_leap_second(), -1); +} + + +// Synthetic strings (with valid Hamming code bits) for a scenario where KP +// announces a +1 s UTC leap second at the end of a quarter. GLONASS day 913 +// of four-year interval 8 is 2026-07-01, which starts at 2026-06-30 21:00 +// UTC; the announced leap second event is at 2026-07-01 00:00:00 UTC +std::string str1_kp_a("0000100000001001111000000000000000000000000000000000000000000000000000000000000011000"); // tk = 02:30:00 (UTC 2026-06-30 23:30) +std::string str1_kp_b("0000100000001100101100000000000000000000000000000000000000000000000000000000000011000"); // tk = 03:11:00 (UTC 2026-07-01 00:11) +std::string str1_kp_c("0000100000001101111000000000000000000000000000000000000000000000000000000000011011111"); // tk = 03:30:00 (UTC 2026-07-01 00:30) +std::string str4_kp("0010000000000000000000000000000000000000000000000000000000001110010001101100001011101"); // N_T = 913, n = 22 +std::string str5_kp("0010101110010001000000000000000000000000000000000010000000000000000000000000001100000"); // N_A = 913, N_4 = 8 -> year 2026 +std::string str6_kp_f5("0011000010110000000000000000000000000000000000000000000000000000000000000000001001100"); // almanac slot 22 identifies frame 5 +std::string str14_kp1("0111000000000000000000000001000000000000000000000000000000000000000000000000011101000"); // KP = 01 (+1 s) + + +/*! + * \brief A leap second announced by KP before the end of the quarter is + * applied to the GLONASS to GPS time conversion once the event epoch is + * reached, so the TOW remains correct even if the event is not yet in the + * receiver's leap second table + */ +TEST(GlonassGnavNavigationMessageTest, KpLeapSecondAppliedAfterQuarterEnd) +{ + Glonass_Gnav_Navigation_Message gnav_nav_message; + + gnav_nav_message.string_decoder(str1_kp_a); // tk = 02:30:00 GLONASS + gnav_nav_message.set_flag_ephemeris_str_2(true); + gnav_nav_message.set_flag_ephemeris_str_3(true); + gnav_nav_message.string_decoder(str4_kp); + gnav_nav_message.string_decoder(str5_kp); // computes the TOW + const double tow_before = gnav_nav_message.get_ephemeris().d_TOW; + gnav_nav_message.string_decoder(str6_kp_f5); + gnav_nav_message.string_decoder(str14_kp1); + + // The correction is announced but the event has not occurred yet + EXPECT_DOUBLE_EQ(gnav_nav_message.get_utc_model().d_KP, 1.0); + EXPECT_DOUBLE_EQ(gnav_nav_message.get_ephemeris().d_kp_leap_correction_s, 0.0); + + gnav_nav_message.string_decoder(str1_kp_b); // tk = 03:11:00, past the event + gnav_nav_message.string_decoder(str5_kp); // recomputes the TOW + + EXPECT_DOUBLE_EQ(gnav_nav_message.get_ephemeris().d_kp_leap_correction_s, 1.0); + // 2460 s elapsed between the two TOW computations, plus the leap second + EXPECT_DOUBLE_EQ(gnav_nav_message.get_ephemeris().d_TOW - tow_before, 2461.0); +} + + +/*! + * \brief If KP still announces a correction right after a quarter boundary + * (broadcast not updated yet), the leap second has just been applied to UTC, + * and the receiver must account for it immediately + */ +TEST(GlonassGnavNavigationMessageTest, KpLeapSecondAppliedRightAfterBoundary) +{ + Glonass_Gnav_Navigation_Message gnav_nav_message; + + gnav_nav_message.string_decoder(str1_kp_c); // tk = 03:30:00 (UTC 00:30, 30 min after the boundary) + gnav_nav_message.set_flag_ephemeris_str_2(true); + gnav_nav_message.set_flag_ephemeris_str_3(true); + gnav_nav_message.string_decoder(str4_kp); + gnav_nav_message.string_decoder(str5_kp); + gnav_nav_message.string_decoder(str6_kp_f5); + gnav_nav_message.string_decoder(str14_kp1); + + EXPECT_DOUBLE_EQ(gnav_nav_message.get_ephemeris().d_kp_leap_correction_s, 1.0); +}