Index: /trunk/BNC/CHANGELOG.md
===================================================================
--- /trunk/BNC/CHANGELOG.md	(revision 10954)
+++ /trunk/BNC/CHANGELOG.md	(revision 10955)
@@ -4,5 +4,7 @@
 - ADDED: L1C/B (RINEX code: '1E') in QZSS Signal ID Mapping
 - ADDED: Minimum Elevation parameter to BNC's RINEX Editing & QC feature [(#195)](https://software.rtcm-ntrip.org/ticket/195)
-- ADDED: GLONASS-M yaw-fixed attitude model for the APC/CoM offset applied when saving SP3 files, reducing along-track/cross-track errors near the orbit noon/midnight points [(#210)](https://software.rtcm-ntrip.org/ticket/210)
+- ADDED: Possibility to select how satellite attitude is modelled when converting Antenna Phase Center (APC) corrections to
+Center-of-Mass (CoM) positions required for SP3 output. Three options are available: (1) Computed (default): BNC applies its own kinematic attitude model: GPS noon/midnight turn manoeuvres (Kouba 2009/2015, Bar-Sever 1996), GLONASS yaw-fixed mode (Dilssner et al. 2011), and
+Galileo / BDS orbit-normal mode switching (Kouba 2017, Dai et al. 2015, Steigenberger et al. 2018). (2) Nominal: a simplified, continuous Sun-pointing model is used without any manoeuvre modelling. (3) SSR: the yaw angle transmitted in the SSR phase bias message is used directly, if present for the satellite and epoch.  [(#210)](https://software.rtcm-ntrip.org/ticket/210)
 - FIXED: BNC is now able to work with com ports with higher numbers, e.g. COM11 or COM17  [(#205)](https://software.rtcm-ntrip.org/ticket/205)
 - FIXED: Handling of ionospheric constraints [(#220)](https://software.rtcm-ntrip.org/ticket/220)
Index: /trunk/BNC/src/bncantex.cpp
===================================================================
--- /trunk/BNC/src/bncantex.cpp	(revision 10954)
+++ /trunk/BNC/src/bncantex.cpp	(revision 10955)
@@ -192,4 +192,22 @@
             line.indexOf("NavIC") == 0 ){
           newAntMap->antName = line.mid(20,3);
+          if (line.indexOf("BLOCK I") == 0) {
+            // Extract GPS block type: "BLOCK IIF   " → "IIF"
+            QString bt = line.mid(0, 20).trimmed();
+            if (bt.startsWith("BLOCK "))
+              newAntMap->blockType = bt.mid(6);
+          }
+          else if (line.indexOf("GALILEO") == 0) {
+            // "GALILEO-1" → "1" (IOV), "GALILEO-2" → "2" (FOC)
+            QString bt = line.mid(0, 20).trimmed();
+            if (bt.startsWith("GALILEO-"))
+              newAntMap->blockType = bt.mid(8);
+          }
+          else if (line.indexOf("BEIDOU") == 0) {
+            // "BEIDOU-2M" → "2M", "BEIDOU-3M-CAST" → "3M-CAST", etc.
+            QString bt = line.mid(0, 20).trimmed();
+            if (bt.startsWith("BEIDOU-"))
+              newAntMap->blockType = bt.mid(7);
+          }
         }
         else {
@@ -448,9 +466,281 @@
 }
 
+// GPS satellite yaw angle during nominal tracking and noon/midnight turns.
+//
+// GPS satellites perform a "noon turn" (and, for some blocks, a "midnight
+// turn") when the Sun's elevation angle above the orbital plane (beta) is
+// small: the required nominal yaw rate then exceeds the satellite's
+// mechanical maximum, so the satellite yaws at that maximum rate until it
+// catches back up to the nominal Sun-pointing orientation (Kouba 2009/2015,
+// Bar-Sever 1996).
+//
+// The max yaw rates below are the best published estimates per block type:
+//   IIA:   0.12 °/s (Kouba 2009)
+//   IIR:   0.20 °/s (Bar-Sever 1996)
+//   IIR-M: 0.20 °/s
+//   IIF:   0.11 °/s (Kouba 2015)
+//   IIIA:  0.15 °/s (tentative)
+//
+// Returns the effective yaw angle [rad] in the velocity-referenced frame,
+// for use with the same Rodrigues rotation as the GLONASS model. During
+// nominal tracking this equals psiNom and the result is identical to the
+// simple sz×xSun formula.
+////////////////////////////////////////////////////////////////////////////
+double bncAntex::gpsYawAngle(const QString& prn, const QString& blockType,
+                              double Mjd,
+                              const ColumnVector& xSat,
+                              const ColumnVector& vSat,
+                              const ColumnVector& xSun) {
+
+  // Max yaw rate [rad/s] by GPS block type
+  double psiDotMax;
+  if      (blockType == "IIA")   psiDotMax = 0.12 * M_PI / 180.0;
+  else if (blockType == "IIR")   psiDotMax = 0.20 * M_PI / 180.0;
+  else if (blockType == "IIR-M") psiDotMax = 0.20 * M_PI / 180.0;
+  else if (blockType == "IIF")   psiDotMax = 0.11 * M_PI / 180.0;
+  else if (blockType == "IIIA")  psiDotMax = 0.15 * M_PI / 180.0;
+  else return 0.0; // unknown block: caller uses simple Sun-pointing
+
+  const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 min in days
+
+  // Inertial velocity
+  ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
+  ColumnVector vInert = vSat + crossproduct(Omega, xSat);
+
+  // Orbital angular momentum vector → orbital rate
+  ColumnVector h     = crossproduct(xSat, vInert);
+  double       hNorm = sqrt(DotProduct(h, h));
+  ColumnVector orbNormal = h / hNorm;
+  double       r    = sqrt(DotProduct(xSat, xSat));
+  double       nRate = hNorm / (r * r); // [rad/s]
+
+  // Beta angle
+  double beta = asin(DotProduct(orbNormal, xSun));
+
+  // Mu: orbit angle from midnight (same geometry as GLONASS)
+  ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
+  sunProj /= sqrt(DotProduct(sunProj, sunProj));
+  ColumnVector eX = -1.0 * sunProj; // midnight direction
+  ColumnVector eY = crossproduct(orbNormal, eX);
+  ColumnVector rHat = xSat / r;
+  double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
+
+  // Nominal yaw and its rate
+  double tanBeta = tan(beta);
+  double sinMu   = sin(mu);
+  double psiNom  = atan2(-tanBeta, sinMu);
+  double denom   = tanBeta * tanBeta + sinMu * sinMu;
+  // |dPsi/dt| (always non-negative, sign comes from sign of tanBeta*cos(mu))
+  double psiDotNomAbs = (denom > 1e-12)
+                        ? nRate * fabs(tanBeta * cos(mu)) / denom
+                        : 1e9;
+  // Sign of the nominal yaw rate: dPsi/dt = nRate * tanBeta * cos(mu) / denom
+  double psiDotNomSign = (tanBeta * cos(mu) >= 0.0) ? 1.0 : -1.0;
+
+  t_gpsYaw& st      = _gpsYaw[prn];
+  double    callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
+
+  // Stale state: reset if there has been a gap in calls
+  if (st.valid && callGap > MAX_CALL_GAP) {
+    _gpsYawLog += QString().asprintf(
+      "%s gpsYaw STALE-RESET  Mjd=%.6f beta=%6.3f mu=%7.2f"
+      " old=%7.2f new=%7.2f gap=%.1fmin\n",
+      prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
+      st.yaw*180.0/M_PI, psiNom*180.0/M_PI, callGap*1440.0);
+    st.valid  = false;
+    st.inTurn = false;
+  }
+
+  double psiEff;
+  if (psiDotNomAbs > psiDotMax) {
+    // Constrained: satellite yaws at psiDotMax in the nominal direction
+    if (!st.valid || !st.inTurn) {
+      // Entering a noon/midnight turn
+      psiEff = st.valid ? st.yaw : psiNom;
+      _gpsYawLog += QString().asprintf(
+        "%s gpsYaw TURN-ENTER   Mjd=%.6f beta=%6.3f mu=%7.2f"
+        " psiNom=%7.2f psiEff=%7.2f psiDotNom=%6.3f max=%5.3f\n",
+        prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
+        psiNom*180.0/M_PI, psiEff*180.0/M_PI,
+        psiDotNomAbs*psiDotNomSign*180.0/M_PI, psiDotMax*180.0/M_PI);
+      st.inTurn = true;
+    } else {
+      // Continuing the turn: integrate at constrained rate
+      double dt  = callGap * 86400.0; // [s]
+      psiEff = st.yaw + psiDotNomSign * psiDotMax * dt;
+    }
+    // Wrap to [-pi, pi]
+    while (psiEff >  M_PI) psiEff -= 2.0 * M_PI;
+    while (psiEff < -M_PI) psiEff += 2.0 * M_PI;
+  }
+  else {
+    // Nominal tracking
+    psiEff = psiNom;
+    if (st.inTurn) {
+      _gpsYawLog += QString().asprintf(
+        "%s gpsYaw TURN-EXIT    Mjd=%.6f beta=%6.3f mu=%7.2f"
+        " frozen=%7.2f nominal=%7.2f\n",
+        prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
+        st.yaw*180.0/M_PI, psiNom*180.0/M_PI);
+      st.inTurn = false;
+    }
+    st.yaw = psiNom;
+  }
+
+  st.yaw         = psiEff;
+  st.lastCallMjd = Mjd;
+  st.valid       = true;
+
+  return psiEff;
+}
+
+//
+// Orbit-Normal Mode Yaw Angle — shared model for Galileo and BDS
+//
+// When |beta| < betaThr the satellite rotates toward yaw = 0 (orbit-normal)
+// at the block-specific maximum yaw rate. When |beta| >= betaThr it returns
+// to nominal yaw-steering (psiNom). Transitions are rate-limited in both
+// directions to match physical satellite behaviour.
+//
+// References:
+//   Galileo IOV/FOC: Kouba (2017), Steigenberger et al. (2018)
+//   BDS MEO/IGSO:    Dai et al. (2015), Wang et al. (2018)
+////////////////////////////////////////////////////////////////////////////
+double bncAntex::onModeYawAngle(const QString& prn,
+                                 double betaThr, double psiDotMax, double Mjd,
+                                 const ColumnVector& xSat,
+                                 const ColumnVector& vSat,
+                                 const ColumnVector& xSun) {
+
+  const double MAX_CALL_GAP = 1800.0 / 86400.0;  // 30 min [days]
+
+  // Inertial velocity
+  ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
+  ColumnVector vInert = vSat + crossproduct(Omega, xSat);
+
+  // Orbit geometry
+  ColumnVector h        = crossproduct(xSat, vInert);
+  double       hNorm    = sqrt(DotProduct(h, h));
+  ColumnVector orbNormal = h / hNorm;
+  double       r        = sqrt(DotProduct(xSat, xSat));
+
+  double beta = asin(DotProduct(orbNormal, xSun));
+
+  ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
+  sunProj /= sqrt(DotProduct(sunProj, sunProj));
+  ColumnVector eX   = -1.0 * sunProj;
+  ColumnVector eY   = crossproduct(orbNormal, eX);
+  ColumnVector rHat = xSat / r;
+  double mu     = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
+  double psiNom = atan2(-tan(beta), sin(mu));
+
+  // Target yaw: orbit-normal (0) when below threshold, nominal otherwise
+  bool   wantsON   = (fabs(beta) < betaThr);
+  double psiTarget = wantsON ? 0.0 : psiNom;
+
+  t_onYaw& st      = _onYaw[prn];
+  double   callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
+
+  if (st.valid && callGap > MAX_CALL_GAP) {
+    _onYawLog += QString().asprintf(
+      "%s onYaw STALE-RESET  Mjd=%.6f beta=%5.2f gap=%.1fmin\n",
+      prn.toLatin1().data(), Mjd, beta*180.0/M_PI, callGap*1440.0);
+    st.valid = false;
+  }
+
+  double psiEff;
+  if (!st.valid) {
+    // Cold start: initialise at psiNom regardless of mode
+    psiEff    = psiNom;
+    st.inON   = false;
+  }
+  else {
+    double dt      = callGap * 86400.0;       // [s]
+    double dpsi    = psiTarget - st.yaw;
+    while (dpsi >  M_PI) dpsi -= 2.0 * M_PI;
+    while (dpsi < -M_PI) dpsi += 2.0 * M_PI;
+    double maxDpsi = psiDotMax * dt;
+
+    if (fabs(dpsi) <= maxDpsi) {
+      // Target reached this step
+      psiEff = psiTarget;
+      bool wasON = st.inON;
+      st.inON    = wantsON;
+      if (!wasON && wantsON && fabs(st.yaw) < 0.5*M_PI/180.0) {
+        _onYawLog += QString().asprintf(
+          "%s onYaw ON-ENTER   Mjd=%.6f beta=%5.2f psiNom=%7.2f\n",
+          prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI);
+      }
+      else if (wasON && !wantsON) {
+        _onYawLog += QString().asprintf(
+          "%s onYaw ON-EXIT    Mjd=%.6f beta=%5.2f psiNom=%7.2f\n",
+          prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI);
+        st.inON = false;
+      }
+    }
+    else {
+      // Still rotating toward target
+      psiEff = st.yaw + (dpsi > 0.0 ? 1.0 : -1.0) * maxDpsi;
+    }
+  }
+
+  while (psiEff >  M_PI) psiEff -= 2.0 * M_PI;
+  while (psiEff < -M_PI) psiEff += 2.0 * M_PI;
+
+  st.yaw         = psiEff;
+  st.lastCallMjd = Mjd;
+  st.valid       = true;
+
+  return psiEff;
+}
+
+//
+////////////////////////////////////////////////////////////////////////////
+double bncAntex::galileoYawAngle(const QString& prn, const QString& blockType,
+                                  double Mjd,
+                                  const ColumnVector& xSat,
+                                  const ColumnVector& vSat,
+                                  const ColumnVector& xSun) {
+  // IOV (type "1") and FOC (type "2"): orbit-normal for |beta| < 2 deg.
+  // Max yaw rate 0.20 deg/s applies to both generations.
+  // Kouba (2017), Steigenberger et al. (2018)
+  Q_UNUSED(blockType);
+  const double betaThr   = 2.0 * M_PI / 180.0;
+  const double psiDotMax = 0.20 * M_PI / 180.0;
+  return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun);
+}
+
+//
+////////////////////////////////////////////////////////////////////////////
+double bncAntex::bdsYawAngle(const QString& prn, const QString& blockType,
+                              double Mjd,
+                              const ColumnVector& xSat,
+                              const ColumnVector& vSat,
+                              const ColumnVector& xSun) {
+  // GEO satellites are always in orbit-normal mode (yaw = 0).
+  if (blockType == "2G" || blockType == "3G-CAST")
+    return 0.0;
+
+  double betaThr, psiDotMax;
+  if (blockType == "2M" || blockType == "2I") {
+    // BDS-2 MEO / IGSO: orbit-normal for |beta| < 4 deg (Dai et al. 2015)
+    betaThr   = 4.0 * M_PI / 180.0;
+    psiDotMax = 0.10 * M_PI / 180.0;
+  }
+  else {
+    // BDS-3 (CAST and SECM MEO/IGSO): orbit-normal for |beta| < 3 deg
+    betaThr   = 3.0 * M_PI / 180.0;
+    psiDotMax = 0.15 * M_PI / 180.0;
+  }
+  return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun);
+}
+
+//
 // Satellite Antenna Offset
 ////////////////////////////////////////////////////////////////////////////
 t_irc bncAntex::satCoMcorrection(const QString& prn, double Mjd,
                                  const ColumnVector& xSat,
-                                 const ColumnVector& vSat, ColumnVector& dx) {
+                                 const ColumnVector& vSat, ColumnVector& dx,
+                                 e_attMode mode, double externalYaw) {
 
   t_frequency::type frqType = t_frequency::dummy;
@@ -495,16 +785,44 @@
       ColumnVector sy, sx;
 
-      // GLONASS: override the nominal Sun-pointing attitude near the
-      // orbit noon/midnight points when the beta angle is small (see
-      // glonassYawAngle() above). Elsewhere GLONASS-M follows the same
-      // nominal law as the other constellations, so the result is
-      // identical to the direct Sun-pointing computation used below.
-      // -----------------------------------------------------------------
-      if (prn[0] == 'R' && vSat.size() == 3) {
-        double psi = glonassYawAngle(prn, Mjd, xSat, vSat, xSun);
-
-        ColumnVector vInert = vSat;
+      // Determine the satellite body frame orientation.
+      //
+      // ATT_NOMINAL: simple nominal Sun-pointing for all systems.
+      // ATT_COMPUTED or ATT_EXTERNAL: use per-system attitude models.
+      //   GLONASS  → yaw-fixed model (Dilssner et al. 2011)
+      //   GPS      → noon/midnight turn model (Kouba 2009/2015)
+      //   others   → simple nominal Sun-pointing
+      // ATT_EXTERNAL: caller supplies the yaw angle [rad] directly
+      //   (velocity-referenced frame, same convention as GLONASS/GPS models).
+      // -----------------------------------------------------------------------
+      bool useVelocityFrame = false;
+      double psiEff = 0.0;
+
+      if (mode == ATT_COMPUTED && prn[0] == 'R' && vSat.size() == 3) {
+        psiEff = glonassYawAngle(prn, Mjd, xSat, vSat, xSun);
+        useVelocityFrame = true;
+      }
+      else if (mode == ATT_COMPUTED && prn[0] == 'G' && vSat.size() == 3
+               && !map->blockType.isEmpty()) {
+        psiEff = gpsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
+        useVelocityFrame = true;
+      }
+      else if (mode == ATT_COMPUTED && prn[0] == 'E' && vSat.size() == 3
+               && !map->blockType.isEmpty()) {
+        psiEff = galileoYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
+        useVelocityFrame = true;
+      }
+      else if (mode == ATT_COMPUTED && prn[0] == 'C' && vSat.size() == 3
+               && !map->blockType.isEmpty()) {
+        psiEff = bdsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
+        useVelocityFrame = true;
+      }
+      else if (mode == ATT_EXTERNAL && vSat.size() == 3) {
+        psiEff = externalYaw;
+        useVelocityFrame = true;
+      }
+
+      if (useVelocityFrame) {
         ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
-        vInert += crossproduct(Omega, xSat);
+        ColumnVector vInert = vSat + crossproduct(Omega, xSat);
 
         ColumnVector sy0 = crossproduct(sz, vInert);
@@ -512,11 +830,13 @@
         ColumnVector sx0 = crossproduct(sy0, sz);
 
-        // Rodrigues rotation of (sx0, sy0) around sz by angle psi
-        double cosY = cos(psi);
-        double sinY = sin(psi);
+        // Rodrigues rotation of (sx0, sy0) around sz by psiEff
+        double cosY = cos(psiEff);
+        double sinY = sin(psiEff);
         sx = sx0 * cosY + crossproduct(sz, sx0) * sinY;
         sy = sy0 * cosY + crossproduct(sz, sy0) * sinY;
       }
       else {
+        // Nominal Sun-pointing: direct formula (ATT_NOMINAL, or computed
+        // with no block-type-specific model available)
         sy = crossproduct(sz, xSun);
         sy /= sqrt(DotProduct(sy,sy));
Index: /trunk/BNC/src/bncantex.h
===================================================================
--- /trunk/BNC/src/bncantex.h	(revision 10954)
+++ /trunk/BNC/src/bncantex.h	(revision 10955)
@@ -46,10 +46,19 @@
   double  rcvCorr(const std::string& antName, t_frequency::type frqType,
                   double eleSat, double azSat, bool& found) const;
+
+  // Attitude model selection for satCoMcorrection().
+  enum e_attMode {
+    ATT_COMPUTED = 0,  // full model: GLONASS yaw-fixed + GPS noon/midnight turn
+    ATT_NOMINAL  = 1,  // simple nominal Sun-pointing only (no maneuver model)
+    ATT_EXTERNAL = 2,  // use caller-supplied externalYaw angle [rad]
+  };
+
   t_irc   satCoMcorrection(const QString& prn, double Mjd,
                            const ColumnVector& xSat, const ColumnVector& vSat,
-                           ColumnVector& dx);
+                           ColumnVector& dx,
+                           e_attMode mode = ATT_COMPUTED,
+                           double externalYaw = 0.0);
 
-  // Drains and returns the diagnostic log accumulated by the GLONASS
-  // yaw-fixed model (mode transitions, with beta/mu/rate at the time).
+  // Drain and return diagnostic logs accumulated by the yaw models.
   // Empty most of the time; only non-empty right after a transition.
   QString takeGlonassYawLog() {
@@ -58,9 +67,18 @@
     return s;
   }
+  QString takeGpsYawLog() {
+    QString s = _gpsYawLog;
+    _gpsYawLog.clear();
+    return s;
+  }
+  QString takeOnYawLog() {
+    QString s = _onYawLog;
+    _onYawLog.clear();
+    return s;
+  }
 
  private:
-  // Per-satellite GLONASS yaw state, used to freeze the yaw angle while
-  // the satellite cannot follow the nominal Sun-pointing attitude law
-  // (see glonassYawAngle() below).
+  // Per-satellite GLONASS yaw state: freezes the yaw angle while the
+  // satellite cannot follow the nominal Sun-pointing law (Dilssner 2011).
   class t_glonassYaw {
    public:
@@ -77,10 +95,64 @@
   };
 
+  // Per-satellite GPS yaw state: rate-limits the yaw during noon/midnight
+  // turns when the required yaw rate exceeds the block's mechanical maximum
+  // (Kouba 2009/2015, Bar-Sever 1996).
+  class t_gpsYaw {
+   public:
+    t_gpsYaw() {
+      yaw         = 0.0;
+      lastCallMjd = 0.0;
+      valid       = false;
+      inTurn      = false;
+    }
+    double yaw;
+    double lastCallMjd;
+    bool   valid;
+    bool   inTurn;  // true while in a constrained noon/midnight turn
+  };
+
+  // Per-satellite Galileo/BDS yaw state: tracks rate-limited rotation between
+  // yaw-steering (psiNom) and orbit-normal mode (psi=0) when |beta| crosses
+  // the constellation-specific threshold (Kouba 2017, Dai et al. 2015).
+  class t_onYaw {
+   public:
+    t_onYaw() {
+      yaw         = 0.0;
+      lastCallMjd = 0.0;
+      valid       = false;
+      inON        = false;
+    }
+    double yaw;
+    double lastCallMjd;
+    bool   valid;
+    bool   inON;  // true while satellite is in (or transitioning to) orbit-normal mode
+  };
+
   QString _glonassYawLog;
+  QString _gpsYawLog;
+  QString _onYawLog;
 
   double glonassYawAngle(const QString& prn, double Mjd, const ColumnVector& xSat,
                           const ColumnVector& vSat, const ColumnVector& xSun);
 
+  double gpsYawAngle(const QString& prn, const QString& blockType, double Mjd,
+                     const ColumnVector& xSat, const ColumnVector& vSat,
+                     const ColumnVector& xSun);
+
+  double onModeYawAngle(const QString& prn, double betaThr, double psiDotMax,
+                        double Mjd, const ColumnVector& xSat,
+                        const ColumnVector& vSat, const ColumnVector& xSun);
+
+  double galileoYawAngle(const QString& prn, const QString& blockType, double Mjd,
+                         const ColumnVector& xSat, const ColumnVector& vSat,
+                         const ColumnVector& xSun);
+
+  double bdsYawAngle(const QString& prn, const QString& blockType, double Mjd,
+                     const ColumnVector& xSat, const ColumnVector& vSat,
+                     const ColumnVector& xSun);
+
   QMap<QString, t_glonassYaw> _glonassYaw;
+  QMap<QString, t_gpsYaw>     _gpsYaw;
+  QMap<QString, t_onYaw>      _onYaw;
 
   class t_frqMap {
@@ -111,4 +183,5 @@
     }
     QString                            antName;
+    QString                            blockType;  // e.g. "IIF", "IIR-M", "IIA", "IIIA" (GPS only)
     double                             zen1;
     double                             zen2;
Index: /trunk/BNC/src/bnchelp.html
===================================================================
--- /trunk/BNC/src/bnchelp.html	(revision 10954)
+++ /trunk/BNC/src/bnchelp.html	(revision 10955)
@@ -5096,4 +5096,19 @@
 Default is an empty option field, meaning that no satellite is excluded from this individual AC.</p>
 <p>
+Use the 'Attitude' field to select how satellite attitude is modelled when converting Antenna Phase Center (APC) corrections to
+Center-of-Mass (CoM) positions required for SP3 output. Three options are available:
+<ul>
+<li><b>Computed</b> (default): BNC applies its own kinematic attitude model:
+GPS noon/midnight turn manoeuvres (Kouba 2009/2015, Bar-Sever 1996),
+GLONASS yaw-fixed mode (Dilssner et al. 2011), and
+Galileo / BDS orbit-normal mode switching (Kouba 2017, Dai et al. 2015, Steigenberger et al. 2018).</li>
+<li><b>Nominal</b>: a simplified, continuous Sun-pointing model is used without any manoeuvre modelling.</li>
+<li><b>SSR</b>: the yaw angle transmitted in the SSR phase bias message is used directly, if present for the satellite and epoch.
+If no yaw angle is available for a particular satellite in a given epoch, BNC falls back to 'Computed'.
+Select this option only if you trust the yaw values provided by the Analysis Center.</li>
+</ul>
+Note that the attitude model affects APC-referenced correction streams only.
+For CoM-referenced streams (SSRC) the Analysis Center has already applied its own attitude model before encoding.</p>
+<p>
 Note that the orbit information in the resulting combination stream is just copied from one of the incoming streams.
 The stream used for providing the orbits may vary over time: if the orbit providing stream has an outage
Index: /trunk/BNC/src/bncwindow.cpp
===================================================================
--- /trunk/BNC/src/bncwindow.cpp	(revision 10954)
+++ /trunk/BNC/src/bncwindow.cpp	(revision 10955)
@@ -472,13 +472,14 @@
   // Combine Corrections
   // -------------------
-  _cmbTable = new QTableWidget(0, 4);
-  _cmbTable->setHorizontalHeaderLabels(QString("Mountpoint, AC Name, Weight Factor, Exclude Satellites").split(","));
+  _cmbTable = new QTableWidget(0, 5);
+  _cmbTable->setHorizontalHeaderLabels(QString("Mountpoint, AC Name, Weight Factor, Exclude Satellites, Attitude").split(","));
   _cmbTable->setSelectionMode(QAbstractItemView::ExtendedSelection);
   _cmbTable->setSelectionBehavior(QAbstractItemView::SelectRows);
-  _cmbTable->setMaximumWidth(40 * ww);
+  _cmbTable->setMaximumWidth(50 * ww);
   _cmbTable->horizontalHeader()->resizeSection(0, 10 * ww);
   _cmbTable->horizontalHeader()->resizeSection(1, 6 * ww);
   _cmbTable->horizontalHeader()->resizeSection(2, 9 * ww);
   _cmbTable->horizontalHeader()->resizeSection(3, 9 * ww);
+  _cmbTable->horizontalHeader()->resizeSection(4, 8 * ww);
 #if QT_VERSION < 0x050000
   _cmbTable->horizontalHeader()->setResizeMode(QHeaderView::Interactive);
@@ -1701,5 +1702,5 @@
   // WhatsThis, Combine Corrections
   // ------------------------------
-  _cmbTable->setWhatsThis(tr("<p>BNC allows to process several orbit and clock correction streams in real-time to produce, encode, upload and save a combination of correctors coming from different providers. </p><p>To add a line to the 'Combine Corrections' table hit the 'Add Row' button, double click on the 'Mountpoint' field to specify a Broadcast Ephemeris Correction mountpoint from the 'Streams' section below and hit Enter. Then double click on the 'AC Name' field to enter your choice of an abbreviation for the Analysis Center (AC) providing the stream. Double click on the 'Weight Factor' field to enter a weight factor to be applied for this stream in the combination. A Factor greater than 1 will enlarge the sigma of the clock pseudo-observations and with it down-weight its contribution. Finally, double click on the 'Exclude Satellites' field and specify satellites, to exclude them for an individual AC. An entry 'G04,G31,R' means to excludes GPS satellites PRN 4 and 31 as well as all GLONASS satellites from one individual AC. Default is an empty option field, meaning that no satellite is excluded from this individual AC.</p><p>Note that the orbit information in the resulting combination stream is just copied from one of the incoming streams. The stream used for providing the orbits may vary over time: if the orbit providing stream has an outage then BNC switches to the next remaining stream for getting hold of the orbit information.</p><p>The combination process requires Broadcast Ephemeris. Besides orbit and clock correction streams BNC should therefore pull a stream carrying Broadcast Ephemeris in the form of RTCM Version 3 messages.</p><p>It is possible to specify only one Broadcast Ephemeris Correction stream in the 'Combine Corrections' table. Instead of combining corrections BNC will then add the corrections to the Broadcast Ephemeris with the possibility to save final orbit and clock results in SP3 and/or Clock RINEX format. <i>[key: cmbStreams]</i></p>"));
+  _cmbTable->setWhatsThis(tr("<p>BNC allows to process several orbit and clock correction streams in real-time to produce, encode, upload and save a combination of correctors coming from different providers. </p><p>To add a line to the 'Combine Corrections' table hit the 'Add Row' button, double click on the 'Mountpoint' field to specify a Broadcast Ephemeris Correction mountpoint from the 'Streams' section below and hit Enter. Then double click on the 'AC Name' field to enter your choice of an abbreviation for the Analysis Center (AC) providing the stream. Double click on the 'Weight Factor' field to enter a weight factor to be applied for this stream in the combination. A Factor greater than 1 will enlarge the sigma of the clock pseudo-observations and with it down-weight its contribution. Finally, double click on the 'Exclude Satellites' field and specify satellites, to exclude them for an individual AC. An entry 'G04,G31,R' means to excludes GPS satellites PRN 4 and 31 as well as all GLONASS satellites from one individual AC. Default is an empty option field, meaning that no satellite is excluded from this individual AC.</p><p>Use the 'Attitude' field to select how satellite attitude is modelled when converting Antenna Phase Center (APC) corrections to Center-of-Mass (CoM) positions for SP3 output. 'Computed' (default) applies BNC's kinematic attitude model, including GPS noon/midnight turn manoeuvres, GLONASS yaw-fixed mode, and Galileo/BDS orbit-normal mode switching. 'Nominal' uses a simplified continuous Sun-pointing model without manoeuvre modelling. 'SSR' uses the yaw angle transmitted in the SSR phase bias message; select this option only if you trust the yaw values provided by the AC. If no yaw angle is available for a particular satellite in a given epoch, BNC falls back to 'Computed'. Note that the attitude model affects APC-referenced streams only; for CoM-referenced streams the AC has already applied attitude before encoding.</p><p>Note that the orbit information in the resulting combination stream is just copied from one of the incoming streams. The stream used for providing the orbits may vary over time: if the orbit providing stream has an outage then BNC switches to the next remaining stream for getting hold of the orbit information.</p><p>The combination process requires Broadcast Ephemeris. Besides orbit and clock correction streams BNC should therefore pull a stream carrying Broadcast Ephemeris in the form of RTCM Version 3 messages.</p><p>It is possible to specify only one Broadcast Ephemeris Correction stream in the 'Combine Corrections' table. Instead of combining corrections BNC will then add the corrections to the Broadcast Ephemeris with the possibility to save final orbit and clock results in SP3 and/or Clock RINEX format. <i>[key: cmbStreams]</i></p>"));
   addCmbRowButton->setWhatsThis(tr("<p>Hit 'Add Row' button to add another line to the 'Combine Corrections' table.</p>"));
   delCmbRowButton->setWhatsThis(tr("<p>Hit 'Delete' button to delete the highlighted line(s) from the 'Combine Corrections' table.</p>"));
@@ -2218,5 +2219,9 @@
     QString hlp;
     for (int iCol = 0; iCol < _cmbTable->columnCount(); iCol++) {
-      if (_cmbTable->item(iRow, iCol)) {
+      if (iCol == 4) {
+        QComboBox* attCombo = qobject_cast<QComboBox*>(_cmbTable->cellWidget(iRow, iCol));
+        hlp += (attCombo ? attCombo->currentText() : "Computed") + " ";
+      }
+      else if (_cmbTable->item(iRow, iCol)) {
         hlp += _cmbTable->item(iRow, iCol)->text() + " ";
       }
@@ -3056,5 +3061,14 @@
   _cmbTable->insertRow(iRow);
   for (int iCol = 0; iCol < _cmbTable->columnCount(); iCol++) {
-    _cmbTable->setItem(iRow, iCol, new QTableWidgetItem(""));
+    if (iCol == 4) {
+      QComboBox* attCombo = new QComboBox();
+      attCombo->setEditable(false);
+      attCombo->addItems(QString("Computed,SSR,Nominal").split(","));
+      attCombo->setFrame(false);
+      _cmbTable->setCellWidget(iRow, iCol, attCombo);
+    }
+    else {
+      _cmbTable->setItem(iRow, iCol, new QTableWidgetItem(""));
+    }
   }
 }
@@ -3110,6 +3124,26 @@
       _cmbTable->insertRow(iRow);
     }
-    for (int iCol = 0; iCol < hlp.size(); iCol++) {
-      _cmbTable->setItem(iRow, iCol, new QTableWidgetItem(hlp[iCol]));
+    for (int iCol = 0; iCol < hlp.size() && iCol < _cmbTable->columnCount(); iCol++) {
+      if (iCol == 4) {
+        QComboBox* attCombo = new QComboBox();
+        attCombo->setEditable(false);
+        attCombo->addItems(QString("Computed,SSR,Nominal").split(","));
+        attCombo->setFrame(false);
+        int idx = attCombo->findText(hlp[iCol]);
+        if (idx != -1) attCombo->setCurrentIndex(idx);
+        _cmbTable->setCellWidget(iRow, iCol, attCombo);
+      }
+      else {
+        _cmbTable->setItem(iRow, iCol, new QTableWidgetItem(hlp[iCol]));
+      }
+    }
+    // Ensure the Attitude combo exists even for old configs without column 4
+    if (hlp.size() <= 4 && _cmbTable->columnCount() > 4 && iRow >= 0
+        && !_cmbTable->cellWidget(iRow, 4)) {
+      QComboBox* attCombo = new QComboBox();
+      attCombo->setEditable(false);
+      attCombo->addItems(QString("Computed,SSR,Nominal").split(","));
+      attCombo->setFrame(false);
+      _cmbTable->setCellWidget(iRow, 4, attCombo);
     }
   }
Index: /trunk/BNC/src/combination/bnccomb.cpp
===================================================================
--- /trunk/BNC/src/combination/bnccomb.cpp	(revision 10954)
+++ /trunk/BNC/src/combination/bnccomb.cpp	(revision 10955)
@@ -181,9 +181,10 @@
       QStringList hlp = it.next().split(" ");
       cmbAC* newAC = new cmbAC();
-      newAC->mountPoint   = hlp[0];
-      newAC->name         = hlp[1];
-      newAC->weightFactor = hlp[2].toDouble();
-      newAC->excludeSats  = hlp[3].split(QRegExp("[ ,]"), Qt::SkipEmptyParts);
-      newAC->isAPC        = bool(newAC->mountPoint.mid(0,4) == "SSRA");
+      newAC->mountPoint     = hlp[0];
+      newAC->name           = hlp[1];
+      newAC->weightFactor   = hlp[2].toDouble();
+      newAC->excludeSats    = hlp[3].split(QRegExp("[ ,]"), Qt::SkipEmptyParts);
+      newAC->isAPC          = bool(newAC->mountPoint.mid(0,4) == "SSRA");
+      newAC->attitudeSource = (hlp.size() > 4) ? hlp[4] : "Computed";
       QMapIterator<char, unsigned> itSys(_cmbSysPrn);
       // init
@@ -859,5 +860,6 @@
           dt -= (0.5 * ssrUpdateInt[pb._updateInt]);
         }
-        _newCorr->_satYawAngle = pb._yaw + pb._yawRate * dt;
+        _newCorr->_satYawAngle      = pb._yaw + pb._yawRate * dt;
+        _newCorr->_satYawAngleValid = true;
 
         // _lambdaIF
@@ -1213,6 +1215,5 @@
       if (corr->_prn.startsWith("R08")) {
         emit newMessage(("bncComb: DIAG " + corr->_prn.mid(0,3) + " getCrd FAILED").toLatin1(), false);
-      }
-      */
+      } */
       delete corr;
       it.remove();
@@ -1222,7 +1223,5 @@
     else if (corr->_prn.startsWith("R08")) {
       emit newMessage(("bncComb: DIAG " + corr->_prn.mid(0,3) + " getCrd OK").toLatin1(), false);
-    }
-    
-    */
+    }*/
     // TEMPORARY DIAGNOSTIC: how far is this epoch from the GLONASS broadcast
     // ephemeris reference time? t_ephGlo::position() numerically integrates
@@ -1251,5 +1250,24 @@
       char sys = corr->_eph->prn().system();
       masterIsAPC = _masterIsAPC[sys];
-      if (_antex->satCoMcorrection(corr->_prn, Mjd, xc.Rows(1,3), vv, dx) != success) {
+
+      // Determine attitude mode from the master orbit AC for this system
+      bncAntex::e_attMode attMode   = bncAntex::ATT_COMPUTED;
+      double              extYaw    = 0.0;
+      const QString&      masterName = _masterOrbitAC[sys];
+      for (const cmbAC* ac : _ACs) {
+        if (ac->name == masterName) {
+          if (ac->attitudeSource == "SSR" && corr->_satYawAngleValid) {
+            attMode = bncAntex::ATT_EXTERNAL;
+            extYaw  = corr->_satYawAngle;
+          }
+          else if (ac->attitudeSource == "Nominal") {
+            attMode = bncAntex::ATT_NOMINAL;
+          }
+          break;
+        }
+      }
+
+      if (_antex->satCoMcorrection(corr->_prn, Mjd, xc.Rows(1,3), vv, dx,
+                                   attMode, extYaw) != success) {
         dx = 0;
         emit newMessage("bncComb: antenna not found " + corr->_prn.mid(0,3).toLatin1(), false);
@@ -1258,4 +1276,12 @@
       if (!glonassYawLog.isEmpty()) {
         emit newMessage(("bncComb: " + glonassYawLog.trimmed()).toLatin1(), false);
+      }
+      QString gpsYawLog = _antex->takeGpsYawLog();
+      if (!gpsYawLog.isEmpty()) {
+        emit newMessage(("bncComb: " + gpsYawLog.trimmed()).toLatin1(), false);
+      }
+      QString onYawLog = _antex->takeOnYawLog();
+      if (!onYawLog.isEmpty()) {
+        emit newMessage(("bncComb: " + onYawLog.trimmed()).toLatin1(), false);
       }
     }
Index: /trunk/BNC/src/combination/bnccomb.h
===================================================================
--- /trunk/BNC/src/combination/bnccomb.h	(revision 10954)
+++ /trunk/BNC/src/combination/bnccomb.h	(revision 10955)
@@ -95,5 +95,6 @@
       numObs['S']  = 0;
       numObs['I']  = 0;
-      isAPC = false;
+      isAPC          = false;
+      attitudeSource = "Computed";
     }
     ~cmbAC() {
@@ -105,4 +106,5 @@
     QStringList          excludeSats;
     bool                 isAPC;
+    QString              attitudeSource; // "Computed", "SSR", or "Nominal"
     QMap<char, unsigned> numObs;
   };
@@ -117,4 +119,5 @@
       _lambdaIF                    = 0.0;
       _satYawAngle                 = 0.0;
+      _satYawAngleValid            = false;
       _weightFactor                = 1.0;
       _satPos.ReSize(3); _satPos   = 0.0;
@@ -135,4 +138,5 @@
     double         _lambdaIF;
     double         _satYawAngle;
+    bool           _satYawAngleValid;
     double         _dClkResult;
     ColumnVector   _satPos;
