From e52191d476c6252e2e71708619d319bf1bfe244e Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Thu, 30 Jul 2026 11:16:46 +0200 Subject: [PATCH 1/4] Add support for RINEX 4.02 observation and navigation files Activated by setting PVT.rinex_version=4 in the configuration file (or with the -RINEX_version=4.02 command-line flag). The default behavior when the parameter is not set remains unchanged (RINEX 3.02). Observation files are generated in the 4.02 version format. Navigation files use the data record structure introduced in RINEX 4.00: - EPH ephemeris records preceded by "> EPH " header lines: GPS/QZSS LNAV, GPS/QZSS CNAV (with its own record layout, Table A10), Galileo INAV/FNAV, GLONASS FDMA (including the new BROADCAST ORBIT - 4 line with status flags, group delay, URAI and health flags), and BeiDou D1/D2. - Ionospheric corrections logged as ION data records (Klobuchar for GPS and BeiDou, NeQuick-G for Galileo) and time system corrections logged as STO records (GPUT, GAUT, GAGP, GLUT, GLGP, BDUT) instead of header lines, written as soon as the corresponding data are received. - The compulsory LEAP SECONDS header record, written blank if the data are not available yet and updated in place afterwards. - Navigation data records use the recommended 'e' exponent indicator instead of the deprecated FORTRAN 'D' notation. Also fix update_obs_header() so that the end-of-file position is preserved after rewriting the header, preventing the destructor from mistaking an updated observation file for an empty one and deleting it. --- docs/CHANGELOG.md | 12 + src/algorithms/PVT/adapters/rtklib_pvt.cc | 12 +- src/algorithms/PVT/adapters/rtklib_pvt.h | 2 +- src/algorithms/PVT/libs/rinex_printer.cc | 568 ++++++++++++++++-- src/algorithms/PVT/libs/rinex_printer.h | 17 +- .../pvt/rinex_printer_test.cc | 299 +++++++++ 6 files changed, 859 insertions(+), 51 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 682c4d16d..f26ccdcbf 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -58,6 +58,18 @@ All notable changes to GNSS-SDR will be documented in this file. ### Improvements in Interoperability: +- Added support for RINEX 4.02 output, activated by setting + `PVT.rinex_version=4` in the configuration file (or with the + `-RINEX_version=4.02` command-line flag). Observation files are generated in + the 4.02 version format, and navigation files make use of the data record + structure introduced in RINEX 4.00: `EPH` ephemeris records preceded by + `> EPH ` header lines (GPS/QZSS LNAV and CNAV, Galileo INAV + and FNAV, GLONASS FDMA including the new broadcast orbit line with status and + health flags, and BeiDou D1/D2), and ionospheric and time system corrections + logged as `ION` (Klobuchar, NeQuick-G) and `STO` (GPUT, GAUT, GAGP, GLUT, + GLGP, BDUT) data records instead of header lines, using the recommended `e` + exponent indicator. The default behavior when `PVT.rinex_version` is not set + remains unchanged (RINEX 3.02). - Added a new Signal Source implementation `Pocket_SDR_Signal_Source`, which supports [Pocket SDR FE](https://www.datagnss.com/products/pocketsdr-gnss-receiver) diff --git a/src/algorithms/PVT/adapters/rtklib_pvt.cc b/src/algorithms/PVT/adapters/rtklib_pvt.cc index 0242d9875..b8defe256 100644 --- a/src/algorithms/PVT/adapters/rtklib_pvt.cc +++ b/src/algorithms/PVT/adapters/rtklib_pvt.cc @@ -102,7 +102,11 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration, // RINEX version pvt_output_parameters.rinex_version = configuration->property(role + ".rinex_version", 3); #if USE_GLOG_AND_GFLAGS - if (FLAGS_RINEX_version == "3.01" || FLAGS_RINEX_version == "3.02" || FLAGS_RINEX_version == "3") + if (FLAGS_RINEX_version == "4.00" || FLAGS_RINEX_version == "4.01" || FLAGS_RINEX_version == "4.02" || FLAGS_RINEX_version == "4") + { + pvt_output_parameters.rinex_version = 4; + } + else if (FLAGS_RINEX_version == "3.01" || FLAGS_RINEX_version == "3.02" || FLAGS_RINEX_version == "3") { pvt_output_parameters.rinex_version = 3; } @@ -111,7 +115,11 @@ Rtklib_Pvt::Rtklib_Pvt(const ConfigurationInterface* configuration, pvt_output_parameters.rinex_version = 2; } #else - if (absl::GetFlag(FLAGS_RINEX_version) == "3.01" || absl::GetFlag(FLAGS_RINEX_version) == "3.02" || absl::GetFlag(FLAGS_RINEX_version) == "3") + if (absl::GetFlag(FLAGS_RINEX_version) == "4.00" || absl::GetFlag(FLAGS_RINEX_version) == "4.01" || absl::GetFlag(FLAGS_RINEX_version) == "4.02" || absl::GetFlag(FLAGS_RINEX_version) == "4") + { + pvt_output_parameters.rinex_version = 4; + } + else if (absl::GetFlag(FLAGS_RINEX_version) == "3.01" || absl::GetFlag(FLAGS_RINEX_version) == "3.02" || absl::GetFlag(FLAGS_RINEX_version) == "3") { pvt_output_parameters.rinex_version = 3; } diff --git a/src/algorithms/PVT/adapters/rtklib_pvt.h b/src/algorithms/PVT/adapters/rtklib_pvt.h index b58ac0026..930a4dfe0 100644 --- a/src/algorithms/PVT/adapters/rtklib_pvt.h +++ b/src/algorithms/PVT/adapters/rtklib_pvt.h @@ -66,7 +66,7 @@ class Gps_Ephemeris; * .nmea_dump_filename - ("./nmea_pvt.nmea") * .nmea_dump_devname - ("/dev/tty1") * - * .rinex_version - (3) overridden by -RINEX_version=n.nn command line argument + * .rinex_version - (3) values 2 (2.11), 3 (3.02) or 4 (4.02), overridden by -RINEX_version=n.nn command line argument * .rinexobs_rate_ms - rate at which RINEX observations are written (1000). Note that * the actual rate is the least common multiple of this value and * .output_rate_ms diff --git a/src/algorithms/PVT/libs/rinex_printer.cc b/src/algorithms/PVT/libs/rinex_printer.cc index f49f912c4..bb1e1cf45 100644 --- a/src/algorithms/PVT/libs/rinex_printer.cc +++ b/src/algorithms/PVT/libs/rinex_printer.cc @@ -1,7 +1,7 @@ /*! * \file rinex_printer.cc - * \brief Implementation of a RINEX 2.11 / 3.02 printer - * See ftp://igs.org/pub/data/format/rinex302.pdf + * \brief Implementation of a RINEX 2.11 / 3.02 / 4.02 printer + * See https://files.igs.org/pub/data/format/rinex_4.02.pdf * \author Carles Fernandez Prades, 2011-2026. cfernandez(at)cttc.es * \author Mathieu Favreau, 2025-2026. favreau.mathieu(at)hotmail.com * @@ -203,6 +203,33 @@ uint32_t to_rinex_qzss_prn(uint32_t prn) } +bool is_beidou_geo_prn(uint32_t prn) +{ + // BeiDou-2/3 GEO satellites (D2 navigation message) use PRNs 1-5 and 59-63 + return prn <= 5 || prn >= 59; +} + + +/* + * Resolves the ambiguity of a truncated week number broadcast modulo + * 2^num_bits by choosing the candidate closest to a full reference week + */ +int32_t expand_truncated_week(int32_t week, int32_t reference_week, int32_t modulus) +{ + int32_t expanded_week = week; + while ((expanded_week - reference_week) < -modulus / 2) + { + expanded_week += modulus; + } + while ((expanded_week - reference_week) > modulus / 2 - 1) + { + expanded_week -= modulus; + } + + return expanded_week; +} + + Constellation_Observables_Map get_constellation_observables_map( const std::map>& constel_signal_flags, const std::map& observables) @@ -844,6 +871,72 @@ std::string doub2for(double d, } +/* + * Convert a double to the E19.12 field used in RINEX 4 navigation data + * records (e.g., " 1.330170780420e-04"). RINEX 4 recommends the 'e' + * exponent indicator and deprecates the FORTRAN 'D' notation + * (see RINEX 4.02 specification, Section 6.8). + */ +std::string doub2e(double d) +{ + std::ostringstream ss; + ss << std::scientific << std::setprecision(12) << d; + return rightJustify(ss.str(), 19); +} + + +/* + * Formats an epoch as used in RINEX 4 navigation data records: + * yyyy mm dd hh mm ss (I4, 5(1X,I2.2)) + */ +std::string get_v4_epoch_string(const boost::posix_time::ptime& p_time) +{ + const std::string timestring = boost::posix_time::to_iso_string(p_time); + std::string line; + line += timestring.substr(0, 4); // year + line += std::string(1, ' '); + line += timestring.substr(4, 2); // month + line += std::string(1, ' '); + line += timestring.substr(6, 2); // day + line += std::string(1, ' '); + line += timestring.substr(9, 2); // hour + line += std::string(1, ' '); + line += timestring.substr(11, 2); // minutes + line += std::string(1, ' '); + line += timestring.substr(13, 2); // seconds + return line; +} + + +/* + * RINEX 4 navigation data record header line, e.g. "> EPH G01 LNAV". + * A blank prn (0) identifies system-wide records such as STO or ION + */ +std::string get_nav_record_header_line(const std::string& record_type, char sys_char, uint32_t prn, const std::string& msg_type) +{ + std::string line; + line += std::string("> "); + line += record_type; + line += std::string(1, ' '); + line += sys_char; + if (prn > 0) + { + if (prn < 10) + { + line += std::string("0"); + } + line += std::to_string(prn); + } + else + { + line += std::string(2, ' '); + } + line += std::string(1, ' '); + line += msg_type; + return line; +} + + /* * Checks that the line is 80 characters length */ @@ -1115,7 +1208,7 @@ std::string get_local_time(int version) line += std::string(5, ' '); } - if (version == 3) + if (version >= 3) { line += boost::gregorian::to_iso_string(today); line += std::string(1, ' '); @@ -1334,6 +1427,18 @@ std::string get_leap_second_line(int32_t Delta_tLS, int32_t Delta_tLSF, int32_t } +std::string get_blank_leap_second_line() +{ + std::string line; + + line += std::string(60, ' '); + line += leftJustify("LEAP SECONDS", 20); + lengthCheck(line); + + return line; +} + + std::string get_end_of_header_line() { std::string line; @@ -1599,7 +1704,7 @@ void add_constellation_obs_sat_record_lines(std::fstream& out, const std::string { std::string line; - if (version == 3) + if (version >= 3) { auto prn = it.first; if (system_char == satelliteSystem.at("QZSS")) @@ -1654,7 +1759,7 @@ void add_constellation_obs_sat_record_lines(std::fstream& out, const Signal_Enab } -std::string get_nav_sv_epoch_svclk_line(const boost::posix_time::ptime& p_utc_time, char sys_char, uint32_t prn, double value0, double value1, double value2) +std::string get_nav_sv_epoch_svclk_line(const boost::posix_time::ptime& p_utc_time, char sys_char, uint32_t prn, double value0, double value1, double value2, int version = 3) { std::string line; @@ -1667,6 +1772,19 @@ std::string get_nav_sv_epoch_svclk_line(const boost::posix_time::ptime& p_utc_ti line += std::to_string(prn); + if (version == 4) + { + line += std::string(1, ' '); + line += get_v4_epoch_string(p_utc_time); + line += doub2e(value0); + line += doub2e(value1); + line += doub2e(value2); + + lengthCheck(line); + + return line; + } + add_svclk_to_line(p_utc_time, true, 3, line); line += doub2for(value0, 18, 2); @@ -1685,6 +1803,20 @@ std::string get_nav_broadcast_orbit(const double* value0, const double* value1, { std::string line; + if (version == 4) + { + // 4X,4E19.12 with blank fields for data not known / not defined + line += std::string(4, ' '); + line += value0 ? doub2e(*value0) : std::string(19, ' '); + line += value1 ? doub2e(*value1) : std::string(19, ' '); + line += value2 ? doub2e(*value2) : std::string(19, ' '); + line += value3 ? doub2e(*value3) : std::string(19, ' '); + + lengthCheck(line); + + return line; + } + line += std::string(version + 2, ' '); line += value0 ? doub2for(*value0, 18, 2) : std::string(18, ' '); line += std::string(1, ' '); @@ -1705,6 +1837,87 @@ std::string get_nav_broadcast_orbit(const double* value0, const double* value1, } +/* + * RINEX 4 System Time Offset (STO) data record + * (see RINEX 4.02 specification, Table A33) + */ +void add_sto_record(std::fstream& out, char sys_char, const std::string& msg_type, + const boost::posix_time::ptime& ref_epoch, const std::string& sto_label, + const std::string& utc_id, double t_tm, double a0, double a1, double a2) +{ + out << get_nav_record_header_line("STO", sys_char, 0, msg_type) << '\n'; + + std::string line; + line += std::string(4, ' '); + line += get_v4_epoch_string(ref_epoch); + line += std::string(1, ' '); + line += leftJustify(sto_label, 18); + line += std::string(1, ' '); + line += leftJustify("", 18); // SBAS ID, blank if not SBUT + line += std::string(1, ' '); + line += leftJustify(utc_id, 18); + lengthCheck(line); + out << line << '\n'; + + line.clear(); + line += std::string(4, ' '); + line += doub2e(t_tm); + line += doub2e(a0); + line += doub2e(a1); + line += doub2e(a2); + lengthCheck(line); + out << line << '\n'; +} + + +/* + * RINEX 4 Ionosphere (ION) Klobuchar model data record + * (see RINEX 4.02 specification, Table A35) + */ +void add_ion_klobuchar_record(std::fstream& out, char sys_char, const std::string& msg_type, + const boost::posix_time::ptime& transmit_time, + double alpha0, double alpha1, double alpha2, double alpha3, + double beta0, double beta1, double beta2, double beta3) +{ + out << get_nav_record_header_line("ION", sys_char, 0, msg_type) << '\n'; + + std::string line; + line += std::string(4, ' '); + line += get_v4_epoch_string(transmit_time); + line += doub2e(alpha0); + line += doub2e(alpha1); + line += doub2e(alpha2); + lengthCheck(line); + out << line << '\n'; + + out << get_nav_broadcast_orbit(&alpha3, &beta0, &beta1, &beta2, 4) << '\n'; + out << get_nav_broadcast_orbit(&beta3, nullptr, nullptr, nullptr, 4) << '\n'; +} + + +/* + * RINEX 4 Ionosphere (ION) NeQuick-G model data record + * (see RINEX 4.02 specification, Table A36) + */ +void add_ion_nequick_g_record(std::fstream& out, + const boost::posix_time::ptime& transmit_time, + double ai0, double ai1, double ai2, double disturbance_flags) +{ + out << get_nav_record_header_line("ION", satelliteSystem.at("Galileo"), 0, "IFNV") << '\n'; + + std::string line; + line += std::string(4, ' '); + line += get_v4_epoch_string(transmit_time); + line += doub2e(ai0); + line += doub2e(ai1); + line += doub2e(ai2); + lengthCheck(line); + out << line << '\n'; + + out << get_nav_broadcast_orbit(&disturbance_flags, nullptr, nullptr, nullptr, 4) << '\n'; +} + + void add_obs_signal_strength(std::fstream& out) { std::string line; @@ -2141,7 +2354,12 @@ int get_version(const Signal_Enabled_Flags& flags, int version) std::cout << "Changing RINEX version to 3.02, because version 2.11 is incompatible with signal selection.\n"; } - return 3; // Only support version 2.11 and 3.02 + if (version == 4) + { + return 4; + } + + return 3; // Only support versions 2.11, 3.02 and 4.02 } } // namespace @@ -2164,7 +2382,7 @@ Rinex_Printer::Rinex_Printer(uint32_t signal_enabled_flags, observationCode(getObservationCodes()), d_flags(signal_enabled_flags), d_version(get_version(d_flags, version)), - d_stringVersion(d_version == 2 ? "2.11" : "3.02"), // Only version 2.11 and 3.02 + d_stringVersion(d_version == 2 ? "2.11" : (d_version == 4 ? "4.02" : "3.02")), // Only versions 2.11, 3.02 and 4.02 d_fake_cnav_iode(1), d_rinex_header_updated(false), d_rinex_header_gps_updated(!d_flags.has_gps), @@ -2358,6 +2576,9 @@ void Rinex_Printer::print_rinex_annotation(const Rtklib_Solver* pvt_solver, std::vector time_corr_lines; std::string leap_second_line; + // From RINEX 4 on, the ionospheric and time system corrections are no + // longer header lines: they are logged as ION and STO data records once + // the corresponding data are received (see log_rinex_nav_v4_ion_sto_records) if (d_flags.check_any_enabled(GPS_1C)) { if (d_version == 2) @@ -2369,27 +2590,36 @@ void Rinex_Printer::print_rinex_annotation(const Rtklib_Solver* pvt_solver, } else { - iono_lines.emplace_back(get_gps_iono_alpha_line(pvt_solver->gps_iono)); - iono_lines.emplace_back(get_gps_iono_beta_line(pvt_solver->gps_iono)); - time_corr_lines.emplace_back(get_gps_time_corr_line(pvt_solver->gps_utc_model, gps_ephemeris_iter->second, d_ref_gps_week)); + if (d_version == 3) + { + iono_lines.emplace_back(get_gps_iono_alpha_line(pvt_solver->gps_iono)); + iono_lines.emplace_back(get_gps_iono_beta_line(pvt_solver->gps_iono)); + time_corr_lines.emplace_back(get_gps_time_corr_line(pvt_solver->gps_utc_model, gps_ephemeris_iter->second, d_ref_gps_week)); + } leap_second_line = get_leap_second_line(pvt_solver->gps_utc_model, gps_ephemeris_iter->second, d_ref_gps_week); } } else if (d_flags.check_any_enabled(GPS_2S, GPS_L5)) { - iono_lines.emplace_back(get_gps_iono_alpha_line(pvt_solver->gps_cnav_iono)); - iono_lines.emplace_back(get_gps_iono_beta_line(pvt_solver->gps_cnav_iono)); - time_corr_lines.emplace_back(get_gps_time_corr_line(pvt_solver->gps_cnav_utc_model)); + if (d_version == 3) + { + iono_lines.emplace_back(get_gps_iono_alpha_line(pvt_solver->gps_cnav_iono)); + iono_lines.emplace_back(get_gps_iono_beta_line(pvt_solver->gps_cnav_iono)); + time_corr_lines.emplace_back(get_gps_time_corr_line(pvt_solver->gps_cnav_utc_model)); + } leap_second_line = get_leap_second_line(pvt_solver->gps_cnav_utc_model); } if (d_flags.has_galileo) { - iono_lines.emplace_back(get_galileo_iono_alpha_line(pvt_solver->galileo_iono)); - time_corr_lines.emplace_back(get_galileo_time_corr_line(pvt_solver->galileo_utc_model)); - if (pvt_solver->galileo_utc_model.flag_GGTO) + if (d_version == 3) { - time_corr_lines.emplace_back(get_gps_to_galileo_time_corr_line(pvt_solver->galileo_utc_model)); + iono_lines.emplace_back(get_galileo_iono_alpha_line(pvt_solver->galileo_iono)); + time_corr_lines.emplace_back(get_galileo_time_corr_line(pvt_solver->galileo_utc_model)); + if (pvt_solver->galileo_utc_model.flag_GGTO) + { + time_corr_lines.emplace_back(get_gps_to_galileo_time_corr_line(pvt_solver->galileo_utc_model)); + } } if (system_time_str == "GAL") @@ -2413,7 +2643,7 @@ void Rinex_Printer::print_rinex_annotation(const Rtklib_Solver* pvt_solver, time_corr_lines.emplace_back(std::move(time_corr_line)); } } - else + else if (d_version == 3) { time_corr_lines.emplace_back(get_glonass_time_corr_line(pvt_solver->glonass_gnav_utc_model)); time_corr_lines.emplace_back(get_glonass_to_gps_time_corr_line(pvt_solver->glonass_gnav_utc_model)); @@ -2421,9 +2651,12 @@ void Rinex_Printer::print_rinex_annotation(const Rtklib_Solver* pvt_solver, } if (d_flags.has_beidou) { - iono_lines.emplace_back(get_beidou_iono_alpha_line(pvt_solver->beidou_dnav_iono)); - iono_lines.emplace_back(get_beidou_iono_beta_line(pvt_solver->beidou_dnav_iono)); - time_corr_lines.emplace_back(get_beidou_time_corr_line(pvt_solver->beidou_dnav_utc_model)); + if (d_version == 3) + { + iono_lines.emplace_back(get_beidou_iono_alpha_line(pvt_solver->beidou_dnav_iono)); + iono_lines.emplace_back(get_beidou_iono_beta_line(pvt_solver->beidou_dnav_iono)); + time_corr_lines.emplace_back(get_beidou_time_corr_line(pvt_solver->beidou_dnav_utc_model)); + } if (system_time_str == "BDS") { @@ -2474,7 +2707,13 @@ void Rinex_Printer::print_rinex_annotation(const Rtklib_Solver* pvt_solver, add_obs_epoch_record(obsFile, system_time, seconds, d_version, constel_observables); add_constellation_obs_sat_record_lines(obsFile, d_flags, constel_observables, d_version); - if (!d_rinex_header_updated) + if (!d_rinex_header_updated && d_version == 4) + { + // From RINEX 4 on, the iono and time system corrections are logged + // as ION and STO data records instead of header lines + log_rinex_nav_v4_ion_sto_records(pvt_solver, rx_time, system_time_str); + } + else if (!d_rinex_header_updated) { std::string leap_second_line; std::vector nav_header_info; @@ -2564,6 +2803,130 @@ void Rinex_Printer::print_rinex_annotation(const Rtklib_Solver* pvt_solver, } +void Rinex_Printer::log_rinex_nav_v4_ion_sto_records(const Rtklib_Solver* pvt_solver, double rx_time, const std::string& system_time_str) +{ + auto& out = navFile; + bool records_written = false; + std::string leap_second_line; + const double tow = fmod(rx_time, 604800.0); + + if (!d_rinex_header_gps_updated) + { + if (d_flags.check_any_enabled(GPS_1C) && pvt_solver->gps_utc_model.A0 != 0) + { + const auto& eph = pvt_solver->gps_ephemeris_map.cbegin()->second; + const auto& utc_model = pvt_solver->gps_utc_model; + const auto& iono = pvt_solver->gps_iono; + add_ion_klobuchar_record(out, satelliteSystem.at("GPS"), "LNAV", Rinex_Printer::compute_GPS_time(eph, rx_time), + iono.alpha0, iono.alpha1, iono.alpha2, iono.alpha3, + iono.beta0, iono.beta1, iono.beta2, iono.beta3); + const int32_t wn_t = expand_lnav_utc_week(utc_model.WN_T, eph.WN, d_ref_gps_week); + add_sto_record(out, satelliteSystem.at("GPS"), "LNAV", gps_time_to_ptime(wn_t, utc_model.tot), + "GPUT", "UTC(USNO)", tow, utc_model.A0, utc_model.A1, utc_model.A2); + leap_second_line = get_leap_second_line(utc_model, eph, d_ref_gps_week); + d_rinex_header_gps_updated = true; + records_written = true; + } + else if (d_flags.check_any_enabled(GPS_2S, GPS_L5) && pvt_solver->gps_cnav_utc_model.A0 != 0) + { + const auto& eph = pvt_solver->gps_cnav_ephemeris_map.cbegin()->second; + const auto& utc_model = pvt_solver->gps_cnav_utc_model; + const auto& iono = pvt_solver->gps_cnav_iono; + add_ion_klobuchar_record(out, satelliteSystem.at("GPS"), "CNVX", Rinex_Printer::compute_GPS_time(eph, rx_time), + iono.alpha0, iono.alpha1, iono.alpha2, iono.alpha3, + iono.beta0, iono.beta1, iono.beta2, iono.beta3); + add_sto_record(out, satelliteSystem.at("GPS"), "CNVX", gps_time_to_ptime(utc_model.WN_T, utc_model.tot), + "GPUT", "UTC(USNO)", tow, utc_model.A0, utc_model.A1, utc_model.A2); + leap_second_line = get_leap_second_line(utc_model); + d_rinex_header_gps_updated = true; + records_written = true; + } + } + if (!d_rinex_header_galileo_updated && pvt_solver->galileo_utc_model.A0 != 0) + { + const auto& eph = pvt_solver->galileo_ephemeris_map.cbegin()->second; + const auto& utc_model = pvt_solver->galileo_utc_model; + const auto& iono = pvt_solver->galileo_iono; + const auto disturbance_flags = static_cast(((iono.Region1_flag ? 1U : 0U) << 4U) | + ((iono.Region2_flag ? 1U : 0U) << 3U) | + ((iono.Region3_flag ? 1U : 0U) << 2U) | + ((iono.Region4_flag ? 1U : 0U) << 1U) | + (iono.Region5_flag ? 1U : 0U)); + add_ion_nequick_g_record(out, Rinex_Printer::compute_Galileo_time(eph, rx_time), iono.ai0, iono.ai1, iono.ai2, disturbance_flags); + + // The continuous GAL week number is aligned to the continuous GPS week number + const int32_t gal_continuous_week = eph.WN + 1024 + 4096 * ((eph.WN + 1024) / 4096); + const int32_t wn_ot = expand_truncated_week(utc_model.WNot, gal_continuous_week, 256); + add_sto_record(out, satelliteSystem.at("Galileo"), "IFNV", gps_time_to_ptime(wn_ot, utc_model.tot), + "GAUT", "UTCGAL", tow, utc_model.A0, utc_model.A1, 0.0); + if (utc_model.flag_GGTO) + { + const int32_t wn_0g = expand_truncated_week(utc_model.WN_0G, gal_continuous_week, 64); + add_sto_record(out, satelliteSystem.at("Galileo"), "IFNV", gps_time_to_ptime(wn_0g, utc_model.t_0G), + "GAGP", "", tow, utc_model.A_0G, utc_model.A_1G, 0.0); + } + + if (system_time_str == "GAL") + { + leap_second_line = get_leap_second_line(utc_model); + } + d_rinex_header_galileo_updated = true; + records_written = true; + } + if (!d_rinex_header_glonass_updated && pvt_solver->glonass_gnav_utc_model.d_tau_c != 0) + { + const auto& eph = pvt_solver->glonass_gnav_ephemeris_map.cbegin()->second; + const auto& utc_model = pvt_solver->glonass_gnav_utc_model; + // The reference epoch is the start of the GLONASS day (t_0d), aligned to UTC + const boost::posix_time::ptime day_start(eph.glot_to_utc(eph.d_t_b, 0.0).date()); + const boost::posix_time::ptime p_utc_time = Rinex_Printer::compute_UTC_time(eph, rx_time); + const double utc_tow = static_cast(p_utc_time.date().day_of_week()) * 86400.0 + + static_cast(p_utc_time.time_of_day().total_seconds()); + // The broadcast GLONASS time offsets are stored as -TauC and -TauGPS + // (see RINEX 4.02 specification, Section 5.4.4 and Table 28) + add_sto_record(out, satelliteSystem.at("GLONASS"), "FDMA", day_start, + "GLUT", "UTC(SU)", utc_tow, -utc_model.d_tau_c, 0.0, 0.0); + add_sto_record(out, satelliteSystem.at("GLONASS"), "FDMA", day_start, + "GLGP", "", utc_tow, -utc_model.d_tau_gps, 0.0, 0.0); + d_rinex_header_glonass_updated = true; + records_written = true; + } + if (!d_rinex_header_beidou_updated && pvt_solver->beidou_dnav_utc_model.A0_UTC != 0) + { + const auto& eph = pvt_solver->beidou_dnav_ephemeris_map.cbegin()->second; + const auto& utc_model = pvt_solver->beidou_dnav_utc_model; + const auto& iono = pvt_solver->beidou_dnav_iono; + add_ion_klobuchar_record(out, satelliteSystem.at("Beidou"), "D1D2", Rinex_Printer::compute_BDS_time(eph, rx_time), + iono.alpha0, iono.alpha1, iono.alpha2, iono.alpha3, + iono.beta0, iono.beta1, iono.beta2, iono.beta3); + // The reference epoch is the start of the BDT week (t_0w) + add_sto_record(out, satelliteSystem.at("Beidou"), "D1D2", Rinex_Printer::compute_BDS_time(eph, 0.0), + "BDUT", "UTC(NTSC)", tow, utc_model.A0_UTC, utc_model.A1_UTC, 0.0); + + if (system_time_str == "BDS") + { + leap_second_line = get_leap_second_line(utc_model); + } + d_rinex_header_beidou_updated = true; + records_written = true; + } + + if (!records_written) + { + return; + } + + if (!leap_second_line.empty()) + { + update_obs_header(obsFile, leap_second_line); + update_nav_header_from_info(navFile, navfilename, {NavHeaderInfo("", "LEAP SECONDS", leap_second_line)}); + } + + std::cout << "The RINEX Navigation file has been updated with UTC and IONO data records.\n"; + d_rinex_header_updated = d_rinex_header_gps_updated && d_rinex_header_galileo_updated && d_rinex_header_glonass_updated && d_rinex_header_beidou_updated; +} + + void Rinex_Printer::log_rinex_nav_gps_nav(const std::map& new_eph) { std::string line; @@ -2618,11 +2981,15 @@ void Rinex_Printer::log_rinex_nav_gps_nav(const std::map out << line << '\n'; line.clear(); } - if (d_version == 3) + if (d_version >= 3) { const auto sat_system_char = is_qzss_prn(eph.PRN) ? satelliteSystem.at("QZSS") : satelliteSystem.at("GPS"); const auto rinex_prn = is_qzss_prn(eph.PRN) ? to_rinex_qzss_prn(eph.PRN) : eph.PRN; - out << get_nav_sv_epoch_svclk_line(p_utc_time, sat_system_char, rinex_prn, eph.af0, eph.af1, eph.af2) << '\n'; + if (d_version == 4) + { + out << get_nav_record_header_line("EPH", sat_system_char, rinex_prn, "LNAV") << '\n'; + } + out << get_nav_sv_epoch_svclk_line(p_utc_time, sat_system_char, rinex_prn, eph.af0, eph.af1, eph.af2, d_version) << '\n'; } // -------- BROADCAST ORBIT - 1 @@ -2746,6 +3113,51 @@ void Rinex_Printer::log_rinex_nav_gps_cnav(const std::map(eph.top); + out << get_nav_broadcast_orbit(&top_d, &eph.Cic, &eph.OMEGA_0, &eph.Cis, d_version) << '\n'; + + // -------- BROADCAST ORBIT - 4 + out << get_nav_broadcast_orbit(&eph.i_0, &eph.Crc, &eph.omega, &eph.OMEGAdot, d_version) << '\n'; + + // -------- BROADCAST ORBIT - 5 + const auto uraned0_d = static_cast(eph.URANED0); + const auto uraned1_d = static_cast(eph.URANED1); + out << get_nav_broadcast_orbit(&eph.idot, &eph.delta_ndot, &uraned0_d, &uraned1_d, d_version) << '\n'; + + // -------- BROADCAST ORBIT - 6 + const auto uraed_d = static_cast(eph.URAED); + const auto health_d = static_cast(eph.signal_health); + const auto uraned2_d = static_cast(eph.URANED2); + out << get_nav_broadcast_orbit(&uraed_d, &health_d, &eph.TGD, &uraned2_d, d_version) << '\n'; + + // -------- BROADCAST ORBIT - 7 + out << get_nav_broadcast_orbit(&eph.ISCL1, &eph.ISCL2, &eph.ISCL5I, &eph.ISCL5Q, d_version) << '\n'; + + // -------- BROADCAST ORBIT - 8 + const auto tow_d = static_cast(eph.tow); + // wn_op: GPS continuous week number with the 8-bit MT30 ambiguity resolved + const auto wn_op_d = static_cast(eph.WNop >= 0 ? expand_truncated_week(eph.WNop, eph.WN, 256) : eph.WN); + const auto flags_d = static_cast((eph.integrity_status_flag ? 1 : 0) | + (eph.l2c_phasing_flag ? 2 : 0) | + (eph.alert_flag ? 4 : 0)); + out << get_nav_broadcast_orbit(&tow_d, &wn_op_d, &flags_d, nullptr, d_version) << '\n'; + continue; + } + out << get_nav_sv_epoch_svclk_line(p_utc_time, sat_system_char, rinex_prn, eph.af0, eph.af1, eph.af2) << '\n'; // -------- BROADCAST ORBIT - 1 @@ -2809,21 +3221,26 @@ void Rinex_Printer::log_rinex_nav_gal_nav(const std::map(eph.IOD_ephemeris); - out << get_nav_broadcast_orbit(&iod_d, &eph.Crs, &eph.delta_n, &eph.M_0) << '\n'; + out << get_nav_broadcast_orbit(&iod_d, &eph.Crs, &eph.delta_n, &eph.M_0, d_version) << '\n'; // -------- BROADCAST ORBIT - 2 - out << get_nav_broadcast_orbit(&eph.Cuc, &eph.ecc, &eph.Cus, &eph.sqrtA) << '\n'; + out << get_nav_broadcast_orbit(&eph.Cuc, &eph.ecc, &eph.Cus, &eph.sqrtA, d_version) << '\n'; // -------- BROADCAST ORBIT - 3 const auto toe_d = static_cast(eph.toe); - out << get_nav_broadcast_orbit(&toe_d, &eph.Cic, &eph.OMEGA_0, &eph.Cis) << '\n'; + out << get_nav_broadcast_orbit(&toe_d, &eph.Cic, &eph.OMEGA_0, &eph.Cis, d_version) << '\n'; // -------- BROADCAST ORBIT - 4 - out << get_nav_broadcast_orbit(&eph.i_0, &eph.Crc, &eph.omega, &eph.OMEGAdot) << '\n'; + out << get_nav_broadcast_orbit(&eph.i_0, &eph.Crc, &eph.omega, &eph.OMEGAdot, d_version) << '\n'; // -------- BROADCAST ORBIT - 5 // RINEX Galileo data-source bits: bit 0 is I/NAV E1-B, bit 1 is @@ -2836,7 +3253,7 @@ void Rinex_Printer::log_rinex_nav_gal_nav(const std::map(health_bits); const auto SISA_d = static_cast(eph.SISA); - out << get_nav_broadcast_orbit(&SISA_d, &SVhealth, &eph.BGD_E1E5a, &eph.BGD_E1E5b) << '\n'; + out << get_nav_broadcast_orbit(&SISA_d, &SVhealth, &eph.BGD_E1E5a, &eph.BGD_E1E5b, d_version) << '\n'; // -------- BROADCAST ORBIT - 7 const auto tow_d = static_cast(eph.tow); - out << get_nav_broadcast_orbit(&tow_d, &zero, nullptr, nullptr) << '\n'; + if (d_version == 4) + { + // Only the transmission time of message (4X,1E19.12), see Table A13 + out << get_nav_broadcast_orbit(&tow_d, nullptr, nullptr, nullptr, d_version) << '\n'; + } + else + { + out << get_nav_broadcast_orbit(&tow_d, &zero, nullptr, nullptr) << '\n'; + } } } @@ -2894,7 +3319,7 @@ void Rinex_Printer::log_rinex_nav_glo_gnav(const std::map= 3) { // Message frame time in seconds of the UTC week (tk is given in // seconds of the GLONASS day, which is offset 3 h from UTC) @@ -2902,7 +3327,11 @@ void Rinex_Printer::log_rinex_nav_glo_gnav(const std::map(p_frame_time_utc.date().day_of_week()); const auto frame_seconds_of_day = static_cast(p_frame_time_utc.time_of_day().total_seconds()); const double message_frame_time = frame_day_of_week * 86400.0 + frame_seconds_of_day; - out << get_nav_sv_epoch_svclk_line(p_utc_time, sys_char, eph.PRN, -eph.d_tau_n, +eph.d_gamma_n, message_frame_time) << '\n'; + if (d_version == 4) + { + out << get_nav_record_header_line("EPH", sys_char, eph.PRN, "FDMA") << '\n'; + } + out << get_nav_sv_epoch_svclk_line(p_utc_time, sys_char, eph.PRN, -eph.d_tau_n, +eph.d_gamma_n, message_frame_time, d_version) << '\n'; } line.clear(); @@ -2917,6 +3346,36 @@ void Rinex_Printer::log_rinex_nav_glo_gnav(const std::map(eph.d_P_1); + int32_t p1_code = 0; + if (p1_minutes == 30) + { + p1_code = 1; + } + else if (p1_minutes == 45) + { + p1_code = 2; + } + else if (p1_minutes == 60) + { + p1_code = 3; + } + const auto status_flags = static_cast((static_cast(eph.d_M) << 7U) | + ((eph.d_P_4 ? 1U : 0U) << 6U) | + ((eph.d_P_3 ? 1U : 0U) << 5U) | + ((eph.d_P_2 ? 1U : 0U) << 4U) | + (static_cast(p1_code) << 2U) | + static_cast(eph.d_P)); + // Health flags: bit 2 holds the string 3 health bit l(3); the almanac + // health bit (bit 0) is not decoded, and bit 1 = 0 marks it as not reported + const auto health_flags = static_cast((eph.d_l3rd_n ? 1U : 0U) << 2U); + out << get_nav_broadcast_orbit(&status_flags, &eph.d_Delta_tau_n, &eph.d_F_T, &health_flags, d_version) << '\n'; + } } } @@ -2932,33 +3391,38 @@ void Rinex_Printer::log_rinex_nav_bds_dnav(const std::map(eph.toe); - out << get_nav_broadcast_orbit(&toe_d, &eph.Cic, &eph.OMEGA_0, &eph.Cis) << '\n'; + out << get_nav_broadcast_orbit(&toe_d, &eph.Cic, &eph.OMEGA_0, &eph.Cis, d_version) << '\n'; // -------- BROADCAST ORBIT - 4 - out << get_nav_broadcast_orbit(&eph.i_0, &eph.Crc, &eph.omega, &eph.OMEGAdot) << '\n'; + out << get_nav_broadcast_orbit(&eph.i_0, &eph.Crc, &eph.omega, &eph.OMEGAdot, d_version) << '\n'; // -------- BROADCAST ORBIT - 5 const auto wn_d = static_cast(eph.WN); - out << get_nav_broadcast_orbit(&eph.idot, nullptr, &wn_d, nullptr) << '\n'; + out << get_nav_broadcast_orbit(&eph.idot, nullptr, &wn_d, nullptr, d_version) << '\n'; // -------- BROADCAST ORBIT - 6 const auto acc_d = static_cast(eph.SV_accuracy); const auto health_d = static_cast(eph.SV_health); - out << get_nav_broadcast_orbit(&acc_d, &health_d, &eph.TGD1, &eph.TGD2) << '\n'; + out << get_nav_broadcast_orbit(&acc_d, &health_d, &eph.TGD1, &eph.TGD2, d_version) << '\n'; // -------- BROADCAST ORBIT - 7 const auto tow_d = static_cast(eph.tow); - out << get_nav_broadcast_orbit(&tow_d, &eph.AODC, nullptr, nullptr) << '\n'; + out << get_nav_broadcast_orbit(&tow_d, &eph.AODC, nullptr, nullptr, d_version) << '\n'; } } @@ -3004,6 +3468,13 @@ void Rinex_Printer::rinex_nav_header(std::fstream& out, add_navigation_header_start(out, type, constellation, d_version, d_stringVersion); + if (d_version == 4) + { + // From RINEX 4 on, station navigation files are to include the + // receiver line (see Table A7) + add_obs_rec_type(out); + } + for (const auto& iono_line : iono_lines) { out << iono_line << '\n'; @@ -3018,6 +3489,12 @@ void Rinex_Printer::rinex_nav_header(std::fstream& out, { out << leap_second_line << '\n'; } + else if (d_version == 4) + { + // LEAP SECONDS is a compulsory header record in RINEX 4 (see Table A7); + // write a blank record to be updated once the data are received + out << get_blank_leap_second_line() << '\n'; + } out << get_end_of_header_line() << '\n'; } @@ -3161,7 +3638,7 @@ void Rinex_Printer::rinex_obs_header(std::fstream& out, // -------- SYS /PHASE SHIFTS - if (d_version == 3 && d_flags.has_glonass) + if (d_version >= 3 && d_flags.has_glonass) { // -------- GLONASS SLOT / FRQ # add_obs_glonass_slot_freq(out); @@ -3177,6 +3654,9 @@ void Rinex_Printer::rinex_obs_header(std::fstream& out, void Rinex_Printer::update_obs_header(std::fstream& out, const std::string& leap_second_line) const { + // Keep the end-of-data position, so that the file is not mistaken + // for an empty one (and thus deleted) when the printer is destroyed + const int64_t pos = out.tellp(); out.seekp(0); std::vector data; @@ -3196,7 +3676,7 @@ void Rinex_Printer::update_obs_header(std::fstream& out, const std::string& leap data.push_back(line_str); } - override_stream_with_new_data(out, obsfilename, data, 0); + override_stream_with_new_data(out, obsfilename, data, pos); } diff --git a/src/algorithms/PVT/libs/rinex_printer.h b/src/algorithms/PVT/libs/rinex_printer.h index 1f2924171..84127b029 100644 --- a/src/algorithms/PVT/libs/rinex_printer.h +++ b/src/algorithms/PVT/libs/rinex_printer.h @@ -1,7 +1,7 @@ /*! * \file rinex_printer.h - * \brief Interface of a RINEX 2.11 / 3.01 printer - * See ftp://igs.org/pub/data/format/rinex301.pdf + * \brief Interface of a RINEX 2.11 / 3.02 / 4.02 printer + * See https://files.igs.org/pub/data/format/rinex_4.02.pdf * * Receiver Independent EXchange Format (RINEX): * The first proposal for the Receiver Independent Exchange Format RINEX @@ -183,6 +183,15 @@ private: const std::vector& time_corr_lines, const std::string& leap_second_line) const; + /* + * Logs RINEX 4 ION and STO data records for the enabled constellations as + * soon as the iono model and UTC data become available, and updates the + * LEAP SECONDS header line of the observation and navigation files + */ + void log_rinex_nav_v4_ion_sto_records(const Rtklib_Solver* pvt_solver, + double rx_time, + const std::string& system_time_str); + /* * Computes the BDS Time and returns a boost::posix_time::ptime object * \details Function used to convert the observation time into BDT time which is used @@ -240,8 +249,8 @@ private: const Signal_Enabled_Flags d_flags; - const int d_version; // RINEX version (2 for 2.10/2.11 and 3 for 3.01) - const std::string d_stringVersion; // RINEX version (2.10/2.11 or 3.01/3.02) + const int d_version; // RINEX version (2 for 2.11, 3 for 3.02, 4 for 4.02) + const std::string d_stringVersion; // RINEX version (2.11, 3.02 or 4.02) double d_fake_cnav_iode; bool d_rinex_header_updated; diff --git a/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc b/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc index 8a0452c54..e5a828589 100644 --- a/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc +++ b/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc @@ -203,6 +203,88 @@ std::string find_rinex_header_line(const std::string& filename, const std::strin } +std::string find_line_starting_with(const std::string& filename, const std::string& prefix) +{ + std::fstream fstr(filename.c_str(), std::fstream::in); + if (!fstr.is_open()) + { + return {}; + } + fstr.seekg(0); + + std::string line_str; + while (!fstr.eof()) + { + std::getline(fstr, line_str); + if (line_str.compare(0, prefix.length(), prefix) == 0) + { + return line_str; + } + } + + return {}; +} + + +std::string find_line_containing(const std::string& filename, const std::string& text) +{ + std::fstream fstr(filename.c_str(), std::fstream::in); + if (!fstr.is_open()) + { + return {}; + } + fstr.seekg(0); + + std::string line_str; + while (!fstr.eof()) + { + std::getline(fstr, line_str); + if (line_str.find(text) != std::string::npos) + { + return line_str; + } + } + + return {}; +} + + +int32_t count_record_body_lines(const std::string& filename, const std::string& record_header) +{ + std::fstream fstr(filename.c_str(), std::fstream::in); + if (!fstr.is_open()) + { + return -1; + } + fstr.seekg(0); + + std::string line_str; + bool found = false; + int32_t body_lines = 0; + while (!fstr.eof()) + { + std::getline(fstr, line_str); + if (!found) + { + if (line_str.compare(0, record_header.length(), record_header) == 0) + { + found = true; + } + } + else + { + if (line_str.empty() || line_str[0] == '>') + { + break; + } + body_lines++; + } + } + + return found ? body_lines : -1; +} + + int32_t expand_test_lnav_utc_week(int32_t utc_week, int32_t reference_week) { int32_t expanded_week = utc_week; @@ -273,6 +355,223 @@ TEST_F(RinexPrinterTest, GpsUtcHeaderExpandsEightBitWeekFields) } +TEST_F(RinexPrinterTest, Rinex4GpsNavAndObs) +{ + Pvt_Conf conf; + conf.use_e6_for_pvt = false; + const auto signal_enabled_flags = GPS_1C; + auto pvt_solution = std::make_shared(rtk, conf, "filename", signal_enabled_flags, false, false); + + Gps_Ephemeris eph; + eph.PRN = 1; + eph.WN = 512; + pvt_solution->gps_ephemeris_map[1] = eph; + pvt_solution->gps_iono.alpha0 = 1.1175870895385742e-08; + pvt_solution->gps_iono.alpha1 = -7.4505805969238281e-09; + pvt_solution->gps_iono.alpha2 = -5.9604644775390625e-08; + pvt_solution->gps_iono.alpha3 = 1.1920928955078125e-07; + pvt_solution->gps_iono.beta0 = 90112.0; + pvt_solution->gps_iono.beta1 = -16384.0; + pvt_solution->gps_iono.beta2 = -131072.0; + pvt_solution->gps_iono.beta3 = 589824.0; + pvt_solution->gps_utc_model.A0 = 9.3132257461547852e-10; + pvt_solution->gps_utc_model.A1 = 8.8817841970012523e-16; + pvt_solution->gps_utc_model.tot = 233472; + pvt_solution->gps_utc_model.WN_T = 5; + pvt_solution->gps_utc_model.DeltaT_LS = 18; + pvt_solution->gps_utc_model.WN_LSF = 7; + pvt_solution->gps_utc_model.DN = 7; + pvt_solution->gps_utc_model.DeltaT_LSF = 18; + + Gnss_Synchro gs{}; + gs.System = 'G'; + gs.PRN = 1; + std::memcpy(static_cast(gs.Signal), "1C", 3); + gs.Pseudorange_m = 22000000.0; + gs.Carrier_phase_rads = 23.4; + gs.Carrier_Doppler_hz = 1534.0; + gs.CN0_dB_hz = 42.0; + gs.Flag_valid_pseudorange = true; + std::map gnss_observables_map; + gnss_observables_map[1] = gs; + + auto rp = std::make_shared(signal_enabled_flags, 4); + rp->print_rinex_annotation(pvt_solution.get(), gnss_observables_map, 42.0, true); + + const std::string obsfile = rp->get_obsfilename(); + const std::string navfile = rp->get_navfilename()[0]; + rp = nullptr; // close the RINEX files so we can inspect them + + const std::string obs_version_line = find_rinex_header_line(obsfile, "RINEX VERSION / TYPE"); + EXPECT_NE(std::string::npos, obs_version_line.find("4.02")); + EXPECT_NE(std::string::npos, obs_version_line.find("OBSERVATION DATA")); + + const std::string nav_version_line = find_rinex_header_line(navfile, "RINEX VERSION / TYPE"); + EXPECT_NE(std::string::npos, nav_version_line.find("4.02")); + EXPECT_NE(std::string::npos, nav_version_line.find("N: GNSS NAV DATA")); + + // From RINEX 4 on, the iono and time system corrections are ION / STO data + // records instead of header lines + EXPECT_TRUE(find_rinex_header_line(navfile, "IONOSPHERIC CORR").empty()); + EXPECT_TRUE(find_rinex_header_line(navfile, "TIME SYSTEM CORR").empty()); + + const std::string leap_second_line = find_rinex_header_line(navfile, "LEAP SECONDS"); + EXPECT_NE(std::string::npos, leap_second_line.find("18")); + + EXPECT_FALSE(find_line_starting_with(navfile, "> EPH G01 LNAV").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> ION G LNAV").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> STO G LNAV").empty()); + + const std::string sto_line = find_line_containing(navfile, "GPUT"); + EXPECT_NE(std::string::npos, sto_line.find("UTC(USNO)")); + + // RINEX 4 navigation records use the 'e' exponent indicator instead of 'D' + EXPECT_TRUE(find_line_containing(navfile, "D+0").empty()); + EXPECT_TRUE(find_line_containing(navfile, "D-0").empty()); + EXPECT_FALSE(find_line_containing(navfile, "e+0").empty()); + + fs::remove(obsfile); + fs::remove(navfile); +} + + +TEST_F(RinexPrinterTest, Rinex4GalileoNav) +{ + Pvt_Conf conf; + conf.use_e6_for_pvt = false; + const auto signal_enabled_flags = GAL_1B; + auto pvt_solution = std::make_shared(rtk, conf, "filename", signal_enabled_flags, false, false); + + auto eph = Galileo_Ephemeris(); + eph.PRN = 11; + eph.WN = 120; + pvt_solution->galileo_ephemeris_map[11] = std::move(eph); + pvt_solution->galileo_iono.ai0 = 45.75; + pvt_solution->galileo_iono.ai1 = 0.1875; + pvt_solution->galileo_iono.ai2 = 0.0135; + pvt_solution->galileo_utc_model.A0 = -9.3132257461547852e-10; + pvt_solution->galileo_utc_model.A1 = -8.8817841970012523e-16; + pvt_solution->galileo_utc_model.tot = 432000; + pvt_solution->galileo_utc_model.WNot = 41; + pvt_solution->galileo_utc_model.Delta_tLS = 18; + pvt_solution->galileo_utc_model.WN_LSF = 137; + pvt_solution->galileo_utc_model.DN = 7; + pvt_solution->galileo_utc_model.Delta_tLSF = 18; + + std::map gnss_observables_map; + Gnss_Synchro gs{}; + gs.PRN = 11; + gnss_observables_map[11] = std::move(gs); + + auto rp = std::make_shared(signal_enabled_flags, 4); + rp->print_rinex_annotation(pvt_solution.get(), gnss_observables_map, 0.0, true); + + const std::string obsfile = rp->get_obsfilename(); + const std::string navfile = rp->get_navfilename()[0]; + rp = nullptr; // close the RINEX files so we can inspect them + + EXPECT_FALSE(find_line_starting_with(navfile, "> EPH E11 INAV").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> ION E IFNV").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> STO E IFNV").empty()); + + const std::string sto_line = find_line_containing(navfile, "GAUT"); + EXPECT_NE(std::string::npos, sto_line.find("UTCGAL")); + + fs::remove(obsfile); + fs::remove(navfile); +} + + +TEST_F(RinexPrinterTest, Rinex4GlonassNav) +{ + Pvt_Conf conf; + conf.use_e6_for_pvt = false; + const auto signal_enabled_flags = GLO_1G; + auto pvt_solution = std::make_shared(rtk, conf, "filename", signal_enabled_flags, false, false); + + auto eph = Glonass_Gnav_Ephemeris(); + eph.PRN = 1; + eph.d_yr = 2020; + eph.d_N_T = 260; + pvt_solution->glonass_gnav_ephemeris_map[1] = std::move(eph); + pvt_solution->glonass_gnav_utc_model.d_tau_c = -9.3132257461547852e-09; + pvt_solution->glonass_gnav_utc_model.d_tau_gps = 1.8626451492309570e-09; + + std::map gnss_observables_map; + Gnss_Synchro gs{}; + gs.PRN = 1; + gnss_observables_map[1] = std::move(gs); + + auto rp = std::make_shared(signal_enabled_flags, 4); + rp->print_rinex_annotation(pvt_solution.get(), gnss_observables_map, 0.0, true); + + const std::string obsfile = rp->get_obsfilename(); + const std::string navfile = rp->get_navfilename()[0]; + rp = nullptr; // close the RINEX files so we can inspect them + + EXPECT_FALSE(find_line_starting_with(navfile, "> EPH R01 FDMA").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> STO R FDMA").empty()); + + const std::string sto_line = find_line_containing(navfile, "GLUT"); + EXPECT_NE(std::string::npos, sto_line.find("UTC(SU)")); + EXPECT_FALSE(find_line_containing(navfile, "GLGP").empty()); + + // The FDMA record has the SV / EPOCH / SV CLK line plus four broadcast + // orbit lines (see RINEX 4.02 specification, Table A15) + EXPECT_EQ(5, count_record_body_lines(navfile, "> EPH R01 FDMA")); + + fs::remove(obsfile); + fs::remove(navfile); +} + + +TEST_F(RinexPrinterTest, Rinex4GpsCnavNav) +{ + Pvt_Conf conf; + conf.use_e6_for_pvt = false; + const auto signal_enabled_flags = GPS_2S; + auto pvt_solution = std::make_shared(rtk, conf, "filename", signal_enabled_flags, false, false); + + Gps_CNAV_Ephemeris eph; + eph.PRN = 1; + eph.WN = 2338; + eph.WNop = 34; + pvt_solution->gps_cnav_ephemeris_map[1] = eph; + pvt_solution->gps_cnav_iono.alpha0 = 1.1175870895385742e-08; + pvt_solution->gps_cnav_iono.beta0 = 90112.0; + pvt_solution->gps_cnav_utc_model.A0 = 9.3132257461547852e-10; + pvt_solution->gps_cnav_utc_model.A1 = 8.8817841970012523e-16; + pvt_solution->gps_cnav_utc_model.tot = 233472; + pvt_solution->gps_cnav_utc_model.WN_T = 2338; + pvt_solution->gps_cnav_utc_model.DeltaT_LS = 18; + + std::map gnss_observables_map; + Gnss_Synchro gs{}; + gs.System = 'G'; + gs.PRN = 1; + std::memcpy(static_cast(gs.Signal), "2S", 3); + gnss_observables_map[1] = std::move(gs); + + auto rp = std::make_shared(signal_enabled_flags, 4); + rp->print_rinex_annotation(pvt_solution.get(), gnss_observables_map, 42.0, true); + + const std::string obsfile = rp->get_obsfilename(); + const std::string navfile = rp->get_navfilename()[0]; + rp = nullptr; // close the RINEX files so we can inspect them + + EXPECT_FALSE(find_line_starting_with(navfile, "> EPH G01 CNAV").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> ION G CNVX").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> STO G CNVX").empty()); + + // The CNAV record has the SV / EPOCH / SV CLK line plus eight broadcast + // orbit lines (see RINEX 4.02 specification, Table A10) + EXPECT_EQ(9, count_record_body_lines(navfile, "> EPH G01 CNAV")); + + fs::remove(obsfile); + fs::remove(navfile); +} + + TEST_F(RinexPrinterTest, GalileoObsHeader) { Pvt_Conf conf; From b327290af14faa9b20f504dbf9315ecc02dc756a Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Thu, 30 Jul 2026 11:27:30 +0200 Subject: [PATCH 2/4] Update changelog --- docs/CHANGELOG.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f26ccdcbf..003aaadb1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -62,14 +62,8 @@ All notable changes to GNSS-SDR will be documented in this file. `PVT.rinex_version=4` in the configuration file (or with the `-RINEX_version=4.02` command-line flag). Observation files are generated in the 4.02 version format, and navigation files make use of the data record - structure introduced in RINEX 4.00: `EPH` ephemeris records preceded by - `> EPH ` header lines (GPS/QZSS LNAV and CNAV, Galileo INAV - and FNAV, GLONASS FDMA including the new broadcast orbit line with status and - health flags, and BeiDou D1/D2), and ionospheric and time system corrections - logged as `ION` (Klobuchar, NeQuick-G) and `STO` (GPUT, GAUT, GAGP, GLUT, - GLGP, BDUT) data records instead of header lines, using the recommended `e` - exponent indicator. The default behavior when `PVT.rinex_version` is not set - remains unchanged (RINEX 3.02). + structure introduced in RINEX 4.00. The default behavior when + `PVT.rinex_version` is not set remains unchanged (RINEX 3.02). - Added a new Signal Source implementation `Pocket_SDR_Signal_Source`, which supports [Pocket SDR FE](https://www.datagnss.com/products/pocketsdr-gnss-receiver) From 4f570e786797aa8a154eea68ba3744a3a7458665 Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Thu, 30 Jul 2026 12:21:59 +0200 Subject: [PATCH 3/4] Store QZSS UTC and iono parameters separately and log RINEX 4 QZUT records The Klobuchar coefficients and the UTC(NICT) offset broadcast by QZSS satellites in LNAV subframe 4 (both the wide-area and Japan-area pages) were stored in the shared GPS structures, overwriting the GPS-sourced values in mixed GPS + QZSS configurations and making it impossible to label them correctly in RINEX output. New Qzss_Utc_Model and Qzss_Iono storage classes are now published by the QZSS-configured L1 C/A telemetry decoder and kept in dedicated Rtklib_Solver members. With this in place: - RINEX 4.02 navigation files get "> STO J LNAV" QZUT records with the UTC(NICT) identifier and "> ION J LNAV" Klobuchar records, and the LEAP SECONDS header is updated from QZSS data in QZSS-only setups. - QZSS-sourced parameters are no longer mislabeled as GPS corrections (GPUT / GPS ION or TIME SYSTEM CORR / IONOSPHERIC CORR lines). - The QZSS slots of the RTKLIB navigation structure (utc_qzs, ion_qzs) are now populated; QZSS-only configurations keep feeding the broadcast ionospheric model and the leap second count as before. - The XML storage output gains qzss_utc_model.xml and qzss_iono.xml. Co-Authored-By: Claude Fable 5 --- docs/CHANGELOG.md | 7 ++ .../PVT/gnuradio_blocks/rtklib_pvt_gs.cc | 86 +++++++++++++++++++ .../PVT/gnuradio_blocks/rtklib_pvt_gs.h | 2 + src/algorithms/PVT/libs/rinex_printer.cc | 25 +++++- src/algorithms/PVT/libs/rinex_printer.h | 1 + src/algorithms/PVT/libs/rtklib_solver.cc | 37 ++++++++ src/algorithms/PVT/libs/rtklib_solver.h | 5 ++ .../gps_l1_ca_telemetry_decoder_gs.cc | 64 +++++++++++--- src/core/system_parameters/CMakeLists.txt | 2 + src/core/system_parameters/qzss_iono.h | 45 ++++++++++ src/core/system_parameters/qzss_utc_model.h | 45 ++++++++++ .../pvt/rinex_printer_test.cc | 53 ++++++++++++ 12 files changed, 359 insertions(+), 13 deletions(-) create mode 100644 src/core/system_parameters/qzss_iono.h create mode 100644 src/core/system_parameters/qzss_utc_model.h diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 003aaadb1..7a4aa413b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -64,6 +64,13 @@ All notable changes to GNSS-SDR will be documented in this file. the 4.02 version format, and navigation files make use of the data record structure introduced in RINEX 4.00. The default behavior when `PVT.rinex_version` is not set remains unchanged (RINEX 3.02). +- The ionospheric Klobuchar coefficients and the UTC(NICT) offset parameters + broadcast by QZSS satellites are now stored separately from the GPS ones + instead of overwriting them. This enables the QZUT / QZSS ION RINEX 4 data + records, prevents QZSS-sourced parameters from being mislabeled as GPS + corrections in mixed GPS + QZSS configurations, feeds the QZSS slots of the + RTKLIB navigation structure, and adds `qzss_utc_model.xml` / `qzss_iono.xml` + to the XML storage output. - Added a new Signal Source implementation `Pocket_SDR_Signal_Source`, which supports [Pocket SDR FE](https://www.datagnss.com/products/pocketsdr-gnss-receiver) diff --git a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc index 9bf0fe4e7..bffb8fe3f 100644 --- a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc +++ b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc @@ -57,6 +57,8 @@ #include "osnma_data.h" #include "pvt_conf.h" #include "qzss.h" +#include "qzss_iono.h" +#include "qzss_utc_model.h" #include "rinex_printer.h" #include "rtcm_printer.h" #include "rtklib_rtkcmn.h" @@ -145,6 +147,8 @@ rtklib_pvt_gs::rtklib_pvt_gs(uint32_t nchannels, d_gps_cnav_iono_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_gps_cnav_utc_model_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_gps_almanac_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), + d_qzss_iono_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), + d_qzss_utc_model_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_galileo_ephemeris_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_galileo_reduced_ced_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_galileo_iono_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), @@ -874,6 +878,66 @@ rtklib_pvt_gs::~rtklib_pvt_gs() LOG(INFO) << "Failed to save GPS CNAV ionospheric model parameters, not valid data"; } + // Save QZSS UTC model parameters + file_name = d_xml_base_path + "qzss_utc_model.xml"; + if (d_internal_pvt_solver->qzss_utc_model.valid) + { + std::ofstream ofs; + try + { + ofs.open(file_name.c_str(), std::ofstream::trunc | std::ofstream::out); + boost::archive::xml_oarchive xml(ofs); + xml << boost::serialization::make_nvp("GNSS-SDR_qzss_utc_model", d_internal_pvt_solver->qzss_utc_model); + LOG(INFO) << "Saved QZSS UTC model parameters"; + } + catch (const boost::archive::archive_exception& e) + { + LOG(WARNING) << e.what(); + } + catch (const std::ofstream::failure& e) + { + LOG(WARNING) << "Problem opening output XML file"; + } + catch (const std::exception& e) + { + LOG(WARNING) << e.what(); + } + } + else + { + LOG(INFO) << "Failed to save QZSS UTC model parameters, not valid data"; + } + + // Save QZSS iono parameters + file_name = d_xml_base_path + "qzss_iono.xml"; + if (d_internal_pvt_solver->qzss_iono.valid == true) + { + std::ofstream ofs; + try + { + ofs.open(file_name.c_str(), std::ofstream::trunc | std::ofstream::out); + boost::archive::xml_oarchive xml(ofs); + xml << boost::serialization::make_nvp("GNSS-SDR_qzss_iono_model", d_internal_pvt_solver->qzss_iono); + LOG(INFO) << "Saved QZSS ionospheric model parameters"; + } + catch (const boost::archive::archive_exception& e) + { + LOG(WARNING) << e.what(); + } + catch (const std::ofstream::failure& e) + { + LOG(WARNING) << "Problem opening output XML file"; + } + catch (const std::exception& e) + { + LOG(WARNING) << e.what(); + } + } + else + { + LOG(INFO) << "Failed to save QZSS ionospheric model parameters, not valid data"; + } + // Save Galileo iono parameters file_name = d_xml_base_path + "gal_iono.xml"; if (d_internal_pvt_solver->galileo_iono.ai0 != 0.0) @@ -1271,6 +1335,28 @@ void rtklib_pvt_gs::msg_handler_telemetry(const pmt::pmt_t& msg) } DLOG(INFO) << "New UTC record has arrived"; } + else if (msg_type_hash_code == d_qzss_iono_sptr_type_hash_code) + { + // ### QZSS IONO ### + const auto qzss_iono = wht::any_cast>(pmt::any_ref(msg)); + d_internal_pvt_solver->qzss_iono = *qzss_iono; + if (d_enable_rx_clock_correction == true) + { + d_user_pvt_solver->qzss_iono = *qzss_iono; + } + DLOG(INFO) << "New QZSS IONO record has arrived"; + } + else if (msg_type_hash_code == d_qzss_utc_model_sptr_type_hash_code) + { + // ### QZSS UTC MODEL ### + const auto qzss_utc_model = wht::any_cast>(pmt::any_ref(msg)); + d_internal_pvt_solver->qzss_utc_model = *qzss_utc_model; + if (d_enable_rx_clock_correction == true) + { + d_user_pvt_solver->qzss_utc_model = *qzss_utc_model; + } + DLOG(INFO) << "New QZSS UTC record has arrived"; + } else if (msg_type_hash_code == d_gps_cnav_ephemeris_sptr_type_hash_code) { // ### GPS CNAV message ### diff --git a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h index 4e4f1c17e..96e424e9d 100644 --- a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h +++ b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h @@ -224,6 +224,8 @@ private: const size_t d_gps_cnav_iono_sptr_type_hash_code; const size_t d_gps_cnav_utc_model_sptr_type_hash_code; const size_t d_gps_almanac_sptr_type_hash_code; + const size_t d_qzss_iono_sptr_type_hash_code; + const size_t d_qzss_utc_model_sptr_type_hash_code; const size_t d_galileo_ephemeris_sptr_type_hash_code; const size_t d_galileo_reduced_ced_sptr_type_hash_code; const size_t d_galileo_iono_sptr_type_hash_code; diff --git a/src/algorithms/PVT/libs/rinex_printer.cc b/src/algorithms/PVT/libs/rinex_printer.cc index bb1e1cf45..15c11a60f 100644 --- a/src/algorithms/PVT/libs/rinex_printer.cc +++ b/src/algorithms/PVT/libs/rinex_printer.cc @@ -2389,6 +2389,7 @@ Rinex_Printer::Rinex_Printer(uint32_t signal_enabled_flags, d_rinex_header_galileo_updated(!d_flags.has_galileo), d_rinex_header_glonass_updated(!d_flags.has_glonass), d_rinex_header_beidou_updated(!d_flags.has_beidou), + d_rinex_header_qzss_updated(!d_flags.check_any_enabled(QZS_J1)), d_rinex_header_written(false), d_ref_gps_week(ref_gps_week), navfilename(getNavFilePath(d_flags, d_version, base_name, base_rinex_path)), @@ -2842,6 +2843,28 @@ void Rinex_Printer::log_rinex_nav_v4_ion_sto_records(const Rtklib_Solver* pvt_so records_written = true; } } + if (!d_rinex_header_qzss_updated && pvt_solver->qzss_utc_model.A0 != 0) + { + // QZSS weeks are aligned with GPS weeks, so any ephemeris of the + // shared GPS/QZSS LNAV map works as reference to resolve the + // 8-bit UTC week number + const auto& eph = pvt_solver->gps_ephemeris_map.cbegin()->second; + const auto& utc_model = pvt_solver->qzss_utc_model; + const auto& iono = pvt_solver->qzss_iono; + add_ion_klobuchar_record(out, satelliteSystem.at("QZSS"), "LNAV", Rinex_Printer::compute_GPS_time(eph, rx_time), + iono.alpha0, iono.alpha1, iono.alpha2, iono.alpha3, + iono.beta0, iono.beta1, iono.beta2, iono.beta3); + const int32_t wn_t = expand_lnav_utc_week(utc_model.WN_T, eph.WN, d_ref_gps_week); + add_sto_record(out, satelliteSystem.at("QZSS"), "LNAV", gps_time_to_ptime(wn_t, utc_model.tot), + "QZUT", "UTC(NICT)", tow, utc_model.A0, utc_model.A1, utc_model.A2); + + if (system_time_str == "QZS") + { + leap_second_line = get_leap_second_line(utc_model, eph, d_ref_gps_week); + } + d_rinex_header_qzss_updated = true; + records_written = true; + } if (!d_rinex_header_galileo_updated && pvt_solver->galileo_utc_model.A0 != 0) { const auto& eph = pvt_solver->galileo_ephemeris_map.cbegin()->second; @@ -2923,7 +2946,7 @@ void Rinex_Printer::log_rinex_nav_v4_ion_sto_records(const Rtklib_Solver* pvt_so } std::cout << "The RINEX Navigation file has been updated with UTC and IONO data records.\n"; - d_rinex_header_updated = d_rinex_header_gps_updated && d_rinex_header_galileo_updated && d_rinex_header_glonass_updated && d_rinex_header_beidou_updated; + d_rinex_header_updated = d_rinex_header_gps_updated && d_rinex_header_qzss_updated && d_rinex_header_galileo_updated && d_rinex_header_glonass_updated && d_rinex_header_beidou_updated; } diff --git a/src/algorithms/PVT/libs/rinex_printer.h b/src/algorithms/PVT/libs/rinex_printer.h index 84127b029..36fb752e7 100644 --- a/src/algorithms/PVT/libs/rinex_printer.h +++ b/src/algorithms/PVT/libs/rinex_printer.h @@ -258,6 +258,7 @@ private: bool d_rinex_header_galileo_updated; bool d_rinex_header_glonass_updated; bool d_rinex_header_beidou_updated; + bool d_rinex_header_qzss_updated; bool d_rinex_header_written; const int32_t d_ref_gps_week; diff --git a/src/algorithms/PVT/libs/rtklib_solver.cc b/src/algorithms/PVT/libs/rtklib_solver.cc index 1d1a44d3e..c2584a5f3 100644 --- a/src/algorithms/PVT/libs/rtklib_solver.cc +++ b/src/algorithms/PVT/libs/rtklib_solver.cc @@ -1876,6 +1876,30 @@ bool Rtklib_Solver::get_PVT(const std::map &gnss_observables_ d_nav_data.ion_gps[6] = gps_cnav_iono.beta2; d_nav_data.ion_gps[7] = gps_cnav_iono.beta3; } + if (qzss_iono.valid) + { + d_nav_data.ion_qzs[0] = qzss_iono.alpha0; + d_nav_data.ion_qzs[1] = qzss_iono.alpha1; + d_nav_data.ion_qzs[2] = qzss_iono.alpha2; + d_nav_data.ion_qzs[3] = qzss_iono.alpha3; + d_nav_data.ion_qzs[4] = qzss_iono.beta0; + d_nav_data.ion_qzs[5] = qzss_iono.beta1; + d_nav_data.ion_qzs[6] = qzss_iono.beta2; + d_nav_data.ion_qzs[7] = qzss_iono.beta3; + } + if (!(gps_iono.valid) && !(gps_cnav_iono.valid) && qzss_iono.valid) + { + // Keep the GPS-interoperable QZSS Klobuchar coefficients available + // to the broadcast ionospheric model in QZSS-only configurations + d_nav_data.ion_gps[0] = qzss_iono.alpha0; + d_nav_data.ion_gps[1] = qzss_iono.alpha1; + d_nav_data.ion_gps[2] = qzss_iono.alpha2; + d_nav_data.ion_gps[3] = qzss_iono.alpha3; + d_nav_data.ion_gps[4] = qzss_iono.beta0; + d_nav_data.ion_gps[5] = qzss_iono.beta1; + d_nav_data.ion_gps[6] = qzss_iono.beta2; + d_nav_data.ion_gps[7] = qzss_iono.beta3; + } if (galileo_iono.ai0 != 0.0) { d_nav_data.ion_gal[0] = galileo_iono.ai0; @@ -1910,6 +1934,19 @@ bool Rtklib_Solver::get_PVT(const std::map &gnss_observables_ d_nav_data.utc_gps[3] = gps_cnav_utc_model.WN_T; d_nav_data.leaps = gps_cnav_utc_model.DeltaT_LS; } + if (qzss_utc_model.valid) + { + d_nav_data.utc_qzs[0] = qzss_utc_model.A0; + d_nav_data.utc_qzs[1] = qzss_utc_model.A1; + d_nav_data.utc_qzs[2] = qzss_utc_model.tot; + d_nav_data.utc_qzs[3] = qzss_utc_model.WN_T; + } + if (!(gps_utc_model.valid) && !(gps_cnav_utc_model.valid) && qzss_utc_model.valid) + { + // QZSS time is aligned with GPS time, so its leap seconds apply + // in QZSS-only configurations + d_nav_data.leaps = qzss_utc_model.DeltaT_LS; + } if (glonass_gnav_utc_model.valid) { d_nav_data.utc_glo[0] = glonass_gnav_utc_model.d_tau_c; // ?? diff --git a/src/algorithms/PVT/libs/rtklib_solver.h b/src/algorithms/PVT/libs/rtklib_solver.h index c86b7dae3..92d20eb99 100644 --- a/src/algorithms/PVT/libs/rtklib_solver.h +++ b/src/algorithms/PVT/libs/rtklib_solver.h @@ -61,6 +61,8 @@ #include "pvt_conf.h" #include "pvt_kf.h" #include "pvt_solution.h" +#include "qzss_iono.h" +#include "qzss_utc_model.h" #include "rtklib.h" #include "rtklib_conversions.h" #include "sensor_data/sensor_data_aggregator.h" @@ -133,6 +135,9 @@ public: Gps_CNAV_Iono gps_cnav_iono; Gps_CNAV_Utc_Model gps_cnav_utc_model; + Qzss_Utc_Model qzss_utc_model; + Qzss_Iono qzss_iono; + Glonass_Gnav_Utc_Model glonass_gnav_utc_model; //!< Map storing GLONASS GNAV UTC Model Glonass_Gnav_Almanac glonass_gnav_almanac; //!< Map storing GLONASS GNAV Almanac Model diff --git a/src/algorithms/telemetry_decoder/gnuradio_blocks/gps_l1_ca_telemetry_decoder_gs.cc b/src/algorithms/telemetry_decoder/gnuradio_blocks/gps_l1_ca_telemetry_decoder_gs.cc index 0454db6a9..8e13b9cfa 100644 --- a/src/algorithms/telemetry_decoder/gnuradio_blocks/gps_l1_ca_telemetry_decoder_gs.cc +++ b/src/algorithms/telemetry_decoder/gnuradio_blocks/gps_l1_ca_telemetry_decoder_gs.cc @@ -23,10 +23,12 @@ #include "dump_logger_helper.h" #include "gnss_sdr_make_unique.h" // for std::make_unique in C++11 #include "gnss_synchro.h" -#include "gnss_time.h" // for timetags produced by Tracking -#include "gps_ephemeris.h" // for Gps_Ephemeris -#include "gps_iono.h" // for Gps_Iono -#include "gps_utc_model.h" // for Gps_Utc_Model +#include "gnss_time.h" // for timetags produced by Tracking +#include "gps_ephemeris.h" // for Gps_Ephemeris +#include "gps_iono.h" // for Gps_Iono +#include "gps_utc_model.h" // for Gps_Utc_Model +#include "qzss_iono.h" // for Qzss_Iono +#include "qzss_utc_model.h" // for Qzss_Utc_Model #include "tlm_conf.h" #include "tlm_crc_stats.h" #include "tlm_utils.h" @@ -373,13 +375,32 @@ bool gps_l1_ca_telemetry_decoder_gs::decode_subframe(double cn0, bool flag_inver case 4: // Possible IONOSPHERE and UTC model update (page 18) if (d_nav->get_flag_iono_valid() == true) { - const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_iono()); - this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + if (d_system == L1LnavSystem::QZSS) + { + // QZSS broadcasts its own Klobuchar coefficients; keep them + // separate from the GPS ones + const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_iono()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } + else + { + const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_iono()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } } if (d_nav->get_flag_utc_model_valid() == true) { - const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_utc_model()); - this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + if (d_system == L1LnavSystem::QZSS) + { + // The QZSS UTC offset refers to UTC(NICT), not to UTC(USNO) + const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_utc_model()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } + else + { + const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_utc_model()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } } if (d_nav->almanac_validation() == true) { @@ -390,13 +411,32 @@ bool gps_l1_ca_telemetry_decoder_gs::decode_subframe(double cn0, bool flag_inver case 5: if (d_nav->get_flag_iono_valid() == true) { - const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_iono()); - this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + if (d_system == L1LnavSystem::QZSS) + { + // QZSS broadcasts its own Klobuchar coefficients; keep them + // separate from the GPS ones + const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_iono()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } + else + { + const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_iono()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } } if (d_nav->get_flag_utc_model_valid() == true) { - const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_utc_model()); - this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + if (d_system == L1LnavSystem::QZSS) + { + // The QZSS UTC offset refers to UTC(NICT), not to UTC(USNO) + const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_utc_model()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } + else + { + const std::shared_ptr tmp_obj = std::make_shared(d_nav->get_utc_model()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } } if (d_nav->almanac_validation() == true) { diff --git a/src/core/system_parameters/CMakeLists.txt b/src/core/system_parameters/CMakeLists.txt index 06bfa0116..c32696083 100644 --- a/src/core/system_parameters/CMakeLists.txt +++ b/src/core/system_parameters/CMakeLists.txt @@ -100,6 +100,8 @@ set(SYSTEM_PARAMETERS_HEADERS osnma_dsm_reader.h tow_to_trk.h qzss.h + qzss_iono.h + qzss_utc_model.h signal_flag.h ) diff --git a/src/core/system_parameters/qzss_iono.h b/src/core/system_parameters/qzss_iono.h new file mode 100644 index 000000000..c0e53bff0 --- /dev/null +++ b/src/core/system_parameters/qzss_iono.h @@ -0,0 +1,45 @@ +/*! + * \file qzss_iono.h + * \brief Interface of a QZSS ionospheric model storage + * \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 + * + * ----------------------------------------------------------------------------- + */ + +#ifndef GNSS_SDR_QZSS_IONO_H +#define GNSS_SDR_QZSS_IONO_H + +#include "gps_iono.h" + +/** \addtogroup Core + * \{ */ +/** \addtogroup System_Parameters + * \{ */ + + +/*! + * \brief This class is a storage for the QZSS Klobuchar ionospheric model + * data as described in IS-QZSS-PNT-006 + * + * The coefficients share the GPS LNAV layout, but they are independently + * determined by QZSS. See https://qzss.go.jp/en/technical/ps-is-qzss/ + */ +class Qzss_Iono : public Gps_Iono +{ +public: + Qzss_Iono() = default; //!< Default constructor + explicit Qzss_Iono(const Gps_Iono& iono) : Gps_Iono(iono) {} +}; + + +/** \} */ +/** \} */ +#endif // GNSS_SDR_QZSS_IONO_H diff --git a/src/core/system_parameters/qzss_utc_model.h b/src/core/system_parameters/qzss_utc_model.h new file mode 100644 index 000000000..c14c92519 --- /dev/null +++ b/src/core/system_parameters/qzss_utc_model.h @@ -0,0 +1,45 @@ +/*! + * \file qzss_utc_model.h + * \brief Interface of a QZSS UTC model storage + * \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 + * + * ----------------------------------------------------------------------------- + */ + +#ifndef GNSS_SDR_QZSS_UTC_MODEL_H +#define GNSS_SDR_QZSS_UTC_MODEL_H + +#include "gps_utc_model.h" + +/** \addtogroup Core + * \{ */ +/** \addtogroup System_Parameters + * \{ */ + + +/*! + * \brief This class is a storage for the QZSS UTC model data as described in + * IS-QZSS-PNT-006 + * + * The parameters share the GPS LNAV layout, but the broadcast offset refers + * to UTC(NICT) instead of UTC(USNO). See https://qzss.go.jp/en/technical/ps-is-qzss/ + */ +class Qzss_Utc_Model : public Gps_Utc_Model +{ +public: + Qzss_Utc_Model() = default; //!< Default constructor + explicit Qzss_Utc_Model(const Gps_Utc_Model& utc_model) : Gps_Utc_Model(utc_model) {} +}; + + +/** \} */ +/** \} */ +#endif // GNSS_SDR_QZSS_UTC_MODEL_H diff --git a/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc b/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc index e5a828589..64ebe051b 100644 --- a/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc +++ b/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc @@ -572,6 +572,59 @@ TEST_F(RinexPrinterTest, Rinex4GpsCnavNav) } +TEST_F(RinexPrinterTest, Rinex4QzssNav) +{ + Pvt_Conf conf; + conf.use_e6_for_pvt = false; + const auto signal_enabled_flags = QZS_J1; + auto pvt_solution = std::make_shared(rtk, conf, "filename", signal_enabled_flags, false, false); + + Gps_Ephemeris eph; + eph.PRN = 193; // QZSS J01 + eph.WN = 512; + pvt_solution->gps_ephemeris_map[193] = eph; + pvt_solution->qzss_iono.alpha0 = 2.6077032089233398e-08; + pvt_solution->qzss_iono.alpha1 = -7.4505805969238281e-09; + pvt_solution->qzss_iono.beta0 = 118784.0; + pvt_solution->qzss_iono.beta1 = -16384.0; + pvt_solution->qzss_utc_model.A0 = -9.3132257461547852e-10; + pvt_solution->qzss_utc_model.A1 = -8.8817841970012523e-16; + pvt_solution->qzss_utc_model.tot = 233472; + pvt_solution->qzss_utc_model.WN_T = 5; + pvt_solution->qzss_utc_model.DeltaT_LS = 18; + pvt_solution->qzss_utc_model.WN_LSF = 7; + pvt_solution->qzss_utc_model.DN = 7; + pvt_solution->qzss_utc_model.DeltaT_LSF = 18; + + std::map gnss_observables_map; + Gnss_Synchro gs{}; + gs.System = 'J'; + gs.PRN = 193; + std::memcpy(static_cast(gs.Signal), "J1", 3); + gnss_observables_map[193] = std::move(gs); + + auto rp = std::make_shared(signal_enabled_flags, 4); + rp->print_rinex_annotation(pvt_solution.get(), gnss_observables_map, 42.0, true); + + const std::string obsfile = rp->get_obsfilename(); + const std::string navfile = rp->get_navfilename()[0]; + rp = nullptr; // close the RINEX files so we can inspect them + + EXPECT_FALSE(find_line_starting_with(navfile, "> EPH J01 LNAV").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> ION J LNAV").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> STO J LNAV").empty()); + + const std::string sto_line = find_line_containing(navfile, "QZUT"); + EXPECT_NE(std::string::npos, sto_line.find("UTC(NICT)")); + + const std::string leap_second_line = find_rinex_header_line(navfile, "LEAP SECONDS"); + EXPECT_NE(std::string::npos, leap_second_line.find("18")); + + fs::remove(obsfile); + fs::remove(navfile); +} + + TEST_F(RinexPrinterTest, GalileoObsHeader) { Pvt_Conf conf; From cfc45fb8bf7e32b2d54d123fefb88c8c49f36404 Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Thu, 30 Jul 2026 12:37:23 +0200 Subject: [PATCH 4/4] Store QZSS L5 CNAV UTC and iono parameters separately, log CNVX records Extend the QZSS L1 C/A LNAV treatment to the L5 CNAV chain: the QZSS- configured L5 telemetry decoder now publishes dedicated Qzss_CNAV_Iono and Qzss_CNAV_Utc_Model objects instead of the shared GPS CNAV types, and PVT keeps them in their own Rtklib_Solver members. - RINEX 4.02 navigation files get "> STO J CNVX" QZUT records with the UTC(NICT) identifier and "> ION J CNVX WIDE" Klobuchar records. The WIDE subtype, compulsory for the QZSS CNVX ION message type, is correct because CNAV Message Type 30 broadcasts the Wide Area coefficient set (IS-QZSS-PNT-006; the Japan area set of Message Type 61 is not decoded). - QZSS CNAV parameters no longer overwrite the GPS CNAV ones in mixed configurations. - The QZSS slots of the RTKLIB navigation structure take the LNAV values with CNAV as fallback, and QZSS-only configurations keep feeding the broadcast ionospheric model and the leap second count. - The XML storage output gains qzss_cnav_utc_model.xml and qzss_cnav_iono.xml. --- docs/CHANGELOG.md | 15 ++-- .../PVT/gnuradio_blocks/rtklib_pvt_gs.cc | 86 +++++++++++++++++++ .../PVT/gnuradio_blocks/rtklib_pvt_gs.h | 2 + src/algorithms/PVT/libs/rinex_printer.cc | 77 ++++++++++++----- src/algorithms/PVT/libs/rtklib_solver.cc | 68 ++++++++------- src/algorithms/PVT/libs/rtklib_solver.h | 5 ++ .../gps_l5_telemetry_decoder_gs.cc | 31 +++++-- src/core/system_parameters/CMakeLists.txt | 2 + src/core/system_parameters/qzss_cnav_iono.h | 45 ++++++++++ .../system_parameters/qzss_cnav_utc_model.h | 45 ++++++++++ .../pvt/rinex_printer_test.cc | 48 +++++++++++ 11 files changed, 358 insertions(+), 66 deletions(-) create mode 100644 src/core/system_parameters/qzss_cnav_iono.h create mode 100644 src/core/system_parameters/qzss_cnav_utc_model.h diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7a4aa413b..76e3dd14b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -65,12 +65,15 @@ All notable changes to GNSS-SDR will be documented in this file. structure introduced in RINEX 4.00. The default behavior when `PVT.rinex_version` is not set remains unchanged (RINEX 3.02). - The ionospheric Klobuchar coefficients and the UTC(NICT) offset parameters - broadcast by QZSS satellites are now stored separately from the GPS ones - instead of overwriting them. This enables the QZUT / QZSS ION RINEX 4 data - records, prevents QZSS-sourced parameters from being mislabeled as GPS - corrections in mixed GPS + QZSS configurations, feeds the QZSS slots of the - RTKLIB navigation structure, and adds `qzss_utc_model.xml` / `qzss_iono.xml` - to the XML storage output. + broadcast by QZSS satellites, both in the L1 C/A LNAV message and in the L5 + CNAV message, are now stored separately from the GPS ones instead of + overwriting them. This enables the QZUT / QZSS ION RINEX 4 data records (with + the compulsory `WIDE` subtype for the CNVX Klobuchar set, broadcast in CNAV + Message Type 30), prevents QZSS-sourced parameters from being mislabeled as + GPS corrections in mixed GPS + QZSS configurations, feeds the QZSS slots of + the RTKLIB navigation structure, and adds `qzss_utc_model.xml`, + `qzss_iono.xml`, `qzss_cnav_utc_model.xml`, and `qzss_cnav_iono.xml` to the + XML storage output. - Added a new Signal Source implementation `Pocket_SDR_Signal_Source`, which supports [Pocket SDR FE](https://www.datagnss.com/products/pocketsdr-gnss-receiver) diff --git a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc index bffb8fe3f..38273f091 100644 --- a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc +++ b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc @@ -57,6 +57,8 @@ #include "osnma_data.h" #include "pvt_conf.h" #include "qzss.h" +#include "qzss_cnav_iono.h" +#include "qzss_cnav_utc_model.h" #include "qzss_iono.h" #include "qzss_utc_model.h" #include "rinex_printer.h" @@ -149,6 +151,8 @@ rtklib_pvt_gs::rtklib_pvt_gs(uint32_t nchannels, d_gps_almanac_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_qzss_iono_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_qzss_utc_model_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), + d_qzss_cnav_iono_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), + d_qzss_cnav_utc_model_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_galileo_ephemeris_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_galileo_reduced_ced_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), d_galileo_iono_sptr_type_hash_code(typeid(std::shared_ptr).hash_code()), @@ -938,6 +942,66 @@ rtklib_pvt_gs::~rtklib_pvt_gs() LOG(INFO) << "Failed to save QZSS ionospheric model parameters, not valid data"; } + // Save QZSS CNAV UTC model parameters + file_name = d_xml_base_path + "qzss_cnav_utc_model.xml"; + if (d_internal_pvt_solver->qzss_cnav_utc_model.valid) + { + std::ofstream ofs; + try + { + ofs.open(file_name.c_str(), std::ofstream::trunc | std::ofstream::out); + boost::archive::xml_oarchive xml(ofs); + xml << boost::serialization::make_nvp("GNSS-SDR_qzss_cnav_utc_model", d_internal_pvt_solver->qzss_cnav_utc_model); + LOG(INFO) << "Saved QZSS CNAV UTC model parameters"; + } + catch (const boost::archive::archive_exception& e) + { + LOG(WARNING) << e.what(); + } + catch (const std::ofstream::failure& e) + { + LOG(WARNING) << "Problem opening output XML file"; + } + catch (const std::exception& e) + { + LOG(WARNING) << e.what(); + } + } + else + { + LOG(INFO) << "Failed to save QZSS CNAV UTC model parameters, not valid data"; + } + + // Save QZSS CNAV iono parameters + file_name = d_xml_base_path + "qzss_cnav_iono.xml"; + if (d_internal_pvt_solver->qzss_cnav_iono.valid == true) + { + std::ofstream ofs; + try + { + ofs.open(file_name.c_str(), std::ofstream::trunc | std::ofstream::out); + boost::archive::xml_oarchive xml(ofs); + xml << boost::serialization::make_nvp("GNSS-SDR_qzss_cnav_iono_model", d_internal_pvt_solver->qzss_cnav_iono); + LOG(INFO) << "Saved QZSS CNAV ionospheric model parameters"; + } + catch (const boost::archive::archive_exception& e) + { + LOG(WARNING) << e.what(); + } + catch (const std::ofstream::failure& e) + { + LOG(WARNING) << "Problem opening output XML file"; + } + catch (const std::exception& e) + { + LOG(WARNING) << e.what(); + } + } + else + { + LOG(INFO) << "Failed to save QZSS CNAV ionospheric model parameters, not valid data"; + } + // Save Galileo iono parameters file_name = d_xml_base_path + "gal_iono.xml"; if (d_internal_pvt_solver->galileo_iono.ai0 != 0.0) @@ -1357,6 +1421,28 @@ void rtklib_pvt_gs::msg_handler_telemetry(const pmt::pmt_t& msg) } DLOG(INFO) << "New QZSS UTC record has arrived"; } + else if (msg_type_hash_code == d_qzss_cnav_iono_sptr_type_hash_code) + { + // ### QZSS CNAV IONO ### + const auto qzss_cnav_iono = wht::any_cast>(pmt::any_ref(msg)); + d_internal_pvt_solver->qzss_cnav_iono = *qzss_cnav_iono; + if (d_enable_rx_clock_correction == true) + { + d_user_pvt_solver->qzss_cnav_iono = *qzss_cnav_iono; + } + DLOG(INFO) << "New QZSS CNAV IONO record has arrived"; + } + else if (msg_type_hash_code == d_qzss_cnav_utc_model_sptr_type_hash_code) + { + // ### QZSS CNAV UTC MODEL ### + const auto qzss_cnav_utc_model = wht::any_cast>(pmt::any_ref(msg)); + d_internal_pvt_solver->qzss_cnav_utc_model = *qzss_cnav_utc_model; + if (d_enable_rx_clock_correction == true) + { + d_user_pvt_solver->qzss_cnav_utc_model = *qzss_cnav_utc_model; + } + DLOG(INFO) << "New QZSS CNAV UTC record has arrived"; + } else if (msg_type_hash_code == d_gps_cnav_ephemeris_sptr_type_hash_code) { // ### GPS CNAV message ### diff --git a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h index 96e424e9d..d2a7af331 100644 --- a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h +++ b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h @@ -226,6 +226,8 @@ private: const size_t d_gps_almanac_sptr_type_hash_code; const size_t d_qzss_iono_sptr_type_hash_code; const size_t d_qzss_utc_model_sptr_type_hash_code; + const size_t d_qzss_cnav_iono_sptr_type_hash_code; + const size_t d_qzss_cnav_utc_model_sptr_type_hash_code; const size_t d_galileo_ephemeris_sptr_type_hash_code; const size_t d_galileo_reduced_ced_sptr_type_hash_code; const size_t d_galileo_iono_sptr_type_hash_code; diff --git a/src/algorithms/PVT/libs/rinex_printer.cc b/src/algorithms/PVT/libs/rinex_printer.cc index 15c11a60f..32857beb4 100644 --- a/src/algorithms/PVT/libs/rinex_printer.cc +++ b/src/algorithms/PVT/libs/rinex_printer.cc @@ -1872,14 +1872,23 @@ void add_sto_record(std::fstream& out, char sys_char, const std::string& msg_typ /* * RINEX 4 Ionosphere (ION) Klobuchar model data record - * (see RINEX 4.02 specification, Table A35) + * (see RINEX 4.02 specification, Table A35). The message subtype is + * compulsory for the QZSS CNVX message type (WIDE or JAPN) and shall be + * left blank otherwise */ void add_ion_klobuchar_record(std::fstream& out, char sys_char, const std::string& msg_type, const boost::posix_time::ptime& transmit_time, double alpha0, double alpha1, double alpha2, double alpha3, - double beta0, double beta1, double beta2, double beta3) + double beta0, double beta1, double beta2, double beta3, + const std::string& subtype = "") { - out << get_nav_record_header_line("ION", sys_char, 0, msg_type) << '\n'; + std::string header_line = get_nav_record_header_line("ION", sys_char, 0, msg_type); + if (!subtype.empty()) + { + header_line += std::string(1, ' '); + header_line += subtype; + } + out << header_line << '\n'; std::string line; line += std::string(4, ' '); @@ -2389,7 +2398,7 @@ Rinex_Printer::Rinex_Printer(uint32_t signal_enabled_flags, d_rinex_header_galileo_updated(!d_flags.has_galileo), d_rinex_header_glonass_updated(!d_flags.has_glonass), d_rinex_header_beidou_updated(!d_flags.has_beidou), - d_rinex_header_qzss_updated(!d_flags.check_any_enabled(QZS_J1)), + d_rinex_header_qzss_updated(!d_flags.has_qzss), d_rinex_header_written(false), d_ref_gps_week(ref_gps_week), navfilename(getNavFilePath(d_flags, d_version, base_name, base_rinex_path)), @@ -2843,27 +2852,51 @@ void Rinex_Printer::log_rinex_nav_v4_ion_sto_records(const Rtklib_Solver* pvt_so records_written = true; } } - if (!d_rinex_header_qzss_updated && pvt_solver->qzss_utc_model.A0 != 0) + if (!d_rinex_header_qzss_updated) { - // QZSS weeks are aligned with GPS weeks, so any ephemeris of the - // shared GPS/QZSS LNAV map works as reference to resolve the - // 8-bit UTC week number - const auto& eph = pvt_solver->gps_ephemeris_map.cbegin()->second; - const auto& utc_model = pvt_solver->qzss_utc_model; - const auto& iono = pvt_solver->qzss_iono; - add_ion_klobuchar_record(out, satelliteSystem.at("QZSS"), "LNAV", Rinex_Printer::compute_GPS_time(eph, rx_time), - iono.alpha0, iono.alpha1, iono.alpha2, iono.alpha3, - iono.beta0, iono.beta1, iono.beta2, iono.beta3); - const int32_t wn_t = expand_lnav_utc_week(utc_model.WN_T, eph.WN, d_ref_gps_week); - add_sto_record(out, satelliteSystem.at("QZSS"), "LNAV", gps_time_to_ptime(wn_t, utc_model.tot), - "QZUT", "UTC(NICT)", tow, utc_model.A0, utc_model.A1, utc_model.A2); - - if (system_time_str == "QZS") + if (d_flags.check_any_enabled(QZS_J1) && pvt_solver->qzss_utc_model.A0 != 0) { - leap_second_line = get_leap_second_line(utc_model, eph, d_ref_gps_week); + // QZSS weeks are aligned with GPS weeks, so any ephemeris of the + // shared GPS/QZSS LNAV map works as reference to resolve the + // 8-bit UTC week number + const auto& eph = pvt_solver->gps_ephemeris_map.cbegin()->second; + const auto& utc_model = pvt_solver->qzss_utc_model; + const auto& iono = pvt_solver->qzss_iono; + add_ion_klobuchar_record(out, satelliteSystem.at("QZSS"), "LNAV", Rinex_Printer::compute_GPS_time(eph, rx_time), + iono.alpha0, iono.alpha1, iono.alpha2, iono.alpha3, + iono.beta0, iono.beta1, iono.beta2, iono.beta3); + const int32_t wn_t = expand_lnav_utc_week(utc_model.WN_T, eph.WN, d_ref_gps_week); + add_sto_record(out, satelliteSystem.at("QZSS"), "LNAV", gps_time_to_ptime(wn_t, utc_model.tot), + "QZUT", "UTC(NICT)", tow, utc_model.A0, utc_model.A1, utc_model.A2); + + if (system_time_str == "QZS") + { + leap_second_line = get_leap_second_line(utc_model, eph, d_ref_gps_week); + } + d_rinex_header_qzss_updated = true; + records_written = true; + } + else if (d_flags.check_any_enabled(QZS_J5) && pvt_solver->qzss_cnav_utc_model.A0 != 0) + { + const auto& eph = pvt_solver->gps_cnav_ephemeris_map.cbegin()->second; + const auto& utc_model = pvt_solver->qzss_cnav_utc_model; + const auto& iono = pvt_solver->qzss_cnav_iono; + // QZSS CNAV Message Type 30 broadcasts the Wide Area coefficient + // set (the Japan area set of Message Type 61 is not decoded), so + // the compulsory CNVX message subtype is WIDE (see Table A35) + add_ion_klobuchar_record(out, satelliteSystem.at("QZSS"), "CNVX", Rinex_Printer::compute_GPS_time(eph, rx_time), + iono.alpha0, iono.alpha1, iono.alpha2, iono.alpha3, + iono.beta0, iono.beta1, iono.beta2, iono.beta3, "WIDE"); + add_sto_record(out, satelliteSystem.at("QZSS"), "CNVX", gps_time_to_ptime(utc_model.WN_T, utc_model.tot), + "QZUT", "UTC(NICT)", tow, utc_model.A0, utc_model.A1, utc_model.A2); + + if (system_time_str == "QZS") + { + leap_second_line = get_leap_second_line(utc_model); + } + d_rinex_header_qzss_updated = true; + records_written = true; } - d_rinex_header_qzss_updated = true; - records_written = true; } if (!d_rinex_header_galileo_updated && pvt_solver->galileo_utc_model.A0 != 0) { diff --git a/src/algorithms/PVT/libs/rtklib_solver.cc b/src/algorithms/PVT/libs/rtklib_solver.cc index c2584a5f3..f9ca62c6b 100644 --- a/src/algorithms/PVT/libs/rtklib_solver.cc +++ b/src/algorithms/PVT/libs/rtklib_solver.cc @@ -1876,29 +1876,30 @@ bool Rtklib_Solver::get_PVT(const std::map &gnss_observables_ d_nav_data.ion_gps[6] = gps_cnav_iono.beta2; d_nav_data.ion_gps[7] = gps_cnav_iono.beta3; } - if (qzss_iono.valid) + if (qzss_iono.valid || qzss_cnav_iono.valid) { - d_nav_data.ion_qzs[0] = qzss_iono.alpha0; - d_nav_data.ion_qzs[1] = qzss_iono.alpha1; - d_nav_data.ion_qzs[2] = qzss_iono.alpha2; - d_nav_data.ion_qzs[3] = qzss_iono.alpha3; - d_nav_data.ion_qzs[4] = qzss_iono.beta0; - d_nav_data.ion_qzs[5] = qzss_iono.beta1; - d_nav_data.ion_qzs[6] = qzss_iono.beta2; - d_nav_data.ion_qzs[7] = qzss_iono.beta3; - } - if (!(gps_iono.valid) && !(gps_cnav_iono.valid) && qzss_iono.valid) - { - // Keep the GPS-interoperable QZSS Klobuchar coefficients available - // to the broadcast ionospheric model in QZSS-only configurations - d_nav_data.ion_gps[0] = qzss_iono.alpha0; - d_nav_data.ion_gps[1] = qzss_iono.alpha1; - d_nav_data.ion_gps[2] = qzss_iono.alpha2; - d_nav_data.ion_gps[3] = qzss_iono.alpha3; - d_nav_data.ion_gps[4] = qzss_iono.beta0; - d_nav_data.ion_gps[5] = qzss_iono.beta1; - d_nav_data.ion_gps[6] = qzss_iono.beta2; - d_nav_data.ion_gps[7] = qzss_iono.beta3; + const Gps_Iono &qzss_iono_ref = qzss_iono.valid ? static_cast(qzss_iono) : static_cast(qzss_cnav_iono); + d_nav_data.ion_qzs[0] = qzss_iono_ref.alpha0; + d_nav_data.ion_qzs[1] = qzss_iono_ref.alpha1; + d_nav_data.ion_qzs[2] = qzss_iono_ref.alpha2; + d_nav_data.ion_qzs[3] = qzss_iono_ref.alpha3; + d_nav_data.ion_qzs[4] = qzss_iono_ref.beta0; + d_nav_data.ion_qzs[5] = qzss_iono_ref.beta1; + d_nav_data.ion_qzs[6] = qzss_iono_ref.beta2; + d_nav_data.ion_qzs[7] = qzss_iono_ref.beta3; + if (!(gps_iono.valid) && !(gps_cnav_iono.valid)) + { + // Keep the GPS-interoperable QZSS Klobuchar coefficients available + // to the broadcast ionospheric model in QZSS-only configurations + d_nav_data.ion_gps[0] = qzss_iono_ref.alpha0; + d_nav_data.ion_gps[1] = qzss_iono_ref.alpha1; + d_nav_data.ion_gps[2] = qzss_iono_ref.alpha2; + d_nav_data.ion_gps[3] = qzss_iono_ref.alpha3; + d_nav_data.ion_gps[4] = qzss_iono_ref.beta0; + d_nav_data.ion_gps[5] = qzss_iono_ref.beta1; + d_nav_data.ion_gps[6] = qzss_iono_ref.beta2; + d_nav_data.ion_gps[7] = qzss_iono_ref.beta3; + } } if (galileo_iono.ai0 != 0.0) { @@ -1934,18 +1935,19 @@ bool Rtklib_Solver::get_PVT(const std::map &gnss_observables_ d_nav_data.utc_gps[3] = gps_cnav_utc_model.WN_T; d_nav_data.leaps = gps_cnav_utc_model.DeltaT_LS; } - if (qzss_utc_model.valid) + if (qzss_utc_model.valid || qzss_cnav_utc_model.valid) { - d_nav_data.utc_qzs[0] = qzss_utc_model.A0; - d_nav_data.utc_qzs[1] = qzss_utc_model.A1; - d_nav_data.utc_qzs[2] = qzss_utc_model.tot; - d_nav_data.utc_qzs[3] = qzss_utc_model.WN_T; - } - if (!(gps_utc_model.valid) && !(gps_cnav_utc_model.valid) && qzss_utc_model.valid) - { - // QZSS time is aligned with GPS time, so its leap seconds apply - // in QZSS-only configurations - d_nav_data.leaps = qzss_utc_model.DeltaT_LS; + const Gps_Utc_Model &qzss_utc_ref = qzss_utc_model.valid ? static_cast(qzss_utc_model) : static_cast(qzss_cnav_utc_model); + d_nav_data.utc_qzs[0] = qzss_utc_ref.A0; + d_nav_data.utc_qzs[1] = qzss_utc_ref.A1; + d_nav_data.utc_qzs[2] = qzss_utc_ref.tot; + d_nav_data.utc_qzs[3] = qzss_utc_ref.WN_T; + if (!(gps_utc_model.valid) && !(gps_cnav_utc_model.valid)) + { + // QZSS time is aligned with GPS time, so its leap seconds apply + // in QZSS-only configurations + d_nav_data.leaps = qzss_utc_ref.DeltaT_LS; + } } if (glonass_gnav_utc_model.valid) { diff --git a/src/algorithms/PVT/libs/rtklib_solver.h b/src/algorithms/PVT/libs/rtklib_solver.h index 92d20eb99..2a85dafba 100644 --- a/src/algorithms/PVT/libs/rtklib_solver.h +++ b/src/algorithms/PVT/libs/rtklib_solver.h @@ -61,6 +61,8 @@ #include "pvt_conf.h" #include "pvt_kf.h" #include "pvt_solution.h" +#include "qzss_cnav_iono.h" +#include "qzss_cnav_utc_model.h" #include "qzss_iono.h" #include "qzss_utc_model.h" #include "rtklib.h" @@ -138,6 +140,9 @@ public: Qzss_Utc_Model qzss_utc_model; Qzss_Iono qzss_iono; + Qzss_CNAV_Utc_Model qzss_cnav_utc_model; + Qzss_CNAV_Iono qzss_cnav_iono; + Glonass_Gnav_Utc_Model glonass_gnav_utc_model; //!< Map storing GLONASS GNAV UTC Model Glonass_Gnav_Almanac glonass_gnav_almanac; //!< Map storing GLONASS GNAV Almanac Model diff --git a/src/algorithms/telemetry_decoder/gnuradio_blocks/gps_l5_telemetry_decoder_gs.cc b/src/algorithms/telemetry_decoder/gnuradio_blocks/gps_l5_telemetry_decoder_gs.cc index 6baa433f6..d3912f423 100644 --- a/src/algorithms/telemetry_decoder/gnuradio_blocks/gps_l5_telemetry_decoder_gs.cc +++ b/src/algorithms/telemetry_decoder/gnuradio_blocks/gps_l5_telemetry_decoder_gs.cc @@ -24,7 +24,9 @@ #include "gnss_synchro.h" #include "gps_cnav_ephemeris.h" #include "gps_cnav_iono.h" -#include "gps_cnav_utc_model.h" // for Gps_CNAV_Utc_Model +#include "gps_cnav_utc_model.h" // for Gps_CNAV_Utc_Model +#include "qzss_cnav_iono.h" // for Qzss_CNAV_Iono +#include "qzss_cnav_utc_model.h" // for Qzss_CNAV_Utc_Model #include "tlm_conf.h" #include "tlm_crc_stats.h" #include "tlm_utils.h" @@ -229,8 +231,18 @@ int gps_l5_telemetry_decoder_gs::general_work(int noutput_items __attribute__((u } if (d_CNAV_Message->have_new_iono() == true) { - const std::shared_ptr tmp_obj = std::make_shared(d_CNAV_Message->get_iono()); - this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + if (d_system == CnavSystem::QZSS) + { + // QZSS broadcasts its own Klobuchar coefficients (the Wide Area + // set in Message Type 30); keep them separate from the GPS ones + const std::shared_ptr tmp_obj = std::make_shared(d_CNAV_Message->get_iono()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } + else + { + const std::shared_ptr tmp_obj = std::make_shared(d_CNAV_Message->get_iono()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } const auto default_precision = std::cout.precision(); std::cout << TEXT_MAGENTA << "New " << ((d_system == CnavSystem::GPS) ? "GPS" : "QZSS") << " L5 CNAV message received in channel " << d_channel @@ -241,8 +253,17 @@ int gps_l5_telemetry_decoder_gs::general_work(int noutput_items __attribute__((u if (d_CNAV_Message->have_new_utc_model() == true) { - const std::shared_ptr tmp_obj = std::make_shared(d_CNAV_Message->get_utc_model()); - this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + if (d_system == CnavSystem::QZSS) + { + // The QZSS UTC offset refers to UTC(NICT), not to UTC(USNO) + const std::shared_ptr tmp_obj = std::make_shared(d_CNAV_Message->get_utc_model()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } + else + { + const std::shared_ptr tmp_obj = std::make_shared(d_CNAV_Message->get_utc_model()); + this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj)); + } const auto default_precision = std::cout.precision(); std::cout << TEXT_MAGENTA << "New " << ((d_system == CnavSystem::GPS) ? "GPS" : "QZSS") << " L5 CNAV message received in channel " << d_channel diff --git a/src/core/system_parameters/CMakeLists.txt b/src/core/system_parameters/CMakeLists.txt index c32696083..0dd5e7a6b 100644 --- a/src/core/system_parameters/CMakeLists.txt +++ b/src/core/system_parameters/CMakeLists.txt @@ -100,6 +100,8 @@ set(SYSTEM_PARAMETERS_HEADERS osnma_dsm_reader.h tow_to_trk.h qzss.h + qzss_cnav_iono.h + qzss_cnav_utc_model.h qzss_iono.h qzss_utc_model.h signal_flag.h diff --git a/src/core/system_parameters/qzss_cnav_iono.h b/src/core/system_parameters/qzss_cnav_iono.h new file mode 100644 index 000000000..2df7edb06 --- /dev/null +++ b/src/core/system_parameters/qzss_cnav_iono.h @@ -0,0 +1,45 @@ +/*! + * \file qzss_cnav_iono.h + * \brief Interface of a QZSS CNAV ionospheric model storage + * \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 + * + * ----------------------------------------------------------------------------- + */ + +#ifndef GNSS_SDR_QZSS_CNAV_IONO_H +#define GNSS_SDR_QZSS_CNAV_IONO_H + +#include "gps_cnav_iono.h" + +/** \addtogroup Core + * \{ */ +/** \addtogroup System_Parameters + * \{ */ + + +/*! + * \brief This class is a storage for the QZSS CNAV Klobuchar ionospheric + * model data as described in IS-QZSS-PNT-006 + * + * Message Type 30 broadcasts the Wide Area coefficient set (the Japan area + * set is broadcast in Message Type 61). See https://qzss.go.jp/en/technical/ps-is-qzss/ + */ +class Qzss_CNAV_Iono : public Gps_CNAV_Iono +{ +public: + Qzss_CNAV_Iono() = default; //!< Default constructor + explicit Qzss_CNAV_Iono(const Gps_CNAV_Iono& iono) : Gps_CNAV_Iono(iono) {} +}; + + +/** \} */ +/** \} */ +#endif // GNSS_SDR_QZSS_CNAV_IONO_H diff --git a/src/core/system_parameters/qzss_cnav_utc_model.h b/src/core/system_parameters/qzss_cnav_utc_model.h new file mode 100644 index 000000000..19195b454 --- /dev/null +++ b/src/core/system_parameters/qzss_cnav_utc_model.h @@ -0,0 +1,45 @@ +/*! + * \file qzss_cnav_utc_model.h + * \brief Interface of a QZSS CNAV UTC model storage + * \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 + * + * ----------------------------------------------------------------------------- + */ + +#ifndef GNSS_SDR_QZSS_CNAV_UTC_MODEL_H +#define GNSS_SDR_QZSS_CNAV_UTC_MODEL_H + +#include "gps_cnav_utc_model.h" + +/** \addtogroup Core + * \{ */ +/** \addtogroup System_Parameters + * \{ */ + + +/*! + * \brief This class is a storage for the QZSS CNAV UTC model data as + * described in IS-QZSS-PNT-006 + * + * The parameters share the GPS CNAV layout, but the broadcast offset refers + * to UTC(NICT) instead of UTC(USNO). See https://qzss.go.jp/en/technical/ps-is-qzss/ + */ +class Qzss_CNAV_Utc_Model : public Gps_CNAV_Utc_Model +{ +public: + Qzss_CNAV_Utc_Model() = default; //!< Default constructor + explicit Qzss_CNAV_Utc_Model(const Gps_CNAV_Utc_Model& utc_model) : Gps_CNAV_Utc_Model(utc_model) {} +}; + + +/** \} */ +/** \} */ +#endif // GNSS_SDR_QZSS_CNAV_UTC_MODEL_H diff --git a/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc b/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc index 64ebe051b..0490ac4d8 100644 --- a/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc +++ b/tests/unit-tests/signal-processing-blocks/pvt/rinex_printer_test.cc @@ -625,6 +625,54 @@ TEST_F(RinexPrinterTest, Rinex4QzssNav) } +TEST_F(RinexPrinterTest, Rinex4QzssCnavNav) +{ + Pvt_Conf conf; + conf.use_e6_for_pvt = false; + const auto signal_enabled_flags = QZS_J5; + auto pvt_solution = std::make_shared(rtk, conf, "filename", signal_enabled_flags, false, false); + + Gps_CNAV_Ephemeris eph; + eph.PRN = 193; // QZSS J01 + eph.WN = 2338; + pvt_solution->gps_cnav_ephemeris_map[193] = eph; + pvt_solution->qzss_cnav_iono.alpha0 = 2.6077032089233398e-08; + pvt_solution->qzss_cnav_iono.beta0 = 118784.0; + pvt_solution->qzss_cnav_utc_model.A0 = -9.3132257461547852e-10; + pvt_solution->qzss_cnav_utc_model.A1 = -8.8817841970012523e-16; + pvt_solution->qzss_cnav_utc_model.tot = 233472; + pvt_solution->qzss_cnav_utc_model.WN_T = 2338; + pvt_solution->qzss_cnav_utc_model.DeltaT_LS = 18; + + std::map gnss_observables_map; + Gnss_Synchro gs{}; + gs.System = 'J'; + gs.PRN = 193; + std::memcpy(static_cast(gs.Signal), "J5", 3); + gnss_observables_map[193] = std::move(gs); + + auto rp = std::make_shared(signal_enabled_flags, 4); + rp->print_rinex_annotation(pvt_solution.get(), gnss_observables_map, 42.0, true); + + const std::string obsfile = rp->get_obsfilename(); + const std::string navfile = rp->get_navfilename()[0]; + rp = nullptr; // close the RINEX files so we can inspect them + + EXPECT_FALSE(find_line_starting_with(navfile, "> EPH J01 CNAV").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> ION J CNVX WIDE").empty()); + EXPECT_FALSE(find_line_starting_with(navfile, "> STO J CNVX").empty()); + + const std::string sto_line = find_line_containing(navfile, "QZUT"); + EXPECT_NE(std::string::npos, sto_line.find("UTC(NICT)")); + + const std::string leap_second_line = find_rinex_header_line(navfile, "LEAP SECONDS"); + EXPECT_NE(std::string::npos, leap_second_line.find("18")); + + fs::remove(obsfile); + fs::remove(navfile); +} + + TEST_F(RinexPrinterTest, GalileoObsHeader) { Pvt_Conf conf;