Merge remote-tracking branch 'cf/rinex4' into next

This commit is contained in:
Carles Fernandez
2026-07-30 17:30:36 +02:00
17 changed files with 1508 additions and 68 deletions
+16
View File
@@ -58,6 +58,22 @@ 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. 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, 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)
+10 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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
@@ -57,6 +57,10 @@
#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"
#include "rtcm_printer.h"
#include "rtklib_rtkcmn.h"
@@ -145,6 +149,10 @@ rtklib_pvt_gs::rtklib_pvt_gs(uint32_t nchannels,
d_gps_cnav_iono_sptr_type_hash_code(typeid(std::shared_ptr<Gps_CNAV_Iono>).hash_code()),
d_gps_cnav_utc_model_sptr_type_hash_code(typeid(std::shared_ptr<Gps_CNAV_Utc_Model>).hash_code()),
d_gps_almanac_sptr_type_hash_code(typeid(std::shared_ptr<Gps_Almanac>).hash_code()),
d_qzss_iono_sptr_type_hash_code(typeid(std::shared_ptr<Qzss_Iono>).hash_code()),
d_qzss_utc_model_sptr_type_hash_code(typeid(std::shared_ptr<Qzss_Utc_Model>).hash_code()),
d_qzss_cnav_iono_sptr_type_hash_code(typeid(std::shared_ptr<Qzss_CNAV_Iono>).hash_code()),
d_qzss_cnav_utc_model_sptr_type_hash_code(typeid(std::shared_ptr<Qzss_CNAV_Utc_Model>).hash_code()),
d_galileo_ephemeris_sptr_type_hash_code(typeid(std::shared_ptr<Galileo_Ephemeris>).hash_code()),
d_galileo_reduced_ced_sptr_type_hash_code(typeid(std::shared_ptr<Galileo_Reduced_CED>).hash_code()),
d_galileo_iono_sptr_type_hash_code(typeid(std::shared_ptr<Galileo_Iono>).hash_code()),
@@ -874,6 +882,126 @@ 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 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)
@@ -1271,6 +1399,50 @@ 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<std::shared_ptr<Qzss_Iono>>(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<std::shared_ptr<Qzss_Utc_Model>>(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_qzss_cnav_iono_sptr_type_hash_code)
{
// ### QZSS CNAV IONO ###
const auto qzss_cnav_iono = wht::any_cast<std::shared_ptr<Qzss_CNAV_Iono>>(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<std::shared_ptr<Qzss_CNAV_Utc_Model>>(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 ###
@@ -224,6 +224,10 @@ 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_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;
+580 -44
View File
@@ -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<char, std::set<signal_flag>>& constel_signal_flags,
const std::map<int32_t, Gnss_Synchro>& 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,96 @@ 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). 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,
const std::string& subtype = "")
{
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, ' ');
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 +2363,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,13 +2391,14 @@ 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),
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.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)),
@@ -2358,6 +2586,9 @@ void Rinex_Printer::print_rinex_annotation(const Rtklib_Solver* pvt_solver,
std::vector<std::string> 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 +2600,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 +2653,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 +2661,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 +2717,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<NavHeaderInfo> nav_header_info;
@@ -2564,6 +2813,176 @@ 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_qzss_updated)
{
if (d_flags.check_any_enabled(QZS_J1) && 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;
}
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;
}
}
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<double>(((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<double>(p_utc_time.date().day_of_week()) * 86400.0 +
static_cast<double>(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_qzss_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<int32_t, Gps_Ephemeris>& new_eph)
{
std::string line;
@@ -2618,11 +3037,15 @@ void Rinex_Printer::log_rinex_nav_gps_nav(const std::map<int32_t, Gps_Ephemeris>
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 +3169,51 @@ void Rinex_Printer::log_rinex_nav_gps_cnav(const std::map<int32_t, Gps_CNAV_Ephe
const boost::posix_time::ptime p_utc_time = Rinex_Printer::compute_GPS_time(eph, eph.toc);
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;
if (d_version == 4)
{
// CNAV navigation message record, see RINEX 4.02 specification, Table A10
out << get_nav_record_header_line("EPH", sat_system_char, rinex_prn, "CNAV") << '\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
out << get_nav_broadcast_orbit(&eph.Adot, &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, d_version) << '\n';
// -------- BROADCAST ORBIT - 3
const auto top_d = static_cast<double>(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<double>(eph.URANED0);
const auto uraned1_d = static_cast<double>(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<double>(eph.URAED);
const auto health_d = static_cast<double>(eph.signal_health);
const auto uraned2_d = static_cast<double>(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<double>(eph.tow);
// wn_op: GPS continuous week number with the 8-bit MT30 ambiguity resolved
const auto wn_op_d = static_cast<double>(eph.WNop >= 0 ? expand_truncated_week(eph.WNop, eph.WN, 256) : eph.WN);
const auto flags_d = static_cast<double>((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 +3277,26 @@ void Rinex_Printer::log_rinex_nav_gal_nav(const std::map<int32_t, Galileo_Epheme
// -------- SV / EPOCH / SV CLK
const boost::posix_time::ptime p_utc_time = Rinex_Printer::compute_Galileo_time(eph, eph.toe);
out << get_nav_sv_epoch_svclk_line(p_utc_time, sys_char, eph.PRN, eph.af0, eph.af1, eph.af2) << '\n';
if (d_version == 4)
{
const std::string msg_type = eph.nav_message_type == Galileo_Nav_Message_Type::FNAV ? "FNAV" : "INAV";
out << get_nav_record_header_line("EPH", sys_char, eph.PRN, msg_type) << '\n';
}
out << get_nav_sv_epoch_svclk_line(p_utc_time, sys_char, eph.PRN, eph.af0, eph.af1, eph.af2, d_version) << '\n';
// -------- BROADCAST ORBIT - 1
const auto iod_d = static_cast<double>(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<double>(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 +3309,7 @@ void Rinex_Printer::log_rinex_nav_gal_nav(const std::map<int32_t, Galileo_Epheme
const double num_GST_rollovers = floor((GST_week + 1024.0) / 4096.0);
const double Galileo_week_continuous_number = GST_week + 1024.0 + num_GST_rollovers * 4096.0;
const double zero = 0.0;
out << get_nav_broadcast_orbit(&eph.idot, &data_source_rinex, &Galileo_week_continuous_number, &zero) << '\n';
out << get_nav_broadcast_orbit(&eph.idot, &data_source_rinex, &Galileo_week_continuous_number, d_version == 4 ? nullptr : &zero, d_version) << '\n';
// -------- BROADCAST ORBIT - 6
const uint32_t unavailable_hs = 3U;
@@ -2857,11 +3330,19 @@ void Rinex_Printer::log_rinex_nav_gal_nav(const std::map<int32_t, Galileo_Epheme
((e5b_dvs & 0x1U) << 8U);
const auto SVhealth = static_cast<double>(health_bits);
const auto SISA_d = static_cast<double>(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<double>(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 +3375,7 @@ void Rinex_Printer::log_rinex_nav_glo_gnav(const std::map<int32_t, Glonass_Gnav_
lengthCheck(line);
out << line << '\n';
}
if (d_version == 3)
if (d_version >= 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 +3383,11 @@ void Rinex_Printer::log_rinex_nav_glo_gnav(const std::map<int32_t, Glonass_Gnav_
const auto frame_day_of_week = static_cast<double>(p_frame_time_utc.date().day_of_week());
const auto frame_seconds_of_day = static_cast<double>(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 +3402,36 @@ void Rinex_Printer::log_rinex_nav_glo_gnav(const std::map<int32_t, Glonass_Gnav_
// -------- BROADCAST ORBIT - 3
out << get_nav_broadcast_orbit(&eph.d_Zn, &eph.d_VZn, &eph.d_AZn, &eph.d_E_n, d_version) << '\n';
// -------- BROADCAST ORBIT - 4 (only from RINEX 4 on, see Table A15)
if (d_version == 4)
{
// P1 is stored in minutes (0/30/45/60); map it back to the 2-bit broadcast code
const auto p1_minutes = static_cast<int32_t>(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<double>((static_cast<uint32_t>(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<uint32_t>(p1_code) << 2U) |
static_cast<uint32_t>(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<double>((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 +3447,38 @@ void Rinex_Printer::log_rinex_nav_bds_dnav(const std::map<int32_t, Beidou_Dnav_E
// -------- SV / EPOCH / SV CLK
const boost::posix_time::ptime p_utc_time = Rinex_Printer::compute_BDS_time(eph, eph.toc);
out << get_nav_sv_epoch_svclk_line(p_utc_time, sys_char, eph.PRN, eph.af0, eph.af1, eph.af2) << '\n';
if (d_version == 4)
{
const std::string msg_type = is_beidou_geo_prn(eph.PRN) ? "D2" : "D1";
out << get_nav_record_header_line("EPH", sys_char, eph.PRN, msg_type) << '\n';
}
out << get_nav_sv_epoch_svclk_line(p_utc_time, sys_char, eph.PRN, eph.af0, eph.af1, eph.af2, d_version) << '\n';
// -------- BROADCAST ORBIT - 1
out << get_nav_broadcast_orbit(&eph.AODE, &eph.Crs, &eph.delta_n, &eph.M_0) << '\n';
out << get_nav_broadcast_orbit(&eph.AODE, &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<double>(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<double>(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<double>(eph.SV_accuracy);
const auto health_d = static_cast<double>(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<double>(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 +3524,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 +3545,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 +3694,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 +3710,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<std::string> data;
@@ -3196,7 +3732,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);
}
+14 -4
View File
@@ -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<std::string>& 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;
@@ -249,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;
+39
View File
@@ -1876,6 +1876,31 @@ bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &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 || qzss_cnav_iono.valid)
{
const Gps_Iono &qzss_iono_ref = qzss_iono.valid ? static_cast<const Gps_Iono &>(qzss_iono) : static_cast<const Gps_Iono &>(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)
{
d_nav_data.ion_gal[0] = galileo_iono.ai0;
@@ -1910,6 +1935,20 @@ bool Rtklib_Solver::get_PVT(const std::map<int, Gnss_Synchro> &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 || qzss_cnav_utc_model.valid)
{
const Gps_Utc_Model &qzss_utc_ref = qzss_utc_model.valid ? static_cast<const Gps_Utc_Model &>(qzss_utc_model) : static_cast<const Gps_Utc_Model &>(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)
{
d_nav_data.utc_glo[0] = glonass_gnav_utc_model.d_tau_c; // ??
+10
View File
@@ -61,6 +61,10 @@
#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"
#include "rtklib_conversions.h"
#include "sensor_data/sensor_data_aggregator.h"
@@ -133,6 +137,12 @@ 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;
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
@@ -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<Gps_Iono> tmp_obj = std::make_shared<Gps_Iono>(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<Qzss_Iono> tmp_obj = std::make_shared<Qzss_Iono>(d_nav->get_iono());
this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj));
}
else
{
const std::shared_ptr<Gps_Iono> tmp_obj = std::make_shared<Gps_Iono>(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<Gps_Utc_Model> tmp_obj = std::make_shared<Gps_Utc_Model>(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<Qzss_Utc_Model> tmp_obj = std::make_shared<Qzss_Utc_Model>(d_nav->get_utc_model());
this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj));
}
else
{
const std::shared_ptr<Gps_Utc_Model> tmp_obj = std::make_shared<Gps_Utc_Model>(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<Gps_Iono> tmp_obj = std::make_shared<Gps_Iono>(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<Qzss_Iono> tmp_obj = std::make_shared<Qzss_Iono>(d_nav->get_iono());
this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj));
}
else
{
const std::shared_ptr<Gps_Iono> tmp_obj = std::make_shared<Gps_Iono>(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<Gps_Utc_Model> tmp_obj = std::make_shared<Gps_Utc_Model>(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<Qzss_Utc_Model> tmp_obj = std::make_shared<Qzss_Utc_Model>(d_nav->get_utc_model());
this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj));
}
else
{
const std::shared_ptr<Gps_Utc_Model> tmp_obj = std::make_shared<Gps_Utc_Model>(d_nav->get_utc_model());
this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj));
}
}
if (d_nav->almanac_validation() == true)
{
@@ -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<Gps_CNAV_Iono> tmp_obj = std::make_shared<Gps_CNAV_Iono>(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<Qzss_CNAV_Iono> tmp_obj = std::make_shared<Qzss_CNAV_Iono>(d_CNAV_Message->get_iono());
this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj));
}
else
{
const std::shared_ptr<Gps_CNAV_Iono> tmp_obj = std::make_shared<Gps_CNAV_Iono>(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<Gps_CNAV_Utc_Model> tmp_obj = std::make_shared<Gps_CNAV_Utc_Model>(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<Qzss_CNAV_Utc_Model> tmp_obj = std::make_shared<Qzss_CNAV_Utc_Model>(d_CNAV_Message->get_utc_model());
this->message_port_pub(pmt::mp("telemetry"), pmt::make_any(tmp_obj));
}
else
{
const std::shared_ptr<Gps_CNAV_Utc_Model> tmp_obj = std::make_shared<Gps_CNAV_Utc_Model>(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
@@ -100,6 +100,10 @@ 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
)
@@ -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
@@ -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
+45
View File
@@ -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
@@ -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
@@ -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,324 @@ 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<Rtklib_Solver>(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<void*>(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<int, Gnss_Synchro> gnss_observables_map;
gnss_observables_map[1] = gs;
auto rp = std::make_shared<Rinex_Printer>(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<Rtklib_Solver>(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<int, Gnss_Synchro> gnss_observables_map;
Gnss_Synchro gs{};
gs.PRN = 11;
gnss_observables_map[11] = std::move(gs);
auto rp = std::make_shared<Rinex_Printer>(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<Rtklib_Solver>(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<int, Gnss_Synchro> gnss_observables_map;
Gnss_Synchro gs{};
gs.PRN = 1;
gnss_observables_map[1] = std::move(gs);
auto rp = std::make_shared<Rinex_Printer>(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<Rtklib_Solver>(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<int, Gnss_Synchro> gnss_observables_map;
Gnss_Synchro gs{};
gs.System = 'G';
gs.PRN = 1;
std::memcpy(static_cast<void*>(gs.Signal), "2S", 3);
gnss_observables_map[1] = std::move(gs);
auto rp = std::make_shared<Rinex_Printer>(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, Rinex4QzssNav)
{
Pvt_Conf conf;
conf.use_e6_for_pvt = false;
const auto signal_enabled_flags = QZS_J1;
auto pvt_solution = std::make_shared<Rtklib_Solver>(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<int, Gnss_Synchro> gnss_observables_map;
Gnss_Synchro gs{};
gs.System = 'J';
gs.PRN = 193;
std::memcpy(static_cast<void*>(gs.Signal), "J1", 3);
gnss_observables_map[193] = std::move(gs);
auto rp = std::make_shared<Rinex_Printer>(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, Rinex4QzssCnavNav)
{
Pvt_Conf conf;
conf.use_e6_for_pvt = false;
const auto signal_enabled_flags = QZS_J5;
auto pvt_solution = std::make_shared<Rtklib_Solver>(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<int, Gnss_Synchro> gnss_observables_map;
Gnss_Synchro gs{};
gs.System = 'J';
gs.PRN = 193;
std::memcpy(static_cast<void*>(gs.Signal), "J5", 3);
gnss_observables_map[193] = std::move(gs);
auto rp = std::make_shared<Rinex_Printer>(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;