diff --git a/AUTHORS b/AUTHORS index dc0731f8f..1557425e9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -69,6 +69,7 @@ Marc Molina marc.molina.pena@gmail.com Contributor Marc Sales marcsales92@gmail.com Contributor Marcus Alagar mvala079@gmail.com Contributor Mathieu Favreau favreau.mathieu@hotmail.com Contributor +Miguel Gómez López mgomezl@ing.uc3m.es Contributor Minhaj Uddin Ahmad mahmad12@crimson.ua.edu Contributor Pedro Pereira pereirapedrocp@gmail.com Contributor Piyush Gupta piyush04111999@gmail.com Contributor diff --git a/conf/File_input/SBAS/gnss-sdr_SBAS_EGNOS_rx.conf b/conf/File_input/SBAS/gnss-sdr_SBAS_EGNOS_rx.conf new file mode 100644 index 000000000..b8bec15f3 --- /dev/null +++ b/conf/File_input/SBAS/gnss-sdr_SBAS_EGNOS_rx.conf @@ -0,0 +1,143 @@ +; GNSS-SDR configuration for SBAS / EGNOS L1 signal reception +; Reference: RTCA DO-229E, Appendix A +; +; SPDX-License-Identifier: GPL-3.0-or-later +; SPDX-FileCopyrightText: (C) 2010-2026 (see AUTHORS file for a list of contributors) +; +; ============================= SBAS / EGNOS notes ============================= +; +; Supported PRN range: 120-138 (RTCA DO-229, Table A-1). Which augmentation +; service (EGNOS, WAAS, ...) and satellite owns a given PRN changes over time +; as satellites are reassigned, so no PRN-to-satellite table is kept here; +; check the current SBAS almanac/status for the PRNs active in your recording. +; +; Signal parameters: +; - Frequency: 1575.42 MHz (same as GPS L1) +; - Code rate: 1.023 Mcps, 1023-chip code, 1 ms period +; - Data rate: 250 bps raw, 500 sps after rate-1/2 FEC (K=7) +; - Symbol period: 2 ms; coherent integration is limited to 1 code period (1 ms) +; because the telemetry decoder performs its own, unsynchronized +; sample/symbol pairing and does not tolerate extended tracking +; integration straddling a symbol boundary. +; - Low Doppler: GEO satellites have < ±100 Hz Doppler for a static receiver; +; doppler_max=1000 is generous, 500 often sufficient +; - No pilot: All SBAS signal power is in the data channel +; - No ranging: This SBAS implementation does not produce pseudoranges; SBAS +; is used here only to decode and dump L1 NAV messages, not as +; a ranging source for the PVT solution. +; +; ================== Adjust the fields marked with "<--" ==================== + +[GNSS-SDR] + +;######### GLOBAL OPTIONS ######################################################## +GNSS-SDR.internal_fs_sps=2048000 ; matches capture sampling frequency + + +;######### SIGNAL SOURCE ######################################################### +SignalSource.implementation=File_Signal_Source +SignalSource.filename=/home/victoria/SBAS_test/noeleec_maranhosa_SBAS.dat ; recorded on 1575.42 MHz +SignalSource.item_type=gr_complex +SignalSource.sampling_frequency=2048000 +SignalSource.samples=0 ; 0 = process entire file (~389 s) +SignalSource.repeat=false +SignalSource.dump=false + + +;######### SIGNAL CONDITIONER ##################################################### +SignalConditioner.implementation=Signal_Conditioner +DataTypeAdapter.implementation=Pass_Through +InputFilter.implementation=Pass_Through +InputFilter.item_type=gr_complex +InputFilter.output_item_type=gr_complex +InputFilter.taps_item_type=float +InputFilter.number_of_taps=5 +InputFilter.number_of_bands=2 +InputFilter.band1_begin=0.0 +InputFilter.band1_end=0.70 +InputFilter.band2_begin=0.80 +InputFilter.band2_end=1.0 +InputFilter.ampl1_begin=1.0 +InputFilter.ampl1_end=1.0 +InputFilter.ampl2_begin=0.0 +InputFilter.ampl2_end=0.0 +InputFilter.band1_error=1.0 +InputFilter.band2_error=1.0 +InputFilter.filter_type=bandpass +InputFilter.grid_density=16 +InputFilter.sampling_frequency=2048000 +InputFilter.IF=0 ; baseband — captured directly on 1575.42 MHz +Resampler.implementation=Pass_Through; +Resampler.sample_freq_in=2048000 +Resampler.sample_freq_out=2048000 +Resampler.item_type=gr_complex + + +;######### CHANNELS ############################################################## +; Signal code "S1" identifies the SBAS L1 channel type. +; Assign the SBAS PRN numbers you want to track (PRN 120–138). +Channel.signal=S1 +Channels.in_acquisition=3 +Channels_S1.count=3 ; satellites present in this recording + +; Pre-assign the PRNs present in this recording so acquisition starts immediately. +Channel0.satellite=121 ; <-- set to a PRN present in your recording +Channel1.satellite=136 ; <-- set to a PRN present in your recording +Channel2.satellite=123 ; <-- set to a PRN present in your recording + + +;######### ACQUISITION ########################################################### +; GEO satellites are quasi-stationary: valid Doppler range is very narrow. +; A doppler_max of 500–1000 Hz is typically more than sufficient. +; Lower values speed up acquisition significantly. +Acquisition_S1.implementation=SBAS_L1_PCPS_Acquisition +Acquisition_S1.item_type=gr_complex +Acquisition_S1.pfa=0.01 ; probability of false alarm – adapts to noise floor +Acquisition_S1.doppler_max=5000 ; [Hz] – GEO Doppler is < ±100 Hz for static RX +Acquisition_S1.doppler_step=250 ; [Hz] – fine step for GEO low-Doppler satellites +Acquisition_S1.dump=false +Acquisition_S1.dump_filename=/home/victoria/SBAS_test/process/sbas_acquisition + + +;######### TRACKING ############################################################## +; extend_correlation_symbols is capped to 1 code period (1 ms): the telemetry +; decoder performs its own, unsynchronized sample/symbol pairing and cannot +; tolerate extended coherent integration straddling a symbol boundary. +Tracking_S1.implementation=SBAS_L1_DLL_PLL_Tracking +Tracking_S1.item_type=gr_complex +;Tracking_S1.early_late_space_chips=0.5 +Tracking_S1.pll_bw_hz=25.0 ; [Hz] – narrow BW for low-dynamics GEO +Tracking_S1.dll_bw_hz=1 ; [Hz] – very stable for GEO +Tracking_S1.extend_correlation_symbols=1 ; 1ms per output; SBAS telemetry decoder handles FEC/bit-sync +Tracking_S1.track_pilot=false ; SBAS has no pilot component +Tracking_S1.dump=true +Tracking_S1.dump_filename=/home/victoria/SBAS_test/process/sbas_tracking_ch_ + + +;######### TELEMETRY DECODER ##################################################### +; The SBAS telemetry decoder implements Viterbi decoding (rate-1/2, K=7), +; CRC-24Q verification, and RTCA DO-229 message framing. +TelemetryDecoder_S1.implementation=SBAS_L1_Telemetry_Decoder +TelemetryDecoder_S1.dump=true +TelemetryDecoder_S1.dump_filename=/home/victoria/SBAS_test/process/sbas_telemetry.dat + + +;######### OBSERVABLES ########################################################### +Observables.implementation=Hybrid_Observables +Observables.dump=true +Observables.dump_filename=/home/victoria/SBAS_test/process/sbas_observables.dat + + +;######### PVT ################################################################### +; Note: SBAS is not used as a ranging source in this configuration. The SBAS +; telemetry decoder never marks its output as valid for pseudorange computation, +; and no SBAS DGPS/integrity corrections are applied to the PVT solution. +PVT.implementation=RTKLIB_PVT +PVT.positioning_mode=Single +PVT.output_rate_ms=100 +PVT.display_rate_ms=500 +PVT.trop_model=Saastamoinen +PVT.flag_rtcm_server=false +PVT.rinex_version=3 +PVT.dump=true +PVT.dump_filename=/home/victoria/SBAS_test/process/sbas_pvt.dat diff --git a/src/algorithms/PVT/libs/rinex_printer.cc b/src/algorithms/PVT/libs/rinex_printer.cc index 05dde2c59..a832b740f 100644 --- a/src/algorithms/PVT/libs/rinex_printer.cc +++ b/src/algorithms/PVT/libs/rinex_printer.cc @@ -86,6 +86,8 @@ std::string signal_flag_to_string(signal_flag flag) { case GPS_1C: return "1C"; + case SBAS_S1: + return "S1"; case GPS_2S: return "2S"; case GPS_L5: diff --git a/src/algorithms/acquisition/adapters/CMakeLists.txt b/src/algorithms/acquisition/adapters/CMakeLists.txt index 047a68a0f..1e3a30c7c 100644 --- a/src/algorithms/acquisition/adapters/CMakeLists.txt +++ b/src/algorithms/acquisition/adapters/CMakeLists.txt @@ -8,11 +8,13 @@ set(ACQ_ADAPTER_SOURCES pcps_acquisition_adapter.cc pcps_acquisition_adapter_custom.cc + sbas_l1_pcps_acquisition.cc ) set(ACQ_ADAPTER_HEADERS pcps_acquisition_adapter.h pcps_acquisition_adapter_custom.h + sbas_l1_pcps_acquisition.h ) if(ENABLE_FPGA) diff --git a/src/algorithms/acquisition/adapters/pcps_acquisition_adapter.cc b/src/algorithms/acquisition/adapters/pcps_acquisition_adapter.cc index 3591fc4f3..972ffe3b3 100644 --- a/src/algorithms/acquisition/adapters/pcps_acquisition_adapter.cc +++ b/src/algorithms/acquisition/adapters/pcps_acquisition_adapter.cc @@ -28,6 +28,7 @@ #include "Galileo_E5a.h" #include "Galileo_E5b.h" #include "Galileo_E6.h" +#include "SBAS_L1.h" #include "acq_conf.h" #include "beidou_b1c_signal_replica.h" #include "beidou_b1i_signal_replica.h" @@ -44,6 +45,7 @@ #include "gps_sdr_signal_replica.h" #include "qzss.h" #include "qzss_signal_replica.h" +#include "sbas_signal_replica.h" #include "signal_flag.h" #if USE_GLOG_AND_GFLAGS @@ -104,6 +106,8 @@ signal_info get_signal_info(signal_flag sig_flag) return {QZSS_L1_CHIP_RATE, QZSS_L1_OPT_ACQ_FS_SPS, QZSS_L1_CODE_LENGTH, QZSS_L1_PERIOD_MS}; case QZS_J5: return {QZSS_L5_CHIP_RATE, QZSS_L5_OPT_ACQ_FS_SPS, QZSS_L5_CODE_LENGTH, QZSS_L5I_PERIOD_MS}; + case SBAS_S1: + return {SBAS_L1_CODE_RATE_CPS, SBAS_L1_OPT_ACQ_FS_SPS, SBAS_L1_CODE_LENGTH_CHIPS, SBAS_L1_CODE_PERIOD_MS}; default: break; } @@ -190,6 +194,9 @@ void code_gen_complex_sampled(signal_flag sig_flag, const Acq_Conf& conf, const case QZS_J5: qzss_l5i_code_gen_complex_sampled(dest, gnss_synchro.PRN, sampling_freq); break; + case SBAS_S1: + sbas_l1_code_gen_complex_sampled(dest, gnss_synchro.PRN, sampling_freq); + break; default: break; } diff --git a/src/algorithms/acquisition/adapters/sbas_l1_pcps_acquisition.cc b/src/algorithms/acquisition/adapters/sbas_l1_pcps_acquisition.cc new file mode 100644 index 000000000..cf801827e --- /dev/null +++ b/src/algorithms/acquisition/adapters/sbas_l1_pcps_acquisition.cc @@ -0,0 +1,30 @@ +/*! + * \file sbas_l1_pcps_acquisition.cc + * \brief Adapts a PCPS acquisition block to an AcquisitionInterface for + * SBAS L1 signals + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com + * + * ----------------------------------------------------------------------------- + * + * 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 + * + * ----------------------------------------------------------------------------- + */ + + +#include "sbas_l1_pcps_acquisition.h" + + +SbasL1PcpsAcquisition::SbasL1PcpsAcquisition( + const ConfigurationInterface* configuration, + const std::string& role, + unsigned int in_streams, + unsigned int out_streams) + : PcpsAcquisitionAdapter(configuration, role, "SBAS_L1_PCPS_Acquisition", in_streams, out_streams, SBAS_S1) +{ +} diff --git a/src/algorithms/acquisition/adapters/sbas_l1_pcps_acquisition.h b/src/algorithms/acquisition/adapters/sbas_l1_pcps_acquisition.h new file mode 100644 index 000000000..3c432a021 --- /dev/null +++ b/src/algorithms/acquisition/adapters/sbas_l1_pcps_acquisition.h @@ -0,0 +1,53 @@ +/*! + * \file sbas_l1_pcps_acquisition.h + * \brief Adapts a PCPS acquisition block to an AcquisitionInterface for + * SBAS L1 signals + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com + * + * ----------------------------------------------------------------------------- + * + * 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_SBAS_L1_PCPS_ACQUISITION_H +#define GNSS_SDR_SBAS_L1_PCPS_ACQUISITION_H + +#include "pcps_acquisition_adapter.h" +#include + +class ConfigurationInterface; + +/** \addtogroup Acquisition + * \{ */ +/** \addtogroup Acq_adapters + * \{ */ + +/*! + * \brief This class adapts a PCPS acquisition block to an AcquisitionInterface + * for SBAS L1 signals (PRN 120–138, same chip rate and code length as GPS L1 C/A). + */ +class SbasL1PcpsAcquisition : public PcpsAcquisitionAdapter +{ +public: + SbasL1PcpsAcquisition( + const ConfigurationInterface* configuration, + const std::string& role, + unsigned int in_streams, + unsigned int out_streams); + + ~SbasL1PcpsAcquisition() = default; +}; + + +/** \} */ +/** \} */ + +#endif // GNSS_SDR_SBAS_L1_PCPS_ACQUISITION_H diff --git a/src/algorithms/libs/CMakeLists.txt b/src/algorithms/libs/CMakeLists.txt index 8f51a823d..eace4b870 100644 --- a/src/algorithms/libs/CMakeLists.txt +++ b/src/algorithms/libs/CMakeLists.txt @@ -34,6 +34,7 @@ set(GNSS_SPLIBS_SOURCES short_x2_to_cshort.cc gnss_sdr_string_literals.cc qzss_signal_replica.cc + sbas_signal_replica.cc ) set(GNSS_SPLIBS_HEADERS @@ -70,6 +71,7 @@ set(GNSS_SPLIBS_HEADERS gnss_time.h matlab_writter_helper.h qzss_signal_replica.h + sbas_signal_replica.h ) set(GNSS_SPLIBS_HEADERS ${GNSS_SPLIBS_HEADERS} diff --git a/src/algorithms/libs/sbas_signal_replica.cc b/src/algorithms/libs/sbas_signal_replica.cc new file mode 100644 index 000000000..8d788a7c6 --- /dev/null +++ b/src/algorithms/libs/sbas_signal_replica.cc @@ -0,0 +1,71 @@ +/*! + * \file sbas_signal_replica.cc + * \brief This file implements signal generators for SBAS L1 signals + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com + * + * SBAS PRN codes (120–138) are generated by the GPS L1 C/A code generator + * using SBAS-specific G2 initial state delays (index offset -88 into the + * delays table, covering indices 31..50 for PRN 120..138). + * + * ----------------------------------------------------------------------------- + * + * 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 + * + * ----------------------------------------------------------------------------- + */ + + +#include "sbas_signal_replica.h" +#include "gps_sdr_signal_replica.h" +#include +#include + + +void sbas_l1_code_gen_complex_sampled(own::span> dest, uint32_t prn, int32_t sampling_freq) +{ + constexpr int32_t code_chip_rate = 1023000; + constexpr int32_t code_length = 1023; + + const float ts = 1.0F / static_cast(sampling_freq); + const float tc = 1.0F / static_cast(code_chip_rate); + + std::array, code_length> code_aux{}; + sbas_l1_code_gen_complex(code_aux, prn, 0U); + + const auto samples_per_code = static_cast(dest.size()); + for (int32_t i = 0; i < samples_per_code; i++) + { + const auto idx = static_cast(std::floor(ts * static_cast(i) / tc)); + if (i == samples_per_code - 1) + { + dest[i] = code_aux[code_length - 1]; + } + else + { + dest[i] = code_aux[idx]; + } + } +} + + +void sbas_l1_code_gen_float(own::span dest, uint32_t prn) +{ + gps_l1_ca_code_gen_float(dest, static_cast(prn), 0U); +} + + +void sbas_l1_code_gen_complex(own::span> dest, uint32_t prn, uint32_t chip_shift) +{ + constexpr uint32_t code_length = 1023; + std::array ca_code_int{}; + gps_l1_ca_code_gen_int(ca_code_int, static_cast(prn), chip_shift); + for (uint32_t ii = 0; ii < code_length; ++ii) + { + dest[ii] = {static_cast(ca_code_int[ii]), 0.0F}; + } +} diff --git a/src/algorithms/libs/sbas_signal_replica.h b/src/algorithms/libs/sbas_signal_replica.h new file mode 100644 index 000000000..6eea88688 --- /dev/null +++ b/src/algorithms/libs/sbas_signal_replica.h @@ -0,0 +1,55 @@ +/*! + * \file sbas_signal_replica.h + * \brief This file implements signal generators for SBAS L1 signals + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com + * + * SBAS PRN codes (120–138) share the same Gold-code generator as GPS L1 C/A + * but use SBAS-specific G2 initial state delays. The underlying code generator + * already handles SBAS PRNs; these wrappers provide a clean SBAS-specific API. + * + * Reference: RTCA DO-229E, Appendix A + * + * ----------------------------------------------------------------------------- + * + * 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_SBAS_SIGNAL_REPLICA_H +#define GNSS_SDR_SBAS_SIGNAL_REPLICA_H + +#include +#include +#if HAS_STD_SPAN +#include +namespace own = std; +#else +#include +namespace own = gsl_lite; +#endif + +/** \addtogroup Algorithms_Library + * \{ */ +/** \addtogroup Algorithm_libs algorithms_libs + * \{ */ + +//! Generates complex SBAS L1 C/A code for the desired SV PRN, sampled at sampling_freq [Hz] +void sbas_l1_code_gen_complex_sampled(own::span> dest, uint32_t prn, int32_t sampling_freq); + +//! Generates real SBAS L1 C/A code for the desired SV PRN (chip_shift=0) +void sbas_l1_code_gen_float(own::span dest, uint32_t prn); + +//! Generates complex SBAS L1 C/A code for the desired SV PRN with a given chip_shift +void sbas_l1_code_gen_complex(own::span> dest, uint32_t prn, uint32_t chip_shift); + +/** \} */ +/** \} */ + +#endif // GNSS_SDR_SBAS_SIGNAL_REPLICA_H diff --git a/src/algorithms/telemetry_decoder/gnuradio_blocks/sbas_l1_telemetry_decoder_gs.cc b/src/algorithms/telemetry_decoder/gnuradio_blocks/sbas_l1_telemetry_decoder_gs.cc index 5e3521168..69581c905 100644 --- a/src/algorithms/telemetry_decoder/gnuradio_blocks/sbas_l1_telemetry_decoder_gs.cc +++ b/src/algorithms/telemetry_decoder/gnuradio_blocks/sbas_l1_telemetry_decoder_gs.cc @@ -2,19 +2,22 @@ * \file sbas_l1_telemetry_decoder_gs.cc * \brief Implementation of a SBAS telemetry data decoder block * \author Daniel Fehr 2013. daniel.co(at)bluewin.ch + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com * * ----------------------------------------------------------------------------- * * GNSS-SDR is a Global Navigation Satellite System software-defined receiver. * This file is part of GNSS-SDR. * - * Copyright (C) 2010-2020 (see AUTHORS file for a list of contributors) + * Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors) * SPDX-License-Identifier: GPL-3.0-or-later * * ----------------------------------------------------------------------------- */ #include "sbas_l1_telemetry_decoder_gs.h" +#include "SBAS_L1.h" #include "gnss_synchro.h" #include "viterbi_decoder_sbas.h" #include // for mp @@ -23,6 +26,9 @@ #include // for abs #include // for exception #include // for operator<<, setw +#include // for std::cout +#include // for std::string +#include // for std::move #if USE_GLOG_AND_GFLAGS #include @@ -37,25 +43,35 @@ #define LMORE 5 // -sbas_l1_telemetry_decoder_gs_sptr sbas_l1_make_telemetry_decoder_gs(bool dump) +sbas_l1_telemetry_decoder_gs_sptr sbas_l1_make_telemetry_decoder_gs( + bool dump, + std::string dump_filename) { - return sbas_l1_telemetry_decoder_gs_sptr(new sbas_l1_telemetry_decoder_gs(dump)); + return sbas_l1_telemetry_decoder_gs_sptr(new sbas_l1_telemetry_decoder_gs(dump, std::move(dump_filename))); } -sbas_l1_telemetry_decoder_gs::sbas_l1_telemetry_decoder_gs(bool dump) - : telemetry_impl_interface("sbas_l1_telemetry_decoder_gs", - gr::io_signature::make(1, 1, sizeof(Gnss_Synchro)), - gr::io_signature::make(1, 1, sizeof(Gnss_Synchro))), - d_dump(dump), - d_channel(0), - d_block_size(D_SAMPLES_PER_SYMBOL * D_SYMBOLS_PER_BIT * D_BLOCK_SIZE_IN_BITS) +sbas_l1_telemetry_decoder_gs::sbas_l1_telemetry_decoder_gs( + bool dump, + std::string dump_filename) : telemetry_impl_interface("sbas_l1_telemetry_decoder_gs", + gr::io_signature::make(1, 1, sizeof(Gnss_Synchro)), + gr::io_signature::make(1, 1, sizeof(Gnss_Synchro))), + d_dump(dump), + d_channel(0), + d_block_size(D_SAMPLES_PER_SYMBOL * D_SYMBOLS_PER_BIT * D_BLOCK_SIZE_IN_BITS) { configure_basic_outputs(); // initialize internal vars LOG(INFO) << "SBAS L1 TELEMETRY PROCESSING: satellite " << d_satellite; set_output_multiple(1); + + if (d_dump && !dump_filename.empty()) + { + // Store the stem; actual EMS file is opened lazily in general_work() + // after set_satellite() has been called and d_satellite PRN is valid. + d_dump_filename = std::move(dump_filename); + } } @@ -72,6 +88,10 @@ sbas_l1_telemetry_decoder_gs::~sbas_l1_telemetry_decoder_gs() LOG(WARNING) << "Exception in destructor closing the dump file " << ex.what(); } } + if (d_ems_file.is_open()) + { + d_ems_file.close(); + } } @@ -89,6 +109,31 @@ void sbas_l1_telemetry_decoder_gs::set_channel(int32_t channel) } +void sbas_l1_telemetry_decoder_gs::reset() +{ + d_sample_buf.clear(); + d_sample_stamps.clear(); + d_sample_aligner.reset(); + d_symbol_aligner_and_decoder.reset(); + d_frame_detector.reset(); + d_crc_verifier.reset(); + if (d_ems_file.is_open()) + { + d_ems_file.close(); + } +} + + +double sbas_l1_telemetry_decoder_gs::compute_message_timestamp(double first_bit_stamp_s, bool sample_aligned, bool symbol_aligned) +{ + // Residual sub-bit correction: which of the two possible sample/symbol + // alignments was chosen shifts the true message start by a few samples + // with respect to the first bit's own timestamp. + const int32_t offset_samples = (sample_aligned ? 0 : -1) + D_SAMPLES_PER_SYMBOL * (symbol_aligned ? 0 : -1); + return first_bit_stamp_s + static_cast(offset_samples) * SBAS_L1_CODE_PERIOD_S; +} + + // ### helper class for sample alignment ### sbas_l1_telemetry_decoder_gs::Sample_Aligner::Sample_Aligner() { @@ -225,10 +270,11 @@ bool sbas_l1_telemetry_decoder_gs::Symbol_Aligner_And_Decoder::get_bits(const st void sbas_l1_telemetry_decoder_gs::Frame_Detector::reset() { d_buffer.clear(); + d_bit_stamps.clear(); } -void sbas_l1_telemetry_decoder_gs::Frame_Detector::get_frame_candidates(const std::vector &bits, std::vector>> &msg_candidates) +void sbas_l1_telemetry_decoder_gs::Frame_Detector::get_frame_candidates(const std::vector &bits, const std::vector &bit_stamps, std::vector>> &msg_candidates) { std::stringstream ss; const uint32_t sbas_msg_length = 250; @@ -239,15 +285,15 @@ void sbas_l1_telemetry_decoder_gs::Frame_Detector::get_frame_candidates(const st << "d_buffer.size()=" << d_buffer.size() << "\tbits.size()=" << bits.size(); ss << "copy bits "; int32_t count = 0; - // copy new bits into the working buffer - for (auto bit_it = bits.cbegin(); bit_it < bits.cend(); ++bit_it) + // copy new bits (and their absolute timestamps) into the working buffer + for (size_t i = 0; i < bits.size(); i++) { - d_buffer.push_back(*bit_it); - ss << *bit_it; + d_buffer.push_back(bits[i]); + d_bit_stamps.push_back(bit_stamps[i]); + ss << bits[i]; count++; } VLOG(SAMP_SYNC) << ss.str() << " into working buffer (" << count << " bits)"; - int32_t relative_preamble_start = 0; while (d_buffer.size() >= sbas_msg_length) { // compare with all preambles @@ -274,7 +320,8 @@ void sbas_l1_telemetry_decoder_gs::Frame_Detector::get_frame_candidates(const st candidate_bit_it = candidate_bit_it == 0 ? 1 : 0; } } - msg_candidates.emplace_back(relative_preamble_start, candidate); + // the candidate's absolute timestamp is that of its first (preamble) bit + msg_candidates.emplace_back(d_bit_stamps.front(), candidate); ss.str(""); ss << "preamble " << preample_it - preambles.begin() << (inv_preamble_detected ? " inverted" : " normal") << " detected! candidate="; for (auto bit_it = candidate.begin(); bit_it < candidate.end(); ++bit_it) @@ -284,9 +331,9 @@ void sbas_l1_telemetry_decoder_gs::Frame_Detector::get_frame_candidates(const st VLOG(EVENT) << ss.str(); } } - relative_preamble_start++; - // remove bit in front + // remove bit (and its timestamp) in front d_buffer.pop_front(); + d_bit_stamps.pop_front(); } } @@ -405,8 +452,11 @@ int sbas_l1_telemetry_decoder_gs::general_work(int noutput_items __attribute__(( // copy correlation samples into samples vector d_sample_buf.push_back(current_symbol.Prompt_I); // add new symbol to the symbol queue - // store the time stamp of the first sample in the processed sample block + // absolute reception timestamp of this sample; stored so that, once a message is + // eventually found several blocks later, its true timestamp can be recovered instead + // of approximated from whichever sample happens to be current at that later time. const double sample_stamp = static_cast(in[0].Tracking_sample_counter) / static_cast(in[0].fs); + d_sample_stamps.push_back(sample_stamp); // decode only if enough samples in buffer if (d_sample_buf.size() >= d_block_size) @@ -421,10 +471,18 @@ int sbas_l1_telemetry_decoder_gs::general_work(int noutput_items __attribute__(( std::vector bits; const bool symbol_alignment = d_symbol_aligner_and_decoder.get_bits(symbols, bits); + // absolute timestamp of each decoded bit: the timestamp of the first + // sample that contributed to it, taken from this block's own samples + std::vector bit_stamps(bits.size()); + for (size_t i = 0; i < bits.size(); i++) + { + bit_stamps[i] = d_sample_stamps[i * D_SAMPLES_PER_SYMBOL * D_SYMBOLS_PER_BIT]; + } + // search for preambles // and extract the corresponding message candidates std::vector msg_candidates; - d_frame_detector.get_frame_candidates(bits, msg_candidates); + d_frame_detector.get_frame_candidates(bits, bit_stamps, msg_candidates); // verify checksum // and return the valid messages @@ -436,30 +494,147 @@ int sbas_l1_telemetry_decoder_gs::general_work(int noutput_items __attribute__(( // std::vector sbas_raw_msgs; for (const auto &valid_msg : valid_msgs) { - const int32_t message_sample_offset = - (sample_alignment ? 0 : -1) + D_SAMPLES_PER_SYMBOL * (symbol_alignment ? -1 : 0) + D_SAMPLES_PER_SYMBOL * D_SYMBOLS_PER_BIT * valid_msg.first; - const double message_sample_stamp = sample_stamp + static_cast(message_sample_offset) / 1000.0; + // valid_msg.first is the absolute timestamp of the message's first bit + const double message_sample_stamp = compute_message_timestamp(valid_msg.first, sample_alignment, symbol_alignment); VLOG(EVENT) << "message_sample_stamp=" << message_sample_stamp - << " (sample_stamp=" << sample_stamp + << " (first_bit_stamp=" << valid_msg.first << " sample_alignment=" << sample_alignment << " symbol_alignment=" << symbol_alignment - << " relative_preamble_start=" << valid_msg.first - << " message_sample_offset=" << message_sample_offset << ")"; - // Sbas_Raw_Msg sbas_raw_msg(message_sample_stamp, this->d_satellite.get_PRN(), it->second); - // sbas_raw_msgs.push_back(sbas_raw_msg); + // extract message type: bits 8-13 of 250-bit message (top 6 bits of byte[1]) + const int msg_type = (valid_msg.second.size() > 1) + ? ((static_cast(valid_msg.second[1]) >> 2) & 0x3F) + : -1; + VLOG(EVENT) << "SBAS PRN " << d_satellite.get_PRN() + << " MT" << msg_type + << " at t=" << std::fixed << std::setprecision(3) + << message_sample_stamp << " s"; + // Lazy-open message dump file on first decoded message so PRN is known. + // Dumps are overwritten (not appended) on every run. + if (d_dump && !d_dump_filename.empty() && !d_ems_file.is_open()) + { + std::string ems_fn = d_dump_filename; + const std::string dat_ext(".dat"); + if (ems_fn.size() > dat_ext.size() && + ems_fn.compare(ems_fn.size() - dat_ext.size(), dat_ext.size(), dat_ext) == 0) + { + ems_fn = ems_fn.substr(0, ems_fn.size() - dat_ext.size()); + } + ems_fn += "_PRN" + std::to_string(d_satellite.get_PRN()) + ".ems"; + d_ems_file.open(ems_fn, std::ios::out | std::ios::trunc); + if (d_ems_file.is_open()) + { + // NOTE on naming/format: this is a raw dump of decoded SBAS L1 messages + // using the same field layout as ESA's EGNOS Message Service (EMS) + // archive files (week, tow, prn, type : hex bytes). It is NOT a + // conformant EMS file: this receiver has no absolute GPS time + // reference, so "week" is always 0 and "tow" is the receiver's + // elapsed time since recording start, not the real GPS time of week. + d_ems_file << "; Raw SBAS L1 message dump (EMS-like layout, not a conformant EMS file)\n"; + d_ems_file << "; Satellite: SBAS PRN " << d_satellite.get_PRN() << "\n"; + d_ems_file << "; TOW field: receiver time in seconds from recording start\n"; + d_ems_file << "; GPS week = 0 (no absolute GPS fix in this output)\n"; + d_ems_file << "; Msg bytes: 32 bytes (250-bit SBAS frame zero-padded to 256 bits)\n"; + d_ems_file << "; Layout: preamble(8b) type(6b) data(212b) CRC-24Q(24b) pad(6b)\n"; + d_ems_file << "; Format: : <64_hex_chars>\n"; + d_ems_file << ";\n"; + LOG(INFO) << "SBAS raw message dump file opened: " << ems_fn; + } + else + { + LOG(WARNING) << "Could not open SBAS raw message dump file: " << ems_fn; + } + } + if (d_ems_file.is_open()) + { + d_ems_file << std::dec << std::setfill(' ') + << std::setw(4) << 0 << " " + << std::setw(10) << std::fixed << std::setprecision(3) + << message_sample_stamp << " " + << std::setw(3) << d_satellite.get_PRN() << " " + << std::setw(2) << msg_type << " : "; + for (const auto byte : valid_msg.second) + { + d_ems_file << std::setw(2) << std::setfill('0') << std::hex + << static_cast(byte); + } + d_ems_file << std::dec << std::setfill(' ') << '\n'; + d_ems_file.flush(); + } + // Sbas_Raw_Msg sbas_raw_msg(message_sample_stamp, this->d_satellite.get_PRN(), it->second); + // sbas_raw_msgs.push_back(sbas_raw_msg); +#if __cplusplus == 201103L + const int default_precision = std::cout.precision(); +#else + const auto default_precision{std::cout.precision()}; +#endif + std::cout << "New SBAS L1 NAV message received in channel " << d_channel + << ": MT" << msg_type << " from satellite " << d_satellite + << " with CN0=" << std::setprecision(2) << current_symbol.CN0_dB_hz + << std::setprecision(default_precision) << " dB-Hz" << std::endl; } - // parse messages - // and send them to the SBAS raw message queue - // for(std::vector::iterator it = sbas_raw_msgs.begin(); it != sbas_raw_msgs.end(); it++) - // { - // std::cout << "SBAS message type " << it->get_msg_type() << " from PRN" << it->get_prn() << " received\n"; - // sbas_telemetry_data.update(*it); - // } + // TODO: parse per-message-type content and apply SBAS corrections + // ----------------------------------------------------------------------- + // SBAS L1 message parsing status (RTCA DO-229E / ICAO SARPs Appendix B) + // ----------------------------------------------------------------------- + // SIGNAL / TRANSPORT LAYER — implemented: + // [x] Viterbi FEC decoding (rate-1/2, K=7 ) + // [x] Three-preamble detection (0x53 / 0x9A / 0xC6) + // [x] CRC-24Q verification + // [x] 6-bit message type extraction + // [x] Raw EMS file output (32 bytes / message) + // + // NAVIGATION DATA LAYER — NOT yet implemented (all content is written to + // the EMS file as raw bytes; no correction data is used by the receiver): + // + // Satellite information messages (DO-229E §A.4.4): + // [ ] MT1 — PRN mask assignments (up to 51 of 210 GNSS/GEO PRNs) + // [ ] MT2–5 — Fast corrections + UDREI for satellites in mask + // [ ] MT6 — Integrity information (UDREI for all 51 mask slots) + // [ ] MT7 — Fast correction degradation factors (aij table) + // [ ] MT9 — GEO navigation message (ECEF pos/vel, URA, t0) + // [ ] MT17 — GEO satellite almanacs (health/status + rough position) + // [ ] MT24 — Mixed fast corrections / long-term corrections + // [ ] MT25 — Long-term satellite error corrections (orbit + clock) + // [ ] MT28 — Clock–Ephemeris covariance matrix (optional) + // + // Ionospheric messages (DO-229E §A.4.4.9–10): + // [ ] MT18 — Ionospheric grid point (IGP) masks (per band 0–10) + // [ ] MT26 — Ionospheric grid delays (GIVD) + error bounds (GIVEI) + // + // Ancillary messages: + // [ ] MT0 — Don't Use / test-mode flag (safety de-selection) + // [ ] MT10 — Degradation parameters (σ²_flt, σ²_iono for PA ops) + // [ ] MT12 — SBAS Network time / UTC offset (GPS-week + SOW) + // [ ] MT27 — Service message / δUDRE regional factors (optional) + // [ ] MT62 — Internal test message (optional, not used by EGNOS) + // [ ] MT63 — Null message (optional, not used by EGNOS) + // + // SBAS L5 / DFMC messages (DO-229F, dual-freq multi-constellation): + // [ ] MT31 — Satellite Mask (up to 92 SV slots) + // [ ] MT32 — Clock-Ephemeris corrections + covariance matrix + // [ ] MT34–36 — Integrity messages (DFRECIs / DFREIs) + // [ ] MT37 — Degradation parameters + DFREI scale table + // [ ] MT39/40 — SBAS satellite ephemeris (Keplerian) + covariance + // [ ] MT47 — SBAS satellite almanacs (Keplerian, 2 GEOs per msg) + // + // Receiver-level corrections (depends on navigation layer above): + // [ ] Apply fast + long-term satellite corrections to pseudoranges + // [ ] Apply ionospheric grid delay corrections (MOPS §A.4.4.10) + // [ ] Enable SBAS ranging (set Flag_valid_word = true when ranging ok) + // [ ] Propagate SBAS correction quality (UDRE, GIVE) into PVT + // + // Reference: https://gssc.esa.int/navipedia/index.php/ + // The_EGNOS_SBAS_Message_Format_Explained + // ----------------------------------------------------------------------- + // (old hook kept for reference) + // for (auto it = sbas_raw_msgs.begin(); it != sbas_raw_msgs.end(); ++it) + // sbas_telemetry_data.update(*it); // clear all processed samples in the input buffer d_sample_buf.clear(); + d_sample_stamps.clear(); } // UPDATE GNSS SYNCHRO DATA diff --git a/src/algorithms/telemetry_decoder/gnuradio_blocks/sbas_l1_telemetry_decoder_gs.h b/src/algorithms/telemetry_decoder/gnuradio_blocks/sbas_l1_telemetry_decoder_gs.h index acad02fd4..9e335876f 100644 --- a/src/algorithms/telemetry_decoder/gnuradio_blocks/sbas_l1_telemetry_decoder_gs.h +++ b/src/algorithms/telemetry_decoder/gnuradio_blocks/sbas_l1_telemetry_decoder_gs.h @@ -2,13 +2,15 @@ * \file sbas_l1_telemetry_decoder_gs.h * \brief Interface of a SBAS telemetry data decoder block * \author Daniel Fehr 2013. daniel.co(at)bluewin.ch + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com * * ----------------------------------------------------------------------------- * * GNSS-SDR is a Global Navigation Satellite System software-defined receiver. * This file is part of GNSS-SDR. * - * Copyright (C) 2010-2020 (see AUTHORS file for a list of contributors) + * Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors) * SPDX-License-Identifier: GPL-3.0-or-later * * ----------------------------------------------------------------------------- @@ -36,7 +38,9 @@ class sbas_l1_telemetry_decoder_gs; using sbas_l1_telemetry_decoder_gs_sptr = gnss_shared_ptr; -sbas_l1_telemetry_decoder_gs_sptr sbas_l1_make_telemetry_decoder_gs(bool dump); +sbas_l1_telemetry_decoder_gs_sptr sbas_l1_make_telemetry_decoder_gs( + bool dump, + std::string dump_filename = std::string()); /*! * \brief This class implements a block that decodes the SBAS integrity and @@ -48,7 +52,15 @@ public: ~sbas_l1_telemetry_decoder_gs() override; void set_satellite(const Gnss_Satellite &satellite) override; //!< Set satellite PRN void set_channel(int32_t channel) override; //!< Set receiver's channel - inline void reset() override {}; + //! Clears all internal decoding state (buffers, aligners, Viterbi decoders) + //! and closes the dump file, so that a channel reassigned to a different + //! PRN never mixes messages from the previous satellite into its file. + void reset() override; + + //! Exposed for unit testing: combines an absolute per-bit reception + //! timestamp with the residual sample/symbol alignment correction to + //! obtain the final message timestamp. + static double compute_message_timestamp(double first_bit_stamp_s, bool sample_aligned, bool symbol_aligned); /*! * \brief This is where all signal processing takes place @@ -57,9 +69,11 @@ public: gr_vector_const_void_star &input_items, gr_vector_void_star &output_items) override; private: - friend sbas_l1_telemetry_decoder_gs_sptr sbas_l1_make_telemetry_decoder_gs(bool dump); + friend sbas_l1_telemetry_decoder_gs_sptr sbas_l1_make_telemetry_decoder_gs( + bool dump, + std::string dump_filename); - explicit sbas_l1_telemetry_decoder_gs(bool dump); + explicit sbas_l1_telemetry_decoder_gs(bool dump, std::string dump_filename = std::string()); void viterbi_decoder(double *page_part_symbols, int32_t *page_part_bits); void align_samples(); @@ -74,12 +88,15 @@ private: std::string d_dump_filename; std::ofstream d_dump_file; + std::ofstream d_ems_file; - size_t d_block_size; //!< number of samples which are processed during one invocation of the algorithms - std::vector d_sample_buf; //!< input buffer holding the samples to be processed in one block + size_t d_block_size; //!< number of samples which are processed during one invocation of the algorithms + std::vector d_sample_buf; //!< input buffer holding the samples to be processed in one block + std::vector d_sample_stamps; //!< absolute reception timestamp [s] of each sample in d_sample_buf, same indexing - typedef std::pair> msg_candiate_int_t; - typedef std::pair> msg_candiate_char_t; + // first element: absolute reception timestamp [s] of the message's first bit + typedef std::pair> msg_candiate_int_t; + typedef std::pair> msg_candiate_char_t; // helper class for sample alignment class Sample_Aligner @@ -123,10 +140,12 @@ private: { public: void reset(); - void get_frame_candidates(const std::vector &bits, std::vector>> &msg_candidates); + // bit_stamps[i] must hold the absolute reception timestamp [s] of bits[i] + void get_frame_candidates(const std::vector &bits, const std::vector &bit_stamps, std::vector>> &msg_candidates); private: std::deque d_buffer; + std::deque d_bit_stamps; //!< absolute reception timestamp [s] of each bit in d_buffer, same indexing } d_frame_detector; diff --git a/src/algorithms/telemetry_decoder/libs/viterbi_decoder_sbas.cc b/src/algorithms/telemetry_decoder/libs/viterbi_decoder_sbas.cc index 02c27e28b..6f42fd4e3 100644 --- a/src/algorithms/telemetry_decoder/libs/viterbi_decoder_sbas.cc +++ b/src/algorithms/telemetry_decoder/libs/viterbi_decoder_sbas.cc @@ -101,6 +101,15 @@ float Viterbi_Decoder_Sbas::decode_continuous(const double sym[], // do add compare select do_acs(sym, nbits_requested); + // Keep accumulating trellis history until a complete traceback is + // available. do_traceback() can clamp the requested depth, but + // do_tb_and_decode() expects that same depth to be present and advances + // its deque iterators by traceback_depth. + if (traceback_depth > static_cast(d_trellis_paths.size())) + { + nbits_decoded = 0; + return d_indicator_metric; + } // the ML sequence in the newest part of the trellis can not be decoded // since it depends on the future values -> traceback, but don't decode const int state = do_traceback(traceback_depth); diff --git a/src/algorithms/tracking/adapters/CMakeLists.txt b/src/algorithms/tracking/adapters/CMakeLists.txt index 97ee94048..2a70f793f 100644 --- a/src/algorithms/tracking/adapters/CMakeLists.txt +++ b/src/algorithms/tracking/adapters/CMakeLists.txt @@ -32,6 +32,7 @@ set(TRACKING_ADAPTER_SOURCES gps_l1_ca_tcp_connector_tracking.cc gps_l1_ca_gaussian_tracking.cc gps_l1_ca_kf_tracking.cc + sbas_l1_dll_pll_tracking.cc ${OPT_TRACKING_ADAPTERS_SOURCES} ) @@ -41,6 +42,7 @@ set(TRACKING_ADAPTER_HEADERS gps_l1_ca_tcp_connector_tracking.h gps_l1_ca_gaussian_tracking.h gps_l1_ca_kf_tracking.h + sbas_l1_dll_pll_tracking.h ${OPT_TRACKING_ADAPTERS_HEADERS} ) diff --git a/src/algorithms/tracking/adapters/dll_pll_tracking_adapter.cc b/src/algorithms/tracking/adapters/dll_pll_tracking_adapter.cc index a567886a4..edc040493 100644 --- a/src/algorithms/tracking/adapters/dll_pll_tracking_adapter.cc +++ b/src/algorithms/tracking/adapters/dll_pll_tracking_adapter.cc @@ -26,6 +26,7 @@ #include "Galileo_E5a.h" #include "Galileo_E5b.h" #include "Galileo_E6.h" +#include "SBAS_L1.h" #include "configuration_interface.h" #include "display.h" #include "qzss.h" @@ -84,6 +85,8 @@ signal_info get_signal_info(signal_flag sig_flag) return {'J', {'J', '1', '\0'}, "QZSS L1 C/A", QZSS_L1_CHIP_RATE, QZSS_L1_CODE_LENGTH, 20, false, true}; case QZS_J5: return {'J', {'J', '5', '\0'}, "QZSS L5", QZSS_L5_CHIP_RATE, QZSS_L5_CODE_LENGTH, QZSS_L5I_NH_CODE_LENGTH, true, false}; + case SBAS_S1: + return {'S', {'S', '1', '\0'}, "SBAS L1", SBAS_L1_CODE_RATE_CPS, SBAS_L1_CODE_LENGTH_CHIPS, SBAS_L1_MAX_COHERENT_INTEGRATION_SYMBOLS, false, true}; default: break; } diff --git a/src/algorithms/tracking/adapters/sbas_l1_dll_pll_tracking.cc b/src/algorithms/tracking/adapters/sbas_l1_dll_pll_tracking.cc new file mode 100644 index 000000000..30eb25eee --- /dev/null +++ b/src/algorithms/tracking/adapters/sbas_l1_dll_pll_tracking.cc @@ -0,0 +1,35 @@ +/*! + * \file sbas_l1_dll_pll_tracking.cc + * \brief Interface of an adapter of a DLL+PLL tracking loop block + * for SBAS L1 signals to a TrackingInterface + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com + * + * Code DLL + carrier PLL according to the algorithms described in: + * K.Borre, D.M.Akos, N.Bertelsen, P.Rinder, and S.H.Jensen, + * A Software-Defined GPS and Galileo Receiver. A Single-Frequency + * Approach, Birkhauser, 2007 + * + * ----------------------------------------------------------------------------- + * + * 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 + * + * ----------------------------------------------------------------------------- + */ + + +#include "sbas_l1_dll_pll_tracking.h" + + +SbasL1DllPllTracking::SbasL1DllPllTracking( + const ConfigurationInterface* configuration, + const std::string& role, + unsigned int in_streams, + unsigned int out_streams) + : DllPllTrackingAdapter(configuration, role, "SBAS_L1_DLL_PLL_Tracking", in_streams, out_streams, SBAS_S1) +{ +} diff --git a/src/algorithms/tracking/adapters/sbas_l1_dll_pll_tracking.h b/src/algorithms/tracking/adapters/sbas_l1_dll_pll_tracking.h new file mode 100644 index 000000000..2aea4f40d --- /dev/null +++ b/src/algorithms/tracking/adapters/sbas_l1_dll_pll_tracking.h @@ -0,0 +1,62 @@ +/*! + * \file sbas_l1_dll_pll_tracking.h + * \brief Interface of an adapter of a DLL+PLL tracking loop block + * for SBAS L1 signals to a TrackingInterface + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com + * + * Code DLL + carrier PLL according to the algorithms described in: + * K.Borre, D.M.Akos, N.Bertelsen, P.Rinder, and S.H.Jensen, + * A Software-Defined GPS and Galileo Receiver. A Single-Frequency + * Approach, Birkhauser, 2007 + * + * SBAS-specific note: the encoded channel symbol period is 2 ms (rate-1/2 + * FEC at 500 sps), so coherent integration is limited to 2 code periods. + * + * ----------------------------------------------------------------------------- + * + * 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_SBAS_L1_DLL_PLL_TRACKING_H +#define GNSS_SDR_SBAS_L1_DLL_PLL_TRACKING_H + +#include "dll_pll_tracking_adapter.h" +#include + +class ConfigurationInterface; + +/** \addtogroup Tracking + * Classes for GNSS signal tracking. + * \{ */ +/** \addtogroup Tracking_adapters tracking_adapters + * Wrap GNU Radio blocks for GNSS signal tracking with a TrackingInterface + * \{ */ + + +/*! + * \brief This class implements a code DLL + carrier PLL tracking loop + * block adapter for SBAS L1 signals (PRN 120–138). + */ +class SbasL1DllPllTracking : public DllPllTrackingAdapter +{ +public: + SbasL1DllPllTracking(const ConfigurationInterface* configuration, + const std::string& role, + unsigned int in_streams, + unsigned int out_streams); + + ~SbasL1DllPllTracking() override = default; +}; + +/** \} */ +/** \} */ + +#endif // GNSS_SDR_SBAS_L1_DLL_PLL_TRACKING_H diff --git a/src/algorithms/tracking/gnuradio_blocks/dll_pll_veml_tracking.cc b/src/algorithms/tracking/gnuradio_blocks/dll_pll_veml_tracking.cc index cef8950b2..cb4cb32e7 100644 --- a/src/algorithms/tracking/gnuradio_blocks/dll_pll_veml_tracking.cc +++ b/src/algorithms/tracking/gnuradio_blocks/dll_pll_veml_tracking.cc @@ -35,6 +35,7 @@ #include "Galileo_E5b.h" #include "Galileo_E6.h" #include "MATH_CONSTANTS.h" +#include "SBAS_L1.h" #include "beidou_b1c_signal_replica.h" #include "beidou_b1i_signal_replica.h" #include "beidou_b3i_signal_replica.h" @@ -54,6 +55,7 @@ #include "matlab_writter_helper.h" #include "qzss.h" #include "qzss_signal_replica.h" +#include "sbas_signal_replica.h" #include "tracking_discriminators.h" #include // for io_signature #include // for scoped_lock @@ -616,7 +618,46 @@ dll_pll_veml_tracking::dll_pll_veml_tracking(const Dll_Pll_Conf &conf_) d_code_samples_per_chip = 0U; d_symbols_per_bit = 0; } - } + } // end QZSS + else if (d_trk_parameters.system == 'S') + { + d_systemName = "SBAS"; + if (d_signal_type == "S1") + { + d_signal_carrier_freq = SBAS_L1_FREQ_HZ; + d_code_period = SBAS_L1_CODE_PERIOD_S; + d_code_chip_rate = SBAS_L1_CODE_RATE_CPS; + d_correlation_length_ms = 1; + d_code_samples_per_chip = 1; + d_code_length_chips = static_cast(SBAS_L1_CODE_LENGTH_CHIPS); + // SBAS has no pilot and no secondary code. + // FEC (rate-1/2, K=7) and bit-sync are handled by sbas_l1_telemetry_decoder_gs. + d_secondary = false; + d_trk_parameters.track_pilot = false; + d_trk_parameters.slope = 1.0; + d_trk_parameters.spc = d_trk_parameters.early_late_space_chips; + d_trk_parameters.y_intercept = 1.0; + // SBAS telemetry decoder handles preamble detection and Viterbi FEC. + // Set d_secondary_code_length to a non-zero value for buffer capacity. + // d_symbols_per_bit=1: each 1 ms correlation is forwarded directly; + // disables the GPS-style histogram bit synchronizer (wrong for SBAS). + d_secondary_code_length = 20U; // placeholder (not used with d_secondary=false) + d_secondary_code_string = GPS_CA_PREAMBLE_SYMBOLS_STR; // placeholder + d_symbols_per_bit = 1; + } + else + { + LOG(WARNING) << "Invalid Signal argument when instantiating tracking blocks"; + std::cerr << "Invalid Signal argument when instantiating tracking blocks\n"; + d_correlation_length_ms = 1; + d_secondary = false; + d_signal_carrier_freq = 0.0; + d_code_period = 0.0; + d_code_length_chips = 0; + d_code_samples_per_chip = 0U; + d_symbols_per_bit = 0; + } + } // end SBAS else { LOG(WARNING) << "Invalid System argument when instantiating tracking blocks"; @@ -1108,6 +1149,10 @@ void dll_pll_veml_tracking::start_tracking() qzss_l5i_code_gen_float(d_tracking_code, d_acquisition_gnss_synchro->PRN); } } + else if (d_systemName == "SBAS" && d_signal_type == "S1") + { + sbas_l1_code_gen_float(d_tracking_code, d_acquisition_gnss_synchro->PRN); + } d_multicorrelator_cpu.set_local_code_and_taps(d_code_samples_per_chip * d_code_length_chips, d_tracking_code.data(), d_local_code_shift_chips.data()); std::fill_n(d_correlator_outs.begin(), d_n_correlator_taps, gr_complex(0.0, 0.0)); diff --git a/src/core/receiver/gnss_block_factory.cc b/src/core/receiver/gnss_block_factory.cc index 7e8941d5c..74c3f2516 100644 --- a/src/core/receiver/gnss_block_factory.cc +++ b/src/core/receiver/gnss_block_factory.cc @@ -74,6 +74,8 @@ #include "pulse_blanking_filter.h" #include "rtklib_pvt.h" #include "rtl_tcp_signal_source.h" +#include "sbas_l1_dll_pll_tracking.h" +#include "sbas_l1_pcps_acquisition.h" #include "sbas_l1_telemetry_decoder_gs.h" #include "signal_conditioner.h" #include "signal_flag.h" @@ -215,6 +217,7 @@ const auto signal_mapping = std::vector>{ {"7X", "GALILEO E5b I (I/NAV OS)"}, {"J1", "QZSS L1 C/A"}, {"J5", "QZSS L5"}, + {"S1", "SBAS L1"}, }; unsigned int get_channel_count(const ConfigurationInterface* configuration) @@ -510,6 +513,10 @@ std::unique_ptr get_acq_block( { return std::make_unique(configuration, role, implementation, in_streams, out_streams, QZS_J5); } + else if (implementation == "SBAS_L1_PCPS_Acquisition") + { + return std::make_unique(configuration, role, in_streams, out_streams); + } #if OPENCL_BLOCKS else if (implementation == "GPS_L1_CA_PCPS_OpenCl_Acquisition") { @@ -626,6 +633,10 @@ std::unique_ptr get_trk_block( { return std::make_unique(configuration, role, implementation, in_streams, out_streams, QZS_J5); } + else if (implementation == "SBAS_L1_DLL_PLL_Tracking") + { + return std::make_unique(configuration, role, in_streams, out_streams); + } #if CUDA_GPU_ACCEL else if (implementation == "GPS_L1_CA_DLL_PLL_Tracking_GPU") { @@ -709,7 +720,9 @@ std::unique_ptr get_tlm_block( } else if (implementation == "SBAS_L1_Telemetry_Decoder") { - telemetry = sbas_l1_make_telemetry_decoder_gs(configuration != nullptr ? configuration->property(role + ".dump", false) : false); + telemetry = sbas_l1_make_telemetry_decoder_gs( + configuration != nullptr ? configuration->property(role + ".dump", false) : false, + configuration != nullptr ? configuration->property(role + ".dump_filename", std::string()) : std::string()); } else if (implementation == "Galileo_E5a_Telemetry_Decoder") { diff --git a/src/core/receiver/gnss_flowgraph.cc b/src/core/receiver/gnss_flowgraph.cc index 47d630c4c..556079fce 100644 --- a/src/core/receiver/gnss_flowgraph.cc +++ b/src/core/receiver/gnss_flowgraph.cc @@ -98,6 +98,7 @@ const auto signal_mapping = std::unordered_map> available_prn_map = { {"GPS", {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}}, - {"SBAS", {123, 131, 135, 136, 138}}, + {"SBAS", {120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138}}, {"Galileo", {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36}}, {"Glonass", {1, 2, 3, 4, 9, 10, 11, 12, 18, 19, 20, 21, 26, 27, 28}}, diff --git a/src/core/system_parameters/SBAS_L1.h b/src/core/system_parameters/SBAS_L1.h new file mode 100644 index 000000000..dcdaf632f --- /dev/null +++ b/src/core/system_parameters/SBAS_L1.h @@ -0,0 +1,70 @@ +/*! + * \file SBAS_L1.h + * \brief Defines system parameters for SBAS L1 signal + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.es + * \author Víctor Castillo Agüero, 2026. victorcastilloaguero(at)gmail.com + * + * Reference: RTCA DO-229E, Minimum Operational Performance Standards for + * Global Positioning System/Wide Area Augmentation System Airborne Equipment + * + * ----------------------------------------------------------------------------- + * + * 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_SBAS_L1_H +#define GNSS_SDR_SBAS_L1_H + +#include "gnss_frequencies.h" +#include + +/** \addtogroup Core + * \{ */ +/** \addtogroup System_Parameters + * \{ */ + +// Carrier and code frequencies +constexpr double SBAS_L1_FREQ_HZ = FREQ1; //!< SBAS L1 carrier frequency [Hz] (same as GPS L1) +constexpr double SBAS_L1_CODE_RATE_CPS = 1.023e6; //!< SBAS L1 C/A code chip rate [chips/s] +constexpr double SBAS_L1_CODE_LENGTH_CHIPS = 1023.0; //!< SBAS L1 C/A code length [chips] +constexpr double SBAS_L1_CODE_PERIOD_S = 0.001; //!< SBAS L1 C/A code period [s] +constexpr uint32_t SBAS_L1_CODE_PERIOD_MS = 1U; //!< SBAS L1 C/A code period [ms] + +// Optimum acquisition sampling frequency +constexpr uint32_t SBAS_L1_OPT_ACQ_FS_SPS = 2000000U; //!< Sampling frequency that maximises acquisition SNR [samples/s] + +// PRN range (RTCA DO-229, Table A-1) +constexpr uint32_t SBAS_L1_PRN_MIN = 120U; //!< Minimum SBAS PRN (inclusive) +constexpr uint32_t SBAS_L1_PRN_MAX = 138U; //!< Maximum SBAS PRN (inclusive) + +// Message rates and encoding +// SBAS broadcasts at 250 raw data bits/s, convolutionally encoded +// at rate 1/2, K=7, yielding a channel symbol rate of 500 symbols/s. +constexpr int32_t SBAS_L1_DATA_RATE_BPS = 250; //!< Raw data bit rate [bits/s] +constexpr int32_t SBAS_L1_CHANNEL_SYMBOL_RATE_SPS = 500; //!< Encoded channel symbol rate (rate-1/2 FEC) [symbols/s] +constexpr int32_t SBAS_L1_CHANNEL_SYMBOL_PERIOD_MS = 2; //!< Encoded channel symbol period [ms] (1/500 sps) +constexpr int32_t SBAS_L1_MSG_LENGTH_BITS = 250; //!< Information bits per 1-second message frame [bits] + +// Maximum coherent integration extension in terms of 1 ms code periods. +// Kept at 1 (no extension): the tracking correlator dumps are not +// synchronized to the encoded symbol boundary, so extending beyond a single +// code period would straddle that boundary and corrupt the sample pairing +// that sbas_l1_telemetry_decoder_gs performs on its own. +constexpr int32_t SBAS_L1_MAX_COHERENT_INTEGRATION_SYMBOLS = 1; + +// Preamble bytes (RTCA DO-229, Section A.4.4.2) +constexpr uint8_t SBAS_L1_PREAMBLE_1 = 0x53U; //!< First preamble byte (binary: 0101 0011) +constexpr uint8_t SBAS_L1_PREAMBLE_2 = 0x9AU; //!< Second preamble byte (binary: 1001 1010) +constexpr uint8_t SBAS_L1_PREAMBLE_3 = 0xC6U; //!< Third preamble byte (binary: 1100 0110) + +/** \} */ +/** \} */ + +#endif // GNSS_SDR_SBAS_L1_H diff --git a/src/core/system_parameters/gnss_satellite.cc b/src/core/system_parameters/gnss_satellite.cc index 10e8b7559..5894c65f5 100644 --- a/src/core/system_parameters/gnss_satellite.cc +++ b/src/core/system_parameters/gnss_satellite.cc @@ -16,6 +16,7 @@ #include "gnss_satellite.h" #include "GLONASS_L1_L2_CA.h" +#include "SBAS_L1.h" #include #include @@ -268,20 +269,19 @@ void Gnss_Satellite::set_PRN(uint32_t PRN_) } else if (system == "SBAS") { - if ((PRN_ == 120) // EGNOS Test Platform.Inmarsat 3-F2 (Atlantic Ocean Region-East) - || (PRN_ == 123) // EGNOS Operational Platform. Astra 5B - || (PRN_ == 131) // WAAS Eutelsat 117 West B - || (PRN_ == 135) // WAAS Galaxy 15 - || (PRN_ == 136) // EGNOS Operational Platform. SES-5 (a.k.a. Sirius 5 or Astra 4B) - || (PRN_ == 138)) // WAAS Anik F1R - { - PRN = PRN_; - } - else + // SBAS PRN assignments (RTCA DO-229, Table A-1) are reassigned between + // satellites over time, so no specific PRN-to-satellite mapping is + // hardcoded here. Any PRN within the supported range is accepted; + // see SBAS_L1_PRN_MIN/SBAS_L1_PRN_MAX for the currently supported range. + if (PRN_ < SBAS_L1_PRN_MIN || PRN_ > SBAS_L1_PRN_MAX) { DLOG(INFO) << "This PRN is not defined"; PRN = 0; } + else + { + PRN = PRN_; + } } else if (system == "Galileo") { @@ -493,29 +493,10 @@ std::string Gnss_Satellite::what_block(const std::string& system_, uint32_t PRN_ } if (system_ == "SBAS") { - switch (PRN_) - { - case 120: - block_ = std::string("EGNOS Test Platform"); // Inmarsat 3-F2 (Atlantic Ocean Region-East) - break; - case 123: - block_ = std::string("EGNOS"); // EGNOS Operational Platform. Astra 5B - break; - case 131: // NOLINT(bugprone-branch-clone) - block_ = std::string("WAAS"); // WAAS Eutelsat 117 West B - break; - case 135: - block_ = std::string("WAAS"); // WAAS Galaxy 15 - break; - case 136: - block_ = std::string("EGNOS"); // EGNOS Operational Platform. SES-5 (a.k.a. Sirius 5 or Astra 4B) - break; - case 138: - block_ = std::string("WAAS"); // WAAS Anik F1R - break; - default: - block_ = std::string("Unknown"); - } + // Which augmentation service (EGNOS, WAAS, GAGAN, ...) owns a given PRN + // changes over time as satellites are reassigned, so no per-PRN service + // name is hardcoded here to avoid publishing stale information. + block_ = std::string("SBAS"); } if (system_ == "Galileo") { diff --git a/src/core/system_parameters/signal_flag.h b/src/core/system_parameters/signal_flag.h index ce06fde21..adaa4c287 100644 --- a/src/core/system_parameters/signal_flag.h +++ b/src/core/system_parameters/signal_flag.h @@ -36,7 +36,8 @@ enum signal_flag : uint32_t BDS_B3 = 0x1 << 10, BDS_B1C = 0x1 << 13, QZS_J1 = 0x1 << 11, - QZS_J5 = 0x1 << 12 + QZS_J5 = 0x1 << 12, + SBAS_S1 = 0x1 << 13 }; #endif // GNSS_SDR_SIGNAL_FLAG_H diff --git a/tests/test_main.cc b/tests/test_main.cc index 65ffd384d..bb59407c9 100644 --- a/tests/test_main.cc +++ b/tests/test_main.cc @@ -176,6 +176,7 @@ private: #include "unit-tests/signal-processing-blocks/sources/gnss_sdr_valve_test.cc" #include "unit-tests/signal-processing-blocks/sources/unpack_2bit_samples_test.cc" #include "unit-tests/signal-processing-blocks/telemetry_decoder/galileo_fnav_inav_decoder_test.cc" +#include "unit-tests/signal-processing-blocks/telemetry_decoder/sbas_l1_telemetry_decoder_test.cc" #include "unit-tests/signal-processing-blocks/tracking/bit_synchronizer_test.cc" #include "unit-tests/signal-processing-blocks/tracking/cpu_multicorrelator_real_codes_test.cc" #include "unit-tests/signal-processing-blocks/tracking/cpu_multicorrelator_test.cc" diff --git a/tests/unit-tests/arithmetic/code_generation_test.cc b/tests/unit-tests/arithmetic/code_generation_test.cc index ec656b84c..228e8f406 100644 --- a/tests/unit-tests/arithmetic/code_generation_test.cc +++ b/tests/unit-tests/arithmetic/code_generation_test.cc @@ -17,9 +17,11 @@ */ #include "Galileo_E6.h" +#include "SBAS_L1.h" #include "galileo_e6_signal_replica.h" #include "gnss_signal_replica.h" #include "gps_sdr_signal_replica.h" +#include "sbas_signal_replica.h" #include #include #include @@ -397,3 +399,90 @@ TEST(CodeGenerationTest, GalileoE6BTest) ASSERT_FLOAT_EQ(gale6b_code[i].real(), expected_output[i].real()); } } + + +// ===== SBAS L1 signal replica tests ===== + +TEST(CodeGenerationTest, SbasL1ComplexCodePrn120Test) +{ + // PRN 120 is the lowest SBAS PRN; verify code generates without error + // and has correct length. Values are ±1 (BPSK spreading). + std::array, 1023> dest{}; + const uint32_t prn = SBAS_L1_PRN_MIN; // 120 + sbas_l1_code_gen_complex(dest, prn, 0U); + + for (const auto& chip : dest) + { + ASSERT_FLOAT_EQ(std::abs(chip.imag()), 0.0F); + ASSERT_TRUE(chip.real() == 1.0F || chip.real() == -1.0F) + << "Chip value out of range for PRN " << prn; + } +} + + +TEST(CodeGenerationTest, SbasL1ComplexCodePrn138Test) +{ + // PRN 138 is the highest SBAS PRN; verify code generates without error. + std::array, 1023> dest{}; + const uint32_t prn = SBAS_L1_PRN_MAX; // 138 + sbas_l1_code_gen_complex(dest, prn, 0U); + + for (const auto& chip : dest) + { + ASSERT_FLOAT_EQ(std::abs(chip.imag()), 0.0F); + ASSERT_TRUE(chip.real() == 1.0F || chip.real() == -1.0F) + << "Chip value out of range for PRN " << prn; + } +} + + +TEST(CodeGenerationTest, SbasL1FloatCodePrn121Test) +{ + // PRN 121 — verify float variant generates 1023 chips of ±1. + std::array dest{}; + sbas_l1_code_gen_float(dest, 121U); + + for (const auto& chip : dest) + { + ASSERT_TRUE(chip == 1.0F || chip == -1.0F) + << "Float chip value out of range for PRN 121"; + } +} + + +TEST(CodeGenerationTest, SbasL1SampledCodeAutocorrelationTest) +{ + // Verify that the sampled SBAS L1 code has a sharp autocorrelation peak + // at zero lag (property of maximal-length sequences). + const int32_t fs = static_cast(SBAS_L1_OPT_ACQ_FS_SPS); // 2 Msps + const int32_t code_rate = static_cast(SBAS_L1_CODE_RATE_CPS); + const int32_t code_length = static_cast(SBAS_L1_CODE_LENGTH_CHIPS); + const int samples_per_code = static_cast(std::round( + static_cast(fs) / (static_cast(code_rate) / static_cast(code_length)))); + + std::vector> code(samples_per_code); + sbas_l1_code_gen_complex_sampled(code, 120U, fs); + + // Compute circular autocorrelation at lag 0 + float r0 = 0.0F; + for (const auto& s : code) + { + r0 += s.real() * s.real() + s.imag() * s.imag(); + } + + // Compute autocorrelation at a full-chip lag to measure the sidelobe level. + // At 2 Msps with 1.023 Mcps, each chip spans ~2 samples, so lag=2 takes + // us to an adjacent-chip offset well outside the main correlation peak. + const int chip_lag = static_cast(std::round(static_cast(fs) / static_cast(code_rate))); + float r1 = 0.0F; + for (int i = 0; i < samples_per_code; ++i) + { + const int j = (i + chip_lag) % samples_per_code; + r1 += code[i].real() * code[j].real() + code[i].imag() * code[j].imag(); + } + + // Peak at zero lag should dominate (ratio > 10) + ASSERT_GT(r0, 0.0F); + ASSERT_GT(r0 / (std::abs(r1) + 1e-6F), 10.0F) + << "Autocorrelation peak-to-sidelobe ratio too small for SBAS PRN 120"; +} diff --git a/tests/unit-tests/signal-processing-blocks/telemetry_decoder/sbas_l1_telemetry_decoder_test.cc b/tests/unit-tests/signal-processing-blocks/telemetry_decoder/sbas_l1_telemetry_decoder_test.cc new file mode 100644 index 000000000..35e8cafef --- /dev/null +++ b/tests/unit-tests/signal-processing-blocks/telemetry_decoder/sbas_l1_telemetry_decoder_test.cc @@ -0,0 +1,72 @@ +/*! + * \file sbas_l1_telemetry_decoder_test.cc + * \brief This class implements a deterministic unit test for the message + * timestamp computation of the SBAS L1 telemetry decoder. + * \author Miguel Gómez López, 2026. mgomezl(at)ing.uc3m.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 + * + * ----------------------------------------------------------------------------- + */ + +#include "SBAS_L1.h" +#include "sbas_l1_telemetry_decoder_gs.h" +#include +#include +#include + +TEST(SbasL1TelemetryDecoderTest, TimestampMatchesFirstBitWhenFullyAligned) +{ + // When both sample and symbol alignment already match the chosen + // hypothesis, no residual correction is applied: the message timestamp + // must equal the first bit's own absolute timestamp. + const double first_bit_stamp_s = 123.456; + const double message_stamp_s = sbas_l1_telemetry_decoder_gs::compute_message_timestamp(first_bit_stamp_s, true, true); + EXPECT_DOUBLE_EQ(first_bit_stamp_s, message_stamp_s); +} + +TEST(SbasL1TelemetryDecoderTest, TimestampCorrectionIsBoundedBySymbolPeriod) +{ + // The correction is always a whole number of code periods, and spans at + // most one code period (sample-pairing ambiguity) plus one symbol, i.e. + // two code periods (bit-pairing ambiguity). It only resolves a small + // sub-bit ambiguity, not the block-level offset that used to cause ~1 s + // errors. + const double first_bit_stamp_s = 1000.0; + const int64_t max_correction_code_periods = 3; + + for (const bool sample_aligned : {false, true}) + { + for (const bool symbol_aligned : {false, true}) + { + const double message_stamp_s = sbas_l1_telemetry_decoder_gs::compute_message_timestamp(first_bit_stamp_s, sample_aligned, symbol_aligned); + const int64_t correction_code_periods = std::llround((first_bit_stamp_s - message_stamp_s) / SBAS_L1_CODE_PERIOD_S); + EXPECT_GE(correction_code_periods, 0); + EXPECT_LE(correction_code_periods, max_correction_code_periods); + } + } +} + + +TEST(SbasL1TelemetryDecoderTest, TimestampIsNeverInTheFuture) +{ + // A decoded message can only be timestamped at or before the time its + // first bit was received; the previous implementation could report a + // timestamp roughly 1 s ahead of the true preamble reception time. + const double first_bit_stamp_s = 42.0; + + for (const bool sample_aligned : {false, true}) + { + for (const bool symbol_aligned : {false, true}) + { + const double message_stamp_s = sbas_l1_telemetry_decoder_gs::compute_message_timestamp(first_bit_stamp_s, sample_aligned, symbol_aligned); + EXPECT_LE(message_stamp_s, first_bit_stamp_s); + } + } +}