// Part of BNC, a utility for retrieving decoding and // converting GNSS data streams from NTRIP broadcasters. // // Copyright (C) 2007 // German Federal Agency for Cartography and Geodesy (BKG) // http://bkg.bund.de // Czech Technical University Prague, Department of Geodesy // http://www.fsv.cvut.cz // // Email: euref-ip@bkg.bund.de // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation, version 2. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. /* ------------------------------------------------------------------------- * BKG NTRIP Client * ------------------------------------------------------------------------- * * Class: RTCM3coDecoder * * Purpose: RTCM3 Clock Orbit Decoder * * Author: L. Mervart * * Created: 05-May-2008 * * Changes: * * -----------------------------------------------------------------------*/ #include #include #include #include "RTCM3coDecoder.h" #include "bncutils.h" #include "bncrinex.h" #include "bnccore.h" #include "bncsettings.h" #include "bnctime.h" using namespace std; // Constructor //////////////////////////////////////////////////////////////////////////// RTCM3coDecoder::RTCM3coDecoder(const QString& staID) { _staID = staID; // File Output // ----------- bncSettings settings; QString path = settings.value("corrPath").toString(); if (!path.isEmpty()) { expandEnvVar(path); if ( path.length() > 0 && path[path.length()-1] != QDir::separator() ) { path += QDir::separator(); } _fileNameSkl = path + staID; } _out = 0; Qt::ConnectionType conType = BNC_CORE->decoderConnectionType(); connect(this, SIGNAL(newOrbCorrections(QList)), BNC_CORE, SLOT(slotNewOrbCorrections(QList)), conType); connect(this, SIGNAL(newClkCorrections(QList)), BNC_CORE, SLOT(slotNewClkCorrections(QList)), conType); connect(this, SIGNAL(newCodeBiases(QList)), BNC_CORE, SLOT(slotNewCodeBiases(QList)), conType); connect(this, SIGNAL(newPhaseBiases(QList)), BNC_CORE, SLOT(slotNewPhaseBiases(QList)), conType); connect(this, SIGNAL(newTec(t_vTec)), BNC_CORE, SLOT(slotNewTec(t_vTec)), conType); connect(this, SIGNAL(newMetaData(t_metaData)), BNC_CORE, SLOT(slotNewMetaData(t_metaData)), conType); connect(this, SIGNAL(newSatAntennas(QList)), BNC_CORE, SLOT(slotNewSatAntennas(QList)), conType); connect(this, SIGNAL(providerIDChanged(QString)), BNC_CORE, SIGNAL(providerIDChanged(QString)), conType); connect(this, SIGNAL(newMessage(QByteArray,bool)), BNC_CORE, SLOT(slotMessage(const QByteArray,bool))); reset(); _providerID[0] = -1; _providerID[1] = -1; _providerID[2] = -1; for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) { _antennaReceived[s] = false; _antennaIODKnown[s] = false; _antennaIODProvider[s] = 0; _antennaIODCur[s] = 0; _antennaIODPrev[s] = 0; } for (unsigned i = 0; i < CLOCKORBIT_COUNTSAT; i++) { _antennaSentProviderID[i] = 0; _antennaSentIOD[i] = 0; // meaningless while _antennaSentTime[i] is invalid ("never sent") } _stateSnapshot = new t_stateSnapshot; _ssrCorr = 0; } // Destructor //////////////////////////////////////////////////////////////////////////// RTCM3coDecoder::~RTCM3coDecoder() { delete _out; delete _ssrCorr; delete _stateSnapshot; _IODs.clear(); _orbCorrections.clear(); _clkCorrections.clear(); _lastClkCorrections.clear(); _codeBiases.clear(); _phaseBiases.clear(); _vTecMap.clear(); _metaDataMap.clear(); _satAntennas.clear(); } // //////////////////////////////////////////////////////////////////////////// void RTCM3coDecoder::reset(bool resetPhaseBias) { memset(&_clkOrb, 0, sizeof(_clkOrb)); memset(&_codeBias, 0, sizeof(_codeBias)); if (resetPhaseBias) { memset(&_phaseBias, 0, sizeof(_phaseBias)); for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) { _phaseBiasSentEpoch[s] = UINT_MAX; // "never sent" } } memset(&_vTEC, 0, sizeof(_vTEC)); } // Reopen Output File //////////////////////////////////////////////////////////////////////// void RTCM3coDecoder::reopen() { if (!_fileNameSkl.isEmpty()) { bncSettings settings; QDateTime datTim = currentDateAndTimeGPS(); QString hlpStr = bncRinex::nextEpochStr(datTim, settings.value("corrIntr").toString(), 3); QString cntStr = (_fileNameSkl.contains("ION")) ? "_ION.ssr" : "_MC.ssr"; QString fileNameHlp = _fileNameSkl + "_S_" + // stream QString("%1").arg(datTim.date().year()) + QString("%1").arg(datTim.date().dayOfYear(), 3, 10, QChar('0')) + hlpStr + // HM_period cntStr; // mixed ION or CLK if (_fileName == fileNameHlp) { return; } else { _fileName = fileNameHlp; } delete _out; if ( Qt::CheckState(settings.value("rnxAppend").toInt()) == Qt::Checked) { _out = new ofstream( _fileName.toLatin1().data(), ios_base::out | ios_base::app ); } else { _out = new ofstream( _fileName.toLatin1().data() ); } } } // //////////////////////////////////////////////////////////////////////////// t_irc RTCM3coDecoder::Decode(char* buffer, int bufLen, vector& errmsg) { errmsg.clear(); _buffer.append(QByteArray(buffer,bufLen)); t_irc retCode = failure; while(_buffer.size()) { // save state memcpy(&_stateSnapshot->clkOrb, &_clkOrb, sizeof(_clkOrb)); memcpy(&_stateSnapshot->codeBias, &_codeBias, sizeof(_codeBias)); memcpy(&_stateSnapshot->phaseBias, &_phaseBias, sizeof(_phaseBias)); memcpy(&_stateSnapshot->vTEC, &_vTEC, sizeof(_vTEC)); memcpy(&_stateSnapshot->metaData, &_metaData, sizeof(_metaData)); memcpy(&_stateSnapshot->antenna, &_antenna, sizeof(_antenna)); // _antenna persists across messages, so its content alone can't tell // whether a Satellite Antenna message arrived in this frame (an unchanged // retransmission looks identical). Clear messageType[] as a marker: the // COBOFS_SATANT decoder sets it (always non-zero) for each system it // decodes. Restored with the snapshot if the frame turns out incomplete. for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) { _antenna.messageType[s] = 0; } int bytesused = 0; GCOB_RETURN irc = _ssrCorr->GetSSR(&_clkOrb, &_codeBias, &_vTEC, &_phaseBias, &_metaData, &_antenna, _buffer.data(), _buffer.size(), &bytesused); if (irc <= -30) { // not enough data - restore state and exit loop memcpy(&_clkOrb, &_stateSnapshot->clkOrb, sizeof(_clkOrb)); memcpy(&_codeBias, &_stateSnapshot->codeBias, sizeof(_codeBias)); memcpy(&_phaseBias, &_stateSnapshot->phaseBias, sizeof(_phaseBias)); memcpy(&_vTEC, &_stateSnapshot->vTEC, sizeof(_vTEC)); memcpy(&_metaData, &_stateSnapshot->metaData, sizeof(_metaData)); memcpy(&_antenna, &_stateSnapshot->antenna, sizeof(_antenna)); break; } else if (irc < 0) { // error - skip this message (or 1 byte if the // frame itself couldn't be recognized) and retry if (irc != GCOBR_UNKNOWNTYPE) { // A recognized-but-unimplemented message (e.g. Tropospheric or // Regional Ionospheric Correction) leaves _clkOrb/_codeBias/ // _phaseBias/_vTEC untouched, and bytesused is already reliably set // from its CRC-validated frame length, so there is nothing to // discard. Every other error code may have left inconsistent // partial state (e.g. mid-satellite-loop) or an unreliable frame // boundary, so reset defensively there. Without this, streams with // frequent unimplemented messages (e.g. dense grid-based Regional // Ionospheric traffic) would wipe unrelated, still-valid orbit/ // clock/bias/phase-bias state every time one is skipped. reset(); } _buffer = _buffer.mid(bytesused ? bytesused : 1); } else { // OK or MESSAGEFOLLOWS _buffer = _buffer.mid(bytesused); if (irc == GCOBR_OK || irc == GCOBR_MESSAGEFOLLOWS ) { // Latch the reception until sendResults() consumes it - it may not // run for this frame if _lastTime is still invalid. for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) { if (_antenna.messageType[s] != 0) { _antennaReceived[s] = true; checkAntennaIOD(s); } } setEpochTime(); // sets _lastTime if (_lastTime.valid()) { reopen(); checkProviderID(); sendResults(); retCode = success; } else { emit newMessage("RTCM3coDecoder: _lastTime invalid: " + _staID.toLatin1(), true); retCode = failure; } // Keep _phaseBias intact: an Extended Phase Bias message (COBOFS_PBEXT) // arriving later needs to correlate against the satellite/signal list // just published by this Phase Bias message. clock_orbit_rtcm_new.cpp's // COBOFS_PBIAS handling resets pb->NumberOfSat[s] itself before decoding // a fresh message, so this doesn't cause stale data to accumulate. reset(false); } } } return retCode; } // //////////////////////////////////////////////////////////////////////////// void RTCM3coDecoder::sendResults() { // Orbit and clock corrections of all satellites // --------------------------------------------- for (unsigned ii = 0; ii < CLOCKORBIT_NUMGPS + CLOCKORBIT_NUMGLONASS + CLOCKORBIT_NUMGALILEO + CLOCKORBIT_NUMQZSS + CLOCKORBIT_NUMSBAS + _clkOrb.NumberOfSat[CLOCKORBIT_SATBDS]; ii++) { if (corrIsOutOfRange(_clkOrb.Sat[ii])) { continue; } char sys = ' '; int num = _clkOrb.Sat[ii].ID; int flag = 0; // to force NAV type usage according SSR standard if (ii < _clkOrb.NumberOfSat[CLOCKORBIT_SATGPS]) { sys = 'G'; flag = t_eph::LNAV; } else if (ii >= CLOCKORBIT_OFFSETGLONASS && ii < CLOCKORBIT_OFFSETGLONASS + _clkOrb.NumberOfSat[CLOCKORBIT_SATGLONASS]) { sys = 'R'; flag = t_eph::FDMA_M; } else if (ii >= CLOCKORBIT_OFFSETGALILEO && ii < CLOCKORBIT_OFFSETGALILEO + _clkOrb.NumberOfSat[CLOCKORBIT_SATGALILEO]) { sys = 'E'; flag = t_eph::INAV; } else if (ii >= CLOCKORBIT_OFFSETQZSS && ii < CLOCKORBIT_OFFSETQZSS + _clkOrb.NumberOfSat[CLOCKORBIT_SATQZSS]) { sys = 'J'; flag = t_eph::LNAV; } else if (ii >= CLOCKORBIT_OFFSETSBAS && ii < CLOCKORBIT_OFFSETSBAS + _clkOrb.NumberOfSat[CLOCKORBIT_SATSBAS]) { sys = 'S'; flag = t_eph::SBASL1; } else if (ii >= CLOCKORBIT_OFFSETBDS && ii < CLOCKORBIT_OFFSETBDS + _clkOrb.NumberOfSat[CLOCKORBIT_SATBDS]) { sys = 'C'; if (num < 6) {// GEO flag = t_eph::D2; } else if (num > 58 && num < 63) { // GEO flag = t_eph::D2; } else { flag = t_eph::D1; } } else { continue; } // Orbit correction // ---------------- if ( _clkOrb.messageType == _ssrCorr->COTYPE_GPSCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_GLONASSCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_GALILEOCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_QZSSCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_SBASCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_BDSCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_GPSORBIT || _clkOrb.messageType == _ssrCorr->COTYPE_GLONASSORBIT || _clkOrb.messageType == _ssrCorr->COTYPE_GALILEOORBIT || _clkOrb.messageType == _ssrCorr->COTYPE_QZSSORBIT || _clkOrb.messageType == _ssrCorr->COTYPE_SBASORBIT || _clkOrb.messageType == _ssrCorr->COTYPE_BDSORBIT ) { t_orbCorr orbCorr; orbCorr._prn.set(sys, num, flag); checkPrnRange(orbCorr._prn); orbCorr._staID = _staID.toStdString(); orbCorr._iod = _clkOrb.Sat[ii].IOD; orbCorr._time = _lastTime; orbCorr._updateInt = _clkOrb.UpdateInterval; orbCorr._system = sys; orbCorr._xr[0] = _clkOrb.Sat[ii].Orbit.DeltaRadial; orbCorr._xr[1] = _clkOrb.Sat[ii].Orbit.DeltaAlongTrack; orbCorr._xr[2] = _clkOrb.Sat[ii].Orbit.DeltaCrossTrack; orbCorr._dotXr[0] = _clkOrb.Sat[ii].Orbit.DotDeltaRadial; orbCorr._dotXr[1] = _clkOrb.Sat[ii].Orbit.DotDeltaAlongTrack; orbCorr._dotXr[2] = _clkOrb.Sat[ii].Orbit.DotDeltaCrossTrack; orbCorr._ssrFormat = (_type == RTCMssr) ? ssrRtcmOld : (_type == RTCMnewssr) ? ssrRtcmNew : ssrUnknown; _orbCorrections[_lastTime].append(orbCorr); _IODs[orbCorr._prn] = _clkOrb.Sat[ii].IOD; } // Clock Corrections // ----------------- if ( _clkOrb.messageType == _ssrCorr->COTYPE_GPSCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_GLONASSCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_GALILEOCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_QZSSCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_SBASCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_BDSCOMBINED || _clkOrb.messageType == _ssrCorr->COTYPE_GPSCLOCK || _clkOrb.messageType == _ssrCorr->COTYPE_GLONASSCLOCK || _clkOrb.messageType == _ssrCorr->COTYPE_GALILEOCLOCK || _clkOrb.messageType == _ssrCorr->COTYPE_QZSSCLOCK || _clkOrb.messageType == _ssrCorr->COTYPE_SBASCLOCK || _clkOrb.messageType == _ssrCorr->COTYPE_BDSCLOCK) { t_clkCorr clkCorr; clkCorr._prn.set(sys, _clkOrb.Sat[ii].ID, flag); checkPrnRange(clkCorr._prn); clkCorr._staID = _staID.toStdString(); clkCorr._time = _lastTime; clkCorr._updateInt = _clkOrb.UpdateInterval; clkCorr._dClk = _clkOrb.Sat[ii].Clock.DeltaA0 / t_CST::c; clkCorr._dotDClk = _clkOrb.Sat[ii].Clock.DeltaA1 / t_CST::c; clkCorr._dotDotDClk = _clkOrb.Sat[ii].Clock.DeltaA2 / t_CST::c; _lastClkCorrections[clkCorr._prn] = clkCorr; if (_IODs.contains(clkCorr._prn)) { clkCorr._iod = _IODs[clkCorr._prn]; _clkCorrections[_lastTime].append(clkCorr); } } // High-Resolution Clocks // ---------------------- if ( _clkOrb.messageType == _ssrCorr->COTYPE_GPSHR || _clkOrb.messageType == _ssrCorr->COTYPE_GLONASSHR || _clkOrb.messageType == _ssrCorr->COTYPE_GALILEOHR || _clkOrb.messageType == _ssrCorr->COTYPE_QZSSHR || _clkOrb.messageType == _ssrCorr->COTYPE_SBASHR || _clkOrb.messageType == _ssrCorr->COTYPE_BDSHR) { t_prn prn(sys, _clkOrb.Sat[ii].ID, flag); if (_lastClkCorrections.contains(prn)) { t_clkCorr clkCorr; clkCorr = _lastClkCorrections[prn]; clkCorr._time = _lastTime; clkCorr._updateInt = _clkOrb.UpdateInterval; clkCorr._dClk += _clkOrb.Sat[ii].hrclock / t_CST::c; if (_IODs.contains(clkCorr._prn)) { clkCorr._iod = _IODs[clkCorr._prn]; _clkCorrections[_lastTime].append(clkCorr); } } } } // Code Biases // ----------- for (unsigned ii = 0; ii < CLOCKORBIT_NUMGPS + CLOCKORBIT_NUMGLONASS + CLOCKORBIT_NUMGALILEO + CLOCKORBIT_NUMQZSS + CLOCKORBIT_NUMSBAS + _codeBias.NumberOfSat[CLOCKORBIT_SATBDS]; ii++) { char sys = ' '; int num = _codeBias.Sat[ii].ID; int flag = 0; if (ii < _codeBias.NumberOfSat[CLOCKORBIT_SATGPS]) { sys = 'G'; flag = t_eph::LNAV; } else if (ii >= CLOCKORBIT_OFFSETGLONASS && ii < CLOCKORBIT_OFFSETGLONASS + _codeBias.NumberOfSat[CLOCKORBIT_SATGLONASS]) { sys = 'R'; flag = t_eph::FDMA_M; } else if (ii >= CLOCKORBIT_OFFSETGALILEO && ii < CLOCKORBIT_OFFSETGALILEO + _codeBias.NumberOfSat[CLOCKORBIT_SATGALILEO]) { sys = 'E'; flag = t_eph::INAV; } else if (ii >= CLOCKORBIT_OFFSETQZSS && ii < CLOCKORBIT_OFFSETQZSS + _codeBias.NumberOfSat[CLOCKORBIT_SATQZSS]) { sys = 'J'; flag = t_eph::LNAV; } else if (ii >= CLOCKORBIT_OFFSETSBAS && ii < CLOCKORBIT_OFFSETSBAS + _codeBias.NumberOfSat[CLOCKORBIT_SATSBAS]) { sys = 'S'; flag = t_eph::SBASL1; } else if (ii >= CLOCKORBIT_OFFSETBDS && ii < CLOCKORBIT_OFFSETBDS + _codeBias.NumberOfSat[CLOCKORBIT_SATBDS]) { sys = 'C'; if (num < 6) {// GEO flag = t_eph::D2; } else if (num > 58 && num < 63) { // GEO flag = t_eph::D2; } else { flag = t_eph::D1; } } else { continue; } t_satCodeBias satCodeBias; satCodeBias._prn.set(sys, num, flag); checkPrnRange(satCodeBias._prn); satCodeBias._staID = _staID.toStdString(); satCodeBias._time = _lastTime; satCodeBias._updateInt = _codeBias.UpdateInterval; satCodeBias._ssrIOD = _codeBias.SSRIOD; satCodeBias._ssrProviderID = _codeBias.SSRProviderID; for (unsigned jj = 0; jj < _codeBias.Sat[ii].NumberOfCodeBiases; jj++) { const SsrCorr::CodeBias::BiasSat::CodeBiasEntry& biasEntry = _codeBias.Sat[ii].Biases[jj]; t_frqCodeBias frqCodeBias; frqCodeBias._rnxType2ch.assign(_ssrCorr->codeTypeToRnxType(sys, biasEntry.Type)); frqCodeBias._value = biasEntry.Bias; if (!frqCodeBias._rnxType2ch.empty()) { satCodeBias._bias.push_back(frqCodeBias); } } _codeBiases[_lastTime].append(satCodeBias); } // Phase Biases // ----------- // _phaseBias is no longer wiped between messages (kept alive so a later // Extended Phase Bias message can correlate against it - see // COBOFS_PBEXT), so without this guard the block below would re-append // the same system's satellites into the output on every other message // decoded in between two actual phase-bias messages. Only emit a // system's satellites once per epoch, tracked per system by comparing // against the epoch we last actually sent for it. bool phaseBiasFresh[CLOCKORBIT_SATNUM]; for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) { phaseBiasFresh[s] = _phaseBias.NumberOfSat[s] > 0 && _phaseBias.EpochTime[s] != _phaseBiasSentEpoch[s]; if (phaseBiasFresh[s]) { _phaseBiasSentEpoch[s] = _phaseBias.EpochTime[s]; } } for (unsigned ii = 0; ii < CLOCKORBIT_NUMGPS + CLOCKORBIT_NUMGLONASS + CLOCKORBIT_NUMGALILEO + CLOCKORBIT_NUMQZSS + CLOCKORBIT_NUMSBAS + _phaseBias.NumberOfSat[CLOCKORBIT_SATBDS]; ii++) { char sys = ' '; int num = _phaseBias.Sat[ii].ID; int flag = 0; if (ii < _phaseBias.NumberOfSat[CLOCKORBIT_SATGPS]) { if (!phaseBiasFresh[CLOCKORBIT_SATGPS]) continue; sys = 'G'; flag = t_eph::LNAV; } else if (ii >= CLOCKORBIT_OFFSETGLONASS && ii < CLOCKORBIT_OFFSETGLONASS + _phaseBias.NumberOfSat[CLOCKORBIT_SATGLONASS]) { if (!phaseBiasFresh[CLOCKORBIT_SATGLONASS]) continue; sys = 'R'; flag = t_eph::FDMA_M; } else if (ii >= CLOCKORBIT_OFFSETGALILEO && ii < CLOCKORBIT_OFFSETGALILEO + _phaseBias.NumberOfSat[CLOCKORBIT_SATGALILEO]) { if (!phaseBiasFresh[CLOCKORBIT_SATGALILEO]) continue; sys = 'E'; flag = t_eph::INAV; } else if (ii >= CLOCKORBIT_OFFSETQZSS && ii < CLOCKORBIT_OFFSETQZSS + _phaseBias.NumberOfSat[CLOCKORBIT_SATQZSS]) { if (!phaseBiasFresh[CLOCKORBIT_SATQZSS]) continue; sys = 'J'; flag = t_eph::LNAV; } else if (ii >= CLOCKORBIT_OFFSETSBAS && ii < CLOCKORBIT_OFFSETSBAS + _phaseBias.NumberOfSat[CLOCKORBIT_SATSBAS]) { if (!phaseBiasFresh[CLOCKORBIT_SATSBAS]) continue; sys = 'S'; flag = t_eph::SBASL1; } else if (ii >= CLOCKORBIT_OFFSETBDS && ii < CLOCKORBIT_OFFSETBDS + _phaseBias.NumberOfSat[CLOCKORBIT_SATBDS]) { if (!phaseBiasFresh[CLOCKORBIT_SATBDS]) continue; sys = 'C'; if (num < 6) {// GEO flag = t_eph::D2; } else if (num > 58 && num < 63) { // GEO flag = t_eph::D2; } else { flag = t_eph::D1; } } else { continue; } t_satPhaseBias satPhaseBias; satPhaseBias._prn.set(sys, num, flag); checkPrnRange(satPhaseBias._prn); satPhaseBias._staID = _staID.toStdString(); satPhaseBias._time = _lastTime; satPhaseBias._updateInt = _phaseBias.UpdateInterval; satPhaseBias._ssrIOD = _phaseBias.SSRIOD; satPhaseBias._ssrProviderID = _phaseBias.SSRProviderID; satPhaseBias._ssrFormat = (_type == RTCMssr) ? ssrRtcmOld : (_type == RTCMnewssr) ? ssrRtcmNew : ssrUnknown; if (satPhaseBias._ssrFormat == e_ssrFormat::ssrRtcmNew) { satPhaseBias._satYawInfoInd = _phaseBias.SatelliteYawInformationIndicator; satPhaseBias._extPBPhaseInd = _phaseBias.ExtendedPhaseBiasPropertyID; } else { satPhaseBias._dispBiasConsistInd = _phaseBias.DispersiveBiasConsistencyIndicator; satPhaseBias._MelbWuebConsistInd = _phaseBias.MWConsistencyIndicator; } satPhaseBias._yaw = _phaseBias.Sat[ii].YawAngle; satPhaseBias._yawRate = _phaseBias.Sat[ii].YawRate; for (unsigned jj = 0; jj < _phaseBias.Sat[ii].NumberOfPhaseBiases; jj++) { const SsrCorr::PhaseBias::PhaseBiasSat::PhaseBiasEntry& biasEntry = _phaseBias.Sat[ii].Biases[jj]; t_frqPhaseBias frqPhaseBias; frqPhaseBias._rnxType2ch.assign(_ssrCorr->codeTypeToRnxType(sys, biasEntry.Type)); frqPhaseBias._value = biasEntry.Bias; frqPhaseBias._fixIndicator = biasEntry.IntegerIndicator; if (satPhaseBias._ssrFormat == e_ssrFormat::ssrRtcmNew) { if (satPhaseBias._extPBPhaseInd) { frqPhaseBias._fixWideLaneIndicator = biasEntry.WidelaneGroupIndicator; } } else { frqPhaseBias._fixWideLaneIndicator = biasEntry.WidelaneGroupIndicator; } frqPhaseBias._jumpCounter = biasEntry.DiscontinuityCounter; if (!frqPhaseBias._rnxType2ch.empty()) { satPhaseBias._bias.push_back(frqPhaseBias); } } _phaseBiases[_lastTime].append(satPhaseBias); } // Ionospheric Model // ----------------- if (_vTEC.NumLayers > 0) { _vTecMap[_lastTime]._time = _lastTime; _vTecMap[_lastTime]._updateInt = _vTEC.UpdateInterval; _vTecMap[_lastTime]._staID = _staID.toStdString(); for (unsigned ii = 0; ii < _vTEC.NumLayers; ii++) { const SsrCorr::VTEC::IonoLayers& ionoLayer = _vTEC.Layers[ii]; t_vTecLayer layer; layer._height = ionoLayer.Height; layer._C.ReSize(ionoLayer.Degree+1, ionoLayer.Order+1); layer._S.ReSize(ionoLayer.Degree+1, ionoLayer.Order+1); for (unsigned iDeg = 0; iDeg <= ionoLayer.Degree; iDeg++) { for (unsigned iOrd = 0; iOrd <= ionoLayer.Order; iOrd++) { layer._C[iDeg][iOrd] = ionoLayer.Cosinus[iDeg][iOrd]; layer._S[iDeg][iOrd] = ionoLayer.Sinus[iDeg][iOrd]; } } _vTecMap[_lastTime]._layers.push_back(layer); } } // Metadata (model correction information) // ---------------------------------------- if (_metaData.NumEntries > 0) { // Metadata carries no epoch field of its own (it is keyed here by // _lastTime, inherited from whichever other message last set it), so a // fresh, complete message must replace rather than accumulate onto // whatever a previous message at the same _lastTime already stored. _metaDataMap[_lastTime] = t_metaData(); _metaDataMap[_lastTime]._time = _lastTime; _metaDataMap[_lastTime]._staID = _staID.toStdString(); _metaDataMap[_lastTime]._ssrIOD = _metaData.SSRIOD; _metaDataMap[_lastTime]._providerID = _metaData.SSRProviderID; _metaDataMap[_lastTime]._solutionID = _metaData.SSRSolutionID; for (unsigned ii = 0; ii < _metaData.NumEntries; ii++) { const SsrCorr::MetaData::ModelPart& part = _metaData.Entries[ii]; t_metaDataEntry entry; entry._typeIndicator = part.TypeIndicator; entry._applicationIndicator = part.ApplicationIndicator; entry._nonDefaultIndicator = part.nonDefaultIndicator; entry._nonDefaultIdentifier = part.nonDefaultIdentifier; entry._dataIODIndicator = part.DataIODIndicator; entry._dataIOD = part.DataIOD; _metaDataMap[_lastTime]._entries.push_back(entry); } } // Satellite Antenna corrections // ------------------------------ // _antenna, like _metaData, carries no epoch field of its own and is never // wiped by reset(), so each system's data persists across unrelated // messages. Only a system for which an Antenna message was actually // received (_antennaReceived, latched in Decode()) is considered, so // persisted data is never re-stamped with a newer time. // // The SatelliteAntennaIOD is unique only per SSR Provider ID (DF414) and // only within 64 days, and a client that has not received the stream for // 64 days must discard cached antenna data (enforced downstream against // t_satAntenna::_time, see ssrSatAntennaTrusted() in pppSatObs.cpp). So an // unchanged (provider ID, IOD) retransmission is suppressed only if it was // last emitted less than ANT_REFRESH_SEC ago. Re-emitting it after that // keeps _time close to the last reception - not the first - so continuously // received data never ages out. It also means a reception after a long // gap is always emitted, even when the provider has legitimately reused // the IOD for different content, and each .ssr file contains the antenna // data at least once per refresh interval. // // This is tracked per satellite, not per system: providers may split a // system's satellites across several messages with rotating satellite // masks (DF394), all carrying the same IOD - a per-system check would // emit only whichever subset happened to arrive first. { const double ANT_REFRESH_SEC = 3600.0; struct SysInfo { unsigned sysIdx; char sysChar; unsigned numSat; unsigned offset; }; const SysInfo sysInfos[] = { { CLOCKORBIT_SATGPS, 'G', CLOCKORBIT_NUMGPS, CLOCKORBIT_OFFSETGPS }, { CLOCKORBIT_SATGLONASS, 'R', CLOCKORBIT_NUMGLONASS, CLOCKORBIT_OFFSETGLONASS }, { CLOCKORBIT_SATGALILEO, 'E', CLOCKORBIT_NUMGALILEO, CLOCKORBIT_OFFSETGALILEO }, { CLOCKORBIT_SATQZSS, 'J', CLOCKORBIT_NUMQZSS, CLOCKORBIT_OFFSETQZSS }, { CLOCKORBIT_SATBDS, 'C', CLOCKORBIT_NUMBDS, CLOCKORBIT_OFFSETBDS }, }; for (const SysInfo& si : sysInfos) { unsigned s = si.sysIdx; if (!_antennaReceived[s]) { continue; // no Antenna message received for this system since the last check } _antennaReceived[s] = false; if (_antenna.SatelliteMask[s] == 0) { continue; // no satellites in this system's antenna data } for (unsigned k = 0; k < si.numSat; k++) { if (!((_antenna.SatelliteMask[s] >> (63 - k)) & 1ULL)) { continue; } unsigned i = si.offset + k; if (_antennaSentTime[i].valid() && _antenna.SSRProviderID[s] == _antennaSentProviderID[i] && _antenna.SatelliteAntennaIOD[s] == _antennaSentIOD[i] && _lastTime - _antennaSentTime[i] < ANT_REFRESH_SEC) { continue; // unchanged retransmission, emitted recently } _antennaSentProviderID[i] = _antenna.SSRProviderID[s]; _antennaSentIOD[i] = _antenna.SatelliteAntennaIOD[s]; _antennaSentTime[i] = _lastTime; int num = k + 1; // PRN within this system, DF394 MSB-first convention int flag = 0; switch (si.sysChar) { case 'G': flag = t_eph::LNAV; break; case 'R': flag = t_eph::FDMA_M; break; case 'E': flag = t_eph::INAV; break; case 'J': flag = t_eph::LNAV; break; case 'C': flag = (num < 6 || (num > 58 && num < 63)) ? t_eph::D2 : t_eph::D1; break; } t_satAntenna satAntenna; satAntenna._prn.set(si.sysChar, num, flag); satAntenna._staID = _staID.toStdString(); satAntenna._time = _lastTime; satAntenna._ssrProviderID = _antenna.SSRProviderID[s]; satAntenna._satelliteAntennaIOD = _antenna.SatelliteAntennaIOD[s]; satAntenna._phaseCenterInfoInd = _antenna.PhaseCenterInformationIndicator[s]; satAntenna._groupDelayInfoInd = _antenna.GroupDelayInformationIndicator[s]; satAntenna._nadirAngleDependentCorrInd = _antenna.NadirAngleDependentCorrectionIndicator[s]; satAntenna._maximumOffNadirAngle = _antenna.maximumOffNadirAngle[s]; satAntenna._nadirAngleRangeExtension = _antenna.NadirAngleDependentCorrectionRangeExtension[s]; const SsrCorr::Antenna::SatellitePart& satPart = _antenna.Sat[i]; for (unsigned f = 0; f < ANT_MAXFREQUENCIES; f++) { if (!((satPart.GnssFrequencyMask >> (ANT_MAXFREQUENCIES - 1 - f)) & 1U)) { continue; } t_frqAntenna frqAntenna; frqAntenna._frqType = _ssrCorr->antFreqIndexToStr(si.sysChar, f); if (frqAntenna._frqType.empty()) { continue; // reserved/unmapped DF+018 bit position } frqAntenna._nadirCorrectionIndicator = satPart.Freq[f].NadirCorrectionIndicator; frqAntenna._nadirCorrection = satPart.Freq[f].NadirCorrection; if (satAntenna._nadirAngleDependentCorrInd) { unsigned count = satAntenna._maximumOffNadirAngle + 1; for (unsigned deg = 0; deg < count && deg < ANT_MAXNADIRDEGREES; deg++) { frqAntenna._nadirAngleCorrection.push_back(satPart.Freq[f].NadirAngleCorrection[deg]); } } satAntenna._freq.push_back(frqAntenna); } _satAntennas[_lastTime].append(satAntenna); } } } // Dump all older epochs // --------------------- dumpEpochs(false); } // Emit and write the collected corrections of all epochs older than // _lastTime (all epochs if all is set, e.g. at the end of a replay) //////////////////////////////////////////////////////////////////////////// void RTCM3coDecoder::dumpEpochs(bool all) { QMutableMapIterator > itOrb(_orbCorrections); while (itOrb.hasNext()) { itOrb.next(); if (all || itOrb.key() < _lastTime) { emit newOrbCorrections(itOrb.value()); t_orbCorr::writeEpoch(_out, itOrb.value()); itOrb.remove(); } } QMutableMapIterator > itClk(_clkCorrections); while (itClk.hasNext()) { itClk.next(); if (all || itClk.key() < _lastTime) { emit newClkCorrections(itClk.value()); t_clkCorr::writeEpoch(_out, itClk.value()); itClk.remove(); } } QMutableMapIterator > itCB(_codeBiases); while (itCB.hasNext()) { itCB.next(); if (all || itCB.key() < _lastTime) { emit newCodeBiases(itCB.value()); t_satCodeBias::writeEpoch(_out, itCB.value()); itCB.remove(); } } QMutableMapIterator > itPB(_phaseBiases); while (itPB.hasNext()) { itPB.next(); if (all || itPB.key() < _lastTime) { emit newPhaseBiases(itPB.value()); t_satPhaseBias::writeEpoch(_out, itPB.value()); itPB.remove(); } } QMutableMapIterator itTec(_vTecMap); while (itTec.hasNext()) { itTec.next(); if (all || itTec.key() < _lastTime) { emit newTec(itTec.value()); t_vTec::write(_out, itTec.value()); itTec.remove(); } } QMutableMapIterator itMD(_metaDataMap); while (itMD.hasNext()) { itMD.next(); if (all || itMD.key() < _lastTime) { emit newMetaData(itMD.value()); t_metaData::write(_out, itMD.value()); itMD.remove(); } } QMutableMapIterator > itAnt(_satAntennas); while (itAnt.hasNext()) { itAnt.next(); if (all || itAnt.key() < _lastTime) { emit newSatAntennas(itAnt.value()); t_satAntenna::writeEpoch(_out, itAnt.value()); itAnt.remove(); } } if (all && _out) { _out->flush(); } } // //////////////////////////////////////////////////////////////////////////// void RTCM3coDecoder::checkProviderID() { if (_clkOrb.SSRProviderID == 0 && _clkOrb.SSRSolutionID == 0 && _clkOrb.SSRIOD == 0) { return; } int newProviderID[3]; newProviderID[0] = _clkOrb.SSRProviderID; newProviderID[1] = _clkOrb.SSRSolutionID; newProviderID[2] = _clkOrb.SSRIOD; QString newProviderIDStr = QString(" [SSR Provider ID: %1 SSR Solution ID: %2 SSR IOD: %3]: ") .arg(newProviderID[0]).arg(newProviderID[1]).arg(newProviderID[2]); bool alreadySet = false; bool different = false; for (unsigned ii = 0; ii < 3; ii++) { if (_providerID[ii] != -1) { alreadySet = true; } if (_providerID[ii] != newProviderID[ii]) { different = true; } _providerID[ii] = newProviderID[ii]; } if (alreadySet && different) { emit newMessage("RTCM3coDecoder: SSR Provider or Service changed " + newProviderIDStr.toLatin1() + _staID.toLatin1(), true); emit providerIDChanged(_staID); } } // Check corrections //////////////////////////////////////////////////////////////////////////// bool RTCM3coDecoder::corrIsOutOfRange(const SsrCorr::ClockOrbit::SatData& coSat) { QString ssrParStr; QString ssrParValue; bool corrIsOutOfRange = false; switch (_type) { // ======== // // IGS SSR // // ======== // case IGSssr: if (coSat.Clock.DeltaA0 < -209.7151 || coSat.Clock.DeltaA0 > +209.7151) { ssrParStr = "Clock::DeltaA0"; ssrParValue = QString::number(coSat.Clock.DeltaA0, 'f', 4); corrIsOutOfRange = true; } if (coSat.Clock.DeltaA1 < -1.048575 || coSat.Clock.DeltaA1 > +1.048575) { ssrParStr = "Clock::DeltaA1"; ssrParValue = QString::number(coSat.Clock.DeltaA1, 'f', 6); corrIsOutOfRange = true; } if (coSat.Clock.DeltaA2 < -1.3421772 || coSat.Clock.DeltaA2 > +1.3421772) { ssrParStr = "Clock::DeltaA2"; ssrParValue = QString::number(coSat.Clock.DeltaA2, 'f', 7); corrIsOutOfRange = true; } if (coSat.Orbit.DeltaRadial < -209.7151 || coSat.Orbit.DeltaRadial > +209.7151) { ssrParStr = "Orbit::DeltaRadial"; ssrParValue = QString::number(coSat.Orbit.DeltaRadial, 'f', 4); corrIsOutOfRange = true; } if (coSat.Orbit.DeltaAlongTrack < -209.7148 || coSat.Orbit.DeltaAlongTrack > +209.7148) { ssrParStr = "Orbit::DeltaAlongTrack"; ssrParValue = QString::number(coSat.Orbit.DeltaAlongTrack, 'f', 4); corrIsOutOfRange = true; } if (coSat.Orbit.DeltaCrossTrack < -209.7148 || coSat.Orbit.DeltaCrossTrack > +209.7148) { ssrParStr = "Orbit::DeltaCrossTrack"; ssrParValue = QString::number(coSat.Orbit.DeltaCrossTrack, 'f', 4); corrIsOutOfRange = true; } if (coSat.Orbit.DotDeltaRadial < -1.048575 || coSat.Orbit.DotDeltaRadial > +1.048575) { ssrParStr = "Orbit::DotDeltaRadial"; ssrParValue = QString::number(coSat.Orbit.DotDeltaRadial, 'f', 6); corrIsOutOfRange = true; } if (coSat.Orbit.DotDeltaAlongTrack < -1.048572 || coSat.Orbit.DotDeltaAlongTrack > +1.048572) { ssrParStr = "Orbit::DotDeltaAlongTrack"; ssrParValue = QString::number(coSat.Orbit.DotDeltaAlongTrack, 'f', 6); corrIsOutOfRange = true; } if (coSat.Orbit.DotDeltaCrossTrack < -1.048572 || coSat.Orbit.DotDeltaCrossTrack > +1.048572) { ssrParStr = "Orbit::DotDeltaCrossTrack"; ssrParValue = QString::number(coSat.Orbit.DotDeltaCrossTrack, 'f', 6); corrIsOutOfRange = true; } break; //==========// // RTCM SSR // // =========// case RTCMssr: case RTCMnewssr: if (coSat.Clock.DeltaA0 < -209.7151 || coSat.Clock.DeltaA0 > +209.7151) { ssrParStr = "Clock::DeltaA0"; ssrParValue = QString::number(coSat.Clock.DeltaA0, 'f', 4); corrIsOutOfRange = true; } if (coSat.Clock.DeltaA1 < -1.048575 || coSat.Clock.DeltaA1 > +1.048575) { ssrParStr = "Clock::DeltaA1"; ssrParValue = QString::number(coSat.Clock.DeltaA1, 'f', 6); corrIsOutOfRange = true; } if (coSat.Clock.DeltaA2 < -1.34217726 || coSat.Clock.DeltaA2 > +1.34217726) { ssrParStr = "Clock::DeltaA2"; ssrParValue = QString::number(coSat.Clock.DeltaA2, 'f', 8); corrIsOutOfRange = true; } if (coSat.Orbit.DeltaRadial < -209.7151 || coSat.Orbit.DeltaRadial > +209.7151) { ssrParStr = "Orbit::DeltaRadial"; ssrParValue = QString::number(coSat.Orbit.DeltaRadial, 'f', 4); corrIsOutOfRange = true; } if (coSat.Orbit.DeltaAlongTrack < -209.7148 || coSat.Orbit.DeltaAlongTrack > +209.7148) { ssrParStr = "Orbit::DeltaAlongTrack"; ssrParValue = QString::number(coSat.Orbit.DeltaAlongTrack, 'f', 4); corrIsOutOfRange = true; } if (coSat.Orbit.DeltaCrossTrack < -209.7148 || coSat.Orbit.DeltaCrossTrack > +209.7148) { ssrParStr = "Orbit::DeltaCrossTrack"; ssrParValue = QString::number(coSat.Orbit.DeltaCrossTrack, 'f', 4); corrIsOutOfRange = true; } if (coSat.Orbit.DotDeltaRadial < -1.048575 || coSat.Orbit.DotDeltaRadial > +1.048575) { ssrParStr = "Orbit::DotDeltaRadial"; ssrParValue = QString::number(coSat.Orbit.DotDeltaRadial, 'f', 6); corrIsOutOfRange = true; } if (coSat.Orbit.DotDeltaAlongTrack < -1.048572 || coSat.Orbit.DotDeltaAlongTrack > +1.048572) { ssrParStr = "Orbit::DotDeltaAlongTrack"; ssrParValue = QString::number(coSat.Orbit.DotDeltaAlongTrack, 'f', 6); corrIsOutOfRange = true; } if (coSat.Orbit.DotDeltaCrossTrack < -1.048572 || coSat.Orbit.DotDeltaCrossTrack > +1.048572) { ssrParStr = "Orbit::DotDeltaCrossTrack"; ssrParValue = QString::number(coSat.Orbit.DotDeltaCrossTrack, 'f', 6); corrIsOutOfRange = true; } break; } if (corrIsOutOfRange) { emit newMessage("RTCM3coDecoder: Correction " + ssrParStr.toLatin1() + " (" + ssrParValue.toLatin1() + ") " + "is out of range " + _staID.toLatin1(), true); } return corrIsOutOfRange; } // //////////////////////////////////////////////////////////////////////////// void RTCM3coDecoder::setEpochTime() { // _phaseBias is no longer wiped between messages (kept alive so a later // Extended Phase Bias message can correlate against it), so // NumberOfSat[s] > 0 alone no longer means "phase bias was just decoded // this cycle" - it can be stale from many messages ago. Gate each // phase-bias fallback below on the same per-system freshness tracking // sendResults() uses (_phaseBiasSentEpoch), so a stale leftover value // never gets picked as this cycle's epoch. bncTime prevTime = _lastTime; _lastTime.reset(); double epoSecGPS = -1.0; double epoSecGlo = -1.0; double epoSecGal = -1.0; double epoSecQzss = -1.0; double epoSecSbas = -1.0; double epoSecBds = -1.0; if (_clkOrb.NumberOfSat[CLOCKORBIT_SATGPS] > 0) { epoSecGPS = _clkOrb.EpochTime[CLOCKORBIT_SATGPS]; // 0 .. 604799 s } else if (_codeBias.NumberOfSat[CLOCKORBIT_SATGPS] > 0) { epoSecGPS = _codeBias.EpochTime[CLOCKORBIT_SATGPS]; // 0 .. 604799 s } else if (_phaseBias.NumberOfSat[CLOCKORBIT_SATGPS] > 0 && _phaseBias.EpochTime[CLOCKORBIT_SATGPS] != _phaseBiasSentEpoch[CLOCKORBIT_SATGPS]) { epoSecGPS = _phaseBias.EpochTime[CLOCKORBIT_SATGPS]; // 0 .. 604799 s } else if (_vTEC.NumLayers > 0) { epoSecGPS = _vTEC.EpochTime; // 0 .. 604799 s } else if (_clkOrb.NumberOfSat[CLOCKORBIT_SATGLONASS] > 0) { epoSecGlo = _clkOrb.EpochTime[CLOCKORBIT_SATGLONASS]; // 0 .. 86399 s } else if (_codeBias.NumberOfSat[CLOCKORBIT_SATGLONASS] > 0) { epoSecGlo = _codeBias.EpochTime[CLOCKORBIT_SATGLONASS]; // 0 .. 86399 s } else if (_phaseBias.NumberOfSat[CLOCKORBIT_SATGLONASS] > 0 && _phaseBias.EpochTime[CLOCKORBIT_SATGLONASS] != _phaseBiasSentEpoch[CLOCKORBIT_SATGLONASS]) { epoSecGlo = _phaseBias.EpochTime[CLOCKORBIT_SATGLONASS]; // 0 .. 86399 s } else if (_clkOrb.NumberOfSat[CLOCKORBIT_SATGALILEO] > 0) { epoSecGal = _clkOrb.EpochTime[CLOCKORBIT_SATGALILEO]; } else if (_codeBias.NumberOfSat[CLOCKORBIT_SATGALILEO] > 0) { epoSecGal = _codeBias.EpochTime[CLOCKORBIT_SATGALILEO]; } else if (_phaseBias.NumberOfSat[CLOCKORBIT_SATGALILEO] > 0 && _phaseBias.EpochTime[CLOCKORBIT_SATGALILEO] != _phaseBiasSentEpoch[CLOCKORBIT_SATGALILEO]) { epoSecGal = _phaseBias.EpochTime[CLOCKORBIT_SATGALILEO]; } else if (_clkOrb.NumberOfSat[CLOCKORBIT_SATQZSS] > 0) { epoSecQzss = _clkOrb.EpochTime[CLOCKORBIT_SATQZSS]; } else if (_codeBias.NumberOfSat[CLOCKORBIT_SATQZSS] > 0) { epoSecQzss = _codeBias.EpochTime[CLOCKORBIT_SATQZSS]; } else if (_phaseBias.NumberOfSat[CLOCKORBIT_SATQZSS] > 0 && _phaseBias.EpochTime[CLOCKORBIT_SATQZSS] != _phaseBiasSentEpoch[CLOCKORBIT_SATQZSS]) { epoSecQzss = _phaseBias.EpochTime[CLOCKORBIT_SATQZSS]; } else if (_clkOrb.NumberOfSat[CLOCKORBIT_SATSBAS] > 0) { epoSecSbas = _clkOrb.EpochTime[CLOCKORBIT_SATSBAS]; } else if (_codeBias.NumberOfSat[CLOCKORBIT_SATSBAS] > 0) { epoSecSbas = _codeBias.EpochTime[CLOCKORBIT_SATSBAS]; } else if (_phaseBias.NumberOfSat[CLOCKORBIT_SATSBAS] > 0 && _phaseBias.EpochTime[CLOCKORBIT_SATSBAS] != _phaseBiasSentEpoch[CLOCKORBIT_SATSBAS]) { epoSecSbas = _phaseBias.EpochTime[CLOCKORBIT_SATSBAS]; } else if (_clkOrb.NumberOfSat[CLOCKORBIT_SATBDS] > 0) { epoSecBds = _clkOrb.EpochTime[CLOCKORBIT_SATBDS]; } else if (_codeBias.NumberOfSat[CLOCKORBIT_SATBDS] > 0) { epoSecBds = _codeBias.EpochTime[CLOCKORBIT_SATBDS]; } else if (_phaseBias.NumberOfSat[CLOCKORBIT_SATBDS] > 0 && _phaseBias.EpochTime[CLOCKORBIT_SATBDS] != _phaseBiasSentEpoch[CLOCKORBIT_SATBDS]) { epoSecBds = _phaseBias.EpochTime[CLOCKORBIT_SATBDS]; } // Retrieve current time // --------------------- int currentWeek = 0; double currentSec = 0.0; currentGPSWeeks(currentWeek, currentSec); bncTime currentTime(currentWeek, currentSec); // Set _lastTime close to currentTime // ---------------------------------- if (_type == IGSssr) { if (epoSecGPS != -1) _igsRefEpoSec = epoSecGPS; else if (epoSecGal != -1) _igsRefEpoSec = epoSecGal; else if (epoSecQzss != -1) _igsRefEpoSec = epoSecQzss; else if (epoSecSbas != -1) _igsRefEpoSec = epoSecSbas; } char epoSys = 0; // system the epoch of this message is taken from if (epoSecGPS != -1) { epoSys = 'G'; _lastTime.set(currentWeek, epoSecGPS); } else if (epoSecGlo != -1) { epoSys = 'R'; QDate date = dateAndTimeFromGPSweek(currentTime.gpsw(), currentTime.gpssec()).date(); double leap = gnumleap(date.year(), date.month(), date.day()); // IGS-SSR: the SSR Epoch Time (IDF003, 20 bit) is given in one continuous // time scale for all GNSS, i.e. GPS seconds of week also for GLONASS // (IGS SSR v1.00) - no conversion. Old encoders wrote UTC instead, as // seconds of day (e.g. BNC before 2026-09-30) or as seconds of week: // recognised by comparison with the GPS/Galileo epoch of the same stream // and converted, with a warning, during a transition period. The value // in GPS time is taken modulo one day around the stream's GPS epoch, // which covers both variants. if (_type == IGSssr && _igsRefEpoSec >= 0.0 && igsLegacyEpoch(epoSecGlo, fmod(_igsRefEpoSec - leap + 7 * 86400.0, 86400.0), 86400.0)) { // seconds of day: outdated convention; seconds of week: erroneous bool secOfWeek = (epoSecGlo >= 86400.0); double dd = fmod(epoSecGlo + leap - _igsRefEpoSec, 86400.0); if (dd > 43200.0) dd -= 86400.0; else if (dd < -43200.0) dd += 86400.0; epoSecGlo = _igsRefEpoSec + dd; bncTime& warned = _legacyEpochWarned['R']; if (!warned.valid() || currentTime - warned >= 3600.0) { warned = currentTime; if (secOfWeek) { emit newMessage(_staID.toLatin1() + ": IGS-SSR GLONASS epoch time erroneously encoded " "(UTC seconds of week instead of GPS seconds of week, IGS SSR v1.00 IDF003) " "detected and converted - the SSR encoder should be corrected", true); } else { emit newMessage(_staID.toLatin1() + ": IGS-SSR GLONASS epoch time in outdated convention " "(UTC seconds of day instead of GPS seconds of week, IGS SSR v1.00 IDF003) " "detected and converted - the SSR encoder should be updated", true); } } } if (_type == RTCMssr || _type == RTCMnewssr) { // Same GLONASS epoch wire convention (UTC+3h) as RTCMssr epoSecGlo = epoSecGlo - 3 * 3600 + leap; } _lastTime.set(currentWeek, epoSecGlo); } else if (epoSecGal != -1) { epoSys = 'E'; _lastTime.set(currentWeek, epoSecGal); } else if (epoSecQzss != -1) { epoSys = 'J'; _lastTime.set(currentWeek, epoSecQzss); } else if (epoSecSbas != -1) { epoSys = 'S'; _lastTime.set(currentWeek, epoSecSbas); } else if (epoSecBds != -1) { epoSys = 'C'; // RTCM-SSR: BDT; IGS-SSR: GPS time scale for all GNSS (IDF003). Old // IGS-SSR encoders wrote BDT: recognised and converted as for GLONASS. bool bdt = (_type != IGSssr); if (_type == IGSssr && _igsRefEpoSec >= 0.0 && igsLegacyEpoch(epoSecBds, fmod(_igsRefEpoSec - 14.0 + 604800.0, 604800.0), 604800.0)) { bdt = true; bncTime& warned = _legacyEpochWarned['C']; if (!warned.valid() || currentTime - warned >= 3600.0) { warned = currentTime; emit newMessage(_staID.toLatin1() + ": IGS-SSR BDS epoch time in outdated convention " "(BDT instead of GPS seconds of week, IGS SSR v1.00 IDF003) " "detected and converted - the SSR encoder should be updated", true); } } if (bdt) { epoSecBds += 14.0; if (epoSecBds > 604800.0) { epoSecBds -= 7.0*24.0*60.0*60.0; } } _lastTime.set(currentWeek, epoSecBds); } // Some message types (e.g. Metadata) carry no epoch field of their own, // and none of the above found anything fresh to use either - fall back // to the last known valid time rather than leaving _lastTime invalid // (which would silently drop the message's output entirely). else if (prevTime.valid()) { _lastTime = prevTime; } if (_lastTime.valid()) { double maxDiff = 12 * 3600.0; while (_lastTime < currentTime - maxDiff) { _lastTime = _lastTime + maxDiff; } while (_lastTime > currentTime + maxDiff) { _lastTime = _lastTime - maxDiff; } } // Plausibility: the orbit/clock epochs of all systems of a stream, // converted to GPS time, refer to the same update cycles as its GPS epochs. A difference // of more than MAX_DIFF to the latest GPS epoch (itself not older than // MAX_DIFF) indicates an erroneous encoding not covered above; the epoch // is used as it is and reported at most once per hour and system. // Only orbit/clock messages are checked: code and phase biases may be // sent with the (older) epoch of their last update (e.g. Galileo HAS). const double MAX_DIFF = 120.0; bool clkOrbMsg = false; for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) { if (_clkOrb.NumberOfSat[s] > 0) { clkOrbMsg = true; } } if (!clkOrbMsg) { return; } if (_lastTime.valid() && epoSys == 'G') { _lastGpsEpoch = _lastTime; } else if (_lastTime.valid() && epoSys != 0 && _lastGpsEpoch.valid() && fabs(currentTime - _lastGpsEpoch) <= MAX_DIFF && fabs(_lastTime - _lastGpsEpoch) > MAX_DIFF) { bncTime& warned = _epochDiffWarned[QChar(epoSys)]; if (!warned.valid() || currentTime - warned >= 3600.0) { warned = currentTime; emit newMessage(QString("%1: %2 SSR epoch time %3 %4 differs by %5 s from the GPS SSR epoch time " "of the stream - erroneously encoded?") .arg(_staID).arg(epoSys) .arg(QString::fromStdString(_lastTime.datestr())) .arg(QString::fromStdString(_lastTime.timestr(0))) .arg(_lastTime - _lastGpsEpoch, 0, 'f', 0).toLatin1(), true); } } } // IGS-SSR: true if value (seconds, periodic with period) matches the epoch // expected in the outdated convention (legacyRef) better than the GPS time // reference of the stream, within the spread of epochs of one update cycle //////////////////////////////////////////////////////////////////////////// bool RTCM3coDecoder::igsLegacyEpoch(double value, double legacyRef, double period) const { auto dist = [period](double a, double b) { double d = fmod(fabs(a - b), period); return std::min(d, period - d); }; double dSpec = dist(value, fmod(_igsRefEpoSec, period)); double dLegacy = dist(value, legacyRef); return dLegacy < dSpec && dLegacy <= 30.0; } // Satellite numbers on the wire can exceed BNC's supported range (e.g. 6-bit // IDs up to 63, while t_prn supports G01-G32). They are passed on unchanged, // but t_prn::toInt() maps them to the unused index 0, so they are not usable // for per-satellite processing - report that once per satellite. //////////////////////////////////////////////////////////////////////////// void RTCM3coDecoder::checkPrnRange(const t_prn& prn) { if (prn.toInt() != 0) { return; } QString prnStr = QString::fromStdString(prn.toString()); if (!_prnOutOfRangeLogged.contains(prnStr)) { _prnOutOfRangeLogged.insert(prnStr); emit newMessage(QString("%1: satellite %2 outside the range supported by BNC - not usable for processing") .arg(_staID).arg(prnStr).toLatin1(), true); } } // All Satellite Antenna messages of one GNSS must use the same Satellite // Antenna IOD (a satellite set may be split over several messages). During // a legitimate IOD change old and new IOD are both seen for a while, until // every message has been sent with the new one - but the IOD never returns // to a replaced value within 64 days (uniqueness per SSR Provider ID), so a // switch back to the replaced IOD within that period reveals messages of the // same GNSS using different IODs. Reported once per GNSS and IOD pair; the // data itself is not changed (PPP checks each satellite's IOD individually). //////////////////////////////////////////////////////////////////////////// void RTCM3coDecoder::checkAntennaIOD(unsigned s) { const double MAX_AGE_SEC = 64.0 * 86400.0; unsigned int iod = _antenna.SatelliteAntennaIOD[s]; unsigned int provider = _antenna.SSRProviderID[s]; int currentWeek = 0; double currentSec = 0.0; currentGPSWeeks(currentWeek, currentSec); bncTime currentTime(currentWeek, currentSec); if (!_antennaIODKnown[s] || provider != _antennaIODProvider[s]) { _antennaIODKnown[s] = true; _antennaIODProvider[s] = provider; _antennaIODCur[s] = iod; _antennaIODPrevTime[s].reset(); // no replaced IOD yet return; } if (iod == _antennaIODCur[s]) { return; } if (_antennaIODPrevTime[s].valid() && iod == _antennaIODPrev[s] && currentTime - _antennaIODPrevTime[s] < MAX_AGE_SEC) { const char sysChars[CLOCKORBIT_SATNUM] = {'G', 'R', 'E', 'J', 'S', 'C'}; char sys = (s < CLOCKORBIT_SATNUM) ? sysChars[s] : '?'; QString key = QString("%1_%2_%3").arg(sys).arg(_antennaIODCur[s]).arg(iod); if (!_antennaIODLogged.contains(key)) { _antennaIODLogged.insert(key); emit newMessage(QString("%1: GNSS %2 Satellite Antenna IOD switches back %3 -> %4" " - messages of one GNSS must use the same IOD") .arg(_staID).arg(sys).arg(_antennaIODCur[s]).arg(iod).toLatin1(), true); } } _antennaIODPrev[s] = _antennaIODCur[s]; _antennaIODPrevTime[s] = currentTime; _antennaIODCur[s] = iod; }