Index: trunk/BNC/src/RTCM3/RTCM3Decoder.cpp
===================================================================
--- trunk/BNC/src/RTCM3/RTCM3Decoder.cpp	(revision 11046)
+++ trunk/BNC/src/RTCM3/RTCM3Decoder.cpp	(revision 11047)
@@ -141,4 +141,5 @@
     flag = t_corrSSR::getSsrNavTypeFlag(sys, num);
     CurrentObs._prn.set(sys, num, flag);
+    checkPrnRange(CurrentObs._prn);
 
     t_frqObs *frqObs = new t_frqObs;
@@ -723,4 +724,5 @@
           else
             CurrentObs._prn.set(sys, RTCM3_MSM_NUMSAT - i);
+          checkPrnRange(CurrentObs._prn);
           j = RTCM3_MSM_NUMSIG;
           while (!(sigmask & (1 << --j)))
@@ -954,7 +956,10 @@
     int flag = t_corrSSR::getSsrNavTypeFlag(sys, sv);
     CurrentObs._prn.set(sys, sv, flag);
+    checkPrnRange(CurrentObs._prn);
     GETBITS(code, 1)
     GETBITS(freq, 5)
-    GLOFreq[sv - 1] = 100 + freq - 7; /* store frequency for other users (MSM) */
+    if (sv >= 1) { /* 6-bit slot 1..63 fits GLOFreq[64]; 0 is invalid */
+      GLOFreq[sv - 1] = 100 + freq - 7; /* store frequency for other users (MSM) */
+    }
 
     t_frqObs *frqObs = new t_frqObs;
@@ -1066,4 +1071,5 @@
     }
     eph._prn.set('G', i);
+    checkPrnRange(eph._prn);
     GETBITS(week, 10)
     if (week < 0 || week > 1023) {
@@ -1185,4 +1191,5 @@
     }
     eph._prn.set('R', sv);
+    checkPrnRange(eph._prn);
 
     GETBITS(i, 5)
@@ -1373,4 +1380,5 @@
     }
     eph._prn.set('J', i);
+    checkPrnRange(eph._prn);
 
     GETBITS(i, 16)
@@ -1493,4 +1501,5 @@
     }
     eph._prn.set('I', i);
+    checkPrnRange(eph._prn);
     GETBITS(week, 10)
     if (week < 0 || week > 1023) {
@@ -1627,4 +1636,5 @@
     }
     eph._prn.set('S', 20 + i);
+    checkPrnRange(eph._prn);
     GETBITS(eph._IODN, 8)
     GETBITS(i, 13)
@@ -1716,4 +1726,5 @@
     }
     eph._prn.set('E', i, eph._inav ? t_eph::INAV : t_eph::FNAV);
+    checkPrnRange(eph._prn);
 
     GETBITS(week, 12) //FIXME: roll-over after week 4095!!
@@ -1863,4 +1874,5 @@
     }
     eph._prn.set('C', i);
+    checkPrnRange(eph._prn);
 
     GETBITS(week, 13)
@@ -2439,5 +2451,18 @@
 }
 
-
-
-
+// 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 RTCM3Decoder::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);
+  }
+}
Index: trunk/BNC/src/RTCM3/RTCM3Decoder.h
===================================================================
--- trunk/BNC/src/RTCM3/RTCM3Decoder.h	(revision 11046)
+++ trunk/BNC/src/RTCM3/RTCM3Decoder.h	(revision 11047)
@@ -60,4 +60,8 @@
 
  private:
+  // Log (once per satellite) a satellite number outside BNC's supported
+  // range (t_prn::MAXPRN_*); such satellites map to the unused index 0.
+  void checkPrnRange(const t_prn& prn);
+  QSet<QString>                         _prnOutOfRangeLogged;
   /**
    * Extract a RTCM3 message. Data is passed in the follow fields:<br>
Index: trunk/BNC/src/RTCM3/RTCM3coDecoder.cpp
===================================================================
--- trunk/BNC/src/RTCM3/RTCM3coDecoder.cpp	(revision 11046)
+++ trunk/BNC/src/RTCM3/RTCM3coDecoder.cpp	(revision 11047)
@@ -105,5 +105,13 @@
 
   for (unsigned s = 0; s < CLOCKORBIT_SATNUM; s++) {
-    _antennaSentIOD[s] = UINT_MAX; // "never sent"
+    _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")
   }
 
@@ -203,4 +211,13 @@
     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;
 
@@ -240,4 +257,12 @@
 
       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
 
@@ -342,4 +367,5 @@
       t_orbCorr orbCorr;
       orbCorr._prn.set(sys, num, flag);
+      checkPrnRange(orbCorr._prn);
       orbCorr._staID     = _staID.toStdString();
       orbCorr._iod       = _clkOrb.Sat[ii].IOD;
@@ -378,4 +404,5 @@
       t_clkCorr clkCorr;
       clkCorr._prn.set(sys, _clkOrb.Sat[ii].ID, flag);
+      checkPrnRange(clkCorr._prn);
       clkCorr._staID      = _staID.toStdString();
       clkCorr._time       = _lastTime;
@@ -470,7 +497,10 @@
     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];
@@ -560,7 +590,10 @@
     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;
@@ -649,9 +682,26 @@
   // _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. _antennaSentIOD tracks the last SatelliteAntennaIOD actually
-  // emitted per system, so unrelated messages (and unchanged retransmissions
-  // of the same antenna data) don't cause duplicate output - the same
-  // freshness problem _phaseBiasSentEpoch solves for phase biases.
+  // 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[] = {
@@ -664,9 +714,10 @@
     for (const SysInfo& si : sysInfos) {
       unsigned s = si.sysIdx;
-      if (_antenna.SatelliteAntennaIOD[s] == _antennaSentIOD[s]) {
-        continue; // nothing new for this system since the last emit
-      }
+      if (!_antennaReceived[s]) {
+        continue; // no Antenna message received for this system since the last check
+      }
+      _antennaReceived[s] = false;
       if (_antenna.SatelliteMask[s] == 0) {
-        continue; // this system's antenna data was never decoded
+        continue; // no satellites in this system's antenna data
       }
       for (unsigned k = 0; k < si.numSat; k++) {
@@ -674,4 +725,14 @@
           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;
@@ -696,5 +757,5 @@
         satAntenna._nadirAngleRangeExtension  = _antenna.NadirAngleDependentCorrectionRangeExtension[s];
 
-        const SsrCorr::Antenna::SatellitePart& satPart = _antenna.Sat[si.offset + k];
+        const SsrCorr::Antenna::SatellitePart& satPart = _antenna.Sat[i];
         for (unsigned f = 0; f < ANT_MAXFREQUENCIES; f++) {
           if (!((satPart.GnssFrequencyMask >> (ANT_MAXFREQUENCIES - 1 - f)) & 1U)) {
@@ -718,5 +779,4 @@
         _satAntennas[_lastTime].append(satAntenna);
       }
-      _antennaSentIOD[s] = _antenna.SatelliteAntennaIOD[s];
     }
   }
@@ -1109,2 +1169,65 @@
   }
 }
+
+// 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;
+}
Index: trunk/BNC/src/RTCM3/RTCM3coDecoder.h
===================================================================
--- trunk/BNC/src/RTCM3/RTCM3coDecoder.h	(revision 11046)
+++ trunk/BNC/src/RTCM3/RTCM3coDecoder.h	(revision 11047)
@@ -30,4 +30,5 @@
 #include <QList>
 #include <QMap>
+#include <QSet>
 #include "GPSDecoder.h"
 #include "../RTCM3/clock_and_orbit/clock_orbit_igs.h"
@@ -75,4 +76,9 @@
   void checkProviderID();
   bool corrIsOutOfRange(const SsrCorr::ClockOrbit::SatData& coSat);
+  // Log (once per satellite) a satellite number outside BNC's supported
+  // range (t_prn::MAXPRN_*); such satellites map to the unused index 0.
+  void checkPrnRange(const t_prn& prn);
+  QSet<QString>                         _prnOutOfRangeLogged;
+
 
   struct t_stateSnapshot {
@@ -111,5 +117,17 @@
   QMap<bncTime, QList<t_satAntenna> >   _satAntennas;
   unsigned int                          _phaseBiasSentEpoch[CLOCKORBIT_SATNUM];
-  unsigned int                          _antennaSentIOD[CLOCKORBIT_SATNUM];
+  bool                                  _antennaReceived[CLOCKORBIT_SATNUM];
+  unsigned int                          _antennaSentProviderID[CLOCKORBIT_COUNTSAT];
+  unsigned int                          _antennaSentIOD[CLOCKORBIT_COUNTSAT];
+  bncTime                               _antennaSentTime[CLOCKORBIT_COUNTSAT];
+  // per GNSS: Satellite Antenna IOD currently in use and the one it replaced,
+  // to detect messages of one GNSS using different IODs (see checkAntennaIOD())
+  void checkAntennaIOD(unsigned s);
+  bool                                  _antennaIODKnown[CLOCKORBIT_SATNUM];
+  unsigned int                          _antennaIODProvider[CLOCKORBIT_SATNUM];
+  unsigned int                          _antennaIODCur[CLOCKORBIT_SATNUM];
+  unsigned int                          _antennaIODPrev[CLOCKORBIT_SATNUM];
+  bncTime                               _antennaIODPrevTime[CLOCKORBIT_SATNUM]; // when _antennaIODPrev was last seen
+  QSet<QString>                         _antennaIODLogged;
 };
 
