Changeset 10955 in ntrip for trunk/BNC/src/bncantex.cpp
- Timestamp:
- Jul 3, 2026, 11:10:42 AM (26 hours ago)
- File:
-
- 1 edited
-
trunk/BNC/src/bncantex.cpp (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/BNC/src/bncantex.cpp
r10951 r10955 192 192 line.indexOf("NavIC") == 0 ){ 193 193 newAntMap->antName = line.mid(20,3); 194 if (line.indexOf("BLOCK I") == 0) { 195 // Extract GPS block type: "BLOCK IIF " → "IIF" 196 QString bt = line.mid(0, 20).trimmed(); 197 if (bt.startsWith("BLOCK ")) 198 newAntMap->blockType = bt.mid(6); 199 } 200 else if (line.indexOf("GALILEO") == 0) { 201 // "GALILEO-1" → "1" (IOV), "GALILEO-2" → "2" (FOC) 202 QString bt = line.mid(0, 20).trimmed(); 203 if (bt.startsWith("GALILEO-")) 204 newAntMap->blockType = bt.mid(8); 205 } 206 else if (line.indexOf("BEIDOU") == 0) { 207 // "BEIDOU-2M" → "2M", "BEIDOU-3M-CAST" → "3M-CAST", etc. 208 QString bt = line.mid(0, 20).trimmed(); 209 if (bt.startsWith("BEIDOU-")) 210 newAntMap->blockType = bt.mid(7); 211 } 194 212 } 195 213 else { … … 448 466 } 449 467 468 // GPS satellite yaw angle during nominal tracking and noon/midnight turns. 469 // 470 // GPS satellites perform a "noon turn" (and, for some blocks, a "midnight 471 // turn") when the Sun's elevation angle above the orbital plane (beta) is 472 // small: the required nominal yaw rate then exceeds the satellite's 473 // mechanical maximum, so the satellite yaws at that maximum rate until it 474 // catches back up to the nominal Sun-pointing orientation (Kouba 2009/2015, 475 // Bar-Sever 1996). 476 // 477 // The max yaw rates below are the best published estimates per block type: 478 // IIA: 0.12 °/s (Kouba 2009) 479 // IIR: 0.20 °/s (Bar-Sever 1996) 480 // IIR-M: 0.20 °/s 481 // IIF: 0.11 °/s (Kouba 2015) 482 // IIIA: 0.15 °/s (tentative) 483 // 484 // Returns the effective yaw angle [rad] in the velocity-referenced frame, 485 // for use with the same Rodrigues rotation as the GLONASS model. During 486 // nominal tracking this equals psiNom and the result is identical to the 487 // simple sz×xSun formula. 488 //////////////////////////////////////////////////////////////////////////// 489 double bncAntex::gpsYawAngle(const QString& prn, const QString& blockType, 490 double Mjd, 491 const ColumnVector& xSat, 492 const ColumnVector& vSat, 493 const ColumnVector& xSun) { 494 495 // Max yaw rate [rad/s] by GPS block type 496 double psiDotMax; 497 if (blockType == "IIA") psiDotMax = 0.12 * M_PI / 180.0; 498 else if (blockType == "IIR") psiDotMax = 0.20 * M_PI / 180.0; 499 else if (blockType == "IIR-M") psiDotMax = 0.20 * M_PI / 180.0; 500 else if (blockType == "IIF") psiDotMax = 0.11 * M_PI / 180.0; 501 else if (blockType == "IIIA") psiDotMax = 0.15 * M_PI / 180.0; 502 else return 0.0; // unknown block: caller uses simple Sun-pointing 503 504 const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 min in days 505 506 // Inertial velocity 507 ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega; 508 ColumnVector vInert = vSat + crossproduct(Omega, xSat); 509 510 // Orbital angular momentum vector → orbital rate 511 ColumnVector h = crossproduct(xSat, vInert); 512 double hNorm = sqrt(DotProduct(h, h)); 513 ColumnVector orbNormal = h / hNorm; 514 double r = sqrt(DotProduct(xSat, xSat)); 515 double nRate = hNorm / (r * r); // [rad/s] 516 517 // Beta angle 518 double beta = asin(DotProduct(orbNormal, xSun)); 519 520 // Mu: orbit angle from midnight (same geometry as GLONASS) 521 ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal; 522 sunProj /= sqrt(DotProduct(sunProj, sunProj)); 523 ColumnVector eX = -1.0 * sunProj; // midnight direction 524 ColumnVector eY = crossproduct(orbNormal, eX); 525 ColumnVector rHat = xSat / r; 526 double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX)); 527 528 // Nominal yaw and its rate 529 double tanBeta = tan(beta); 530 double sinMu = sin(mu); 531 double psiNom = atan2(-tanBeta, sinMu); 532 double denom = tanBeta * tanBeta + sinMu * sinMu; 533 // |dPsi/dt| (always non-negative, sign comes from sign of tanBeta*cos(mu)) 534 double psiDotNomAbs = (denom > 1e-12) 535 ? nRate * fabs(tanBeta * cos(mu)) / denom 536 : 1e9; 537 // Sign of the nominal yaw rate: dPsi/dt = nRate * tanBeta * cos(mu) / denom 538 double psiDotNomSign = (tanBeta * cos(mu) >= 0.0) ? 1.0 : -1.0; 539 540 t_gpsYaw& st = _gpsYaw[prn]; 541 double callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0; 542 543 // Stale state: reset if there has been a gap in calls 544 if (st.valid && callGap > MAX_CALL_GAP) { 545 _gpsYawLog += QString().asprintf( 546 "%s gpsYaw STALE-RESET Mjd=%.6f beta=%6.3f mu=%7.2f" 547 " old=%7.2f new=%7.2f gap=%.1fmin\n", 548 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI, 549 st.yaw*180.0/M_PI, psiNom*180.0/M_PI, callGap*1440.0); 550 st.valid = false; 551 st.inTurn = false; 552 } 553 554 double psiEff; 555 if (psiDotNomAbs > psiDotMax) { 556 // Constrained: satellite yaws at psiDotMax in the nominal direction 557 if (!st.valid || !st.inTurn) { 558 // Entering a noon/midnight turn 559 psiEff = st.valid ? st.yaw : psiNom; 560 _gpsYawLog += QString().asprintf( 561 "%s gpsYaw TURN-ENTER Mjd=%.6f beta=%6.3f mu=%7.2f" 562 " psiNom=%7.2f psiEff=%7.2f psiDotNom=%6.3f max=%5.3f\n", 563 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI, 564 psiNom*180.0/M_PI, psiEff*180.0/M_PI, 565 psiDotNomAbs*psiDotNomSign*180.0/M_PI, psiDotMax*180.0/M_PI); 566 st.inTurn = true; 567 } else { 568 // Continuing the turn: integrate at constrained rate 569 double dt = callGap * 86400.0; // [s] 570 psiEff = st.yaw + psiDotNomSign * psiDotMax * dt; 571 } 572 // Wrap to [-pi, pi] 573 while (psiEff > M_PI) psiEff -= 2.0 * M_PI; 574 while (psiEff < -M_PI) psiEff += 2.0 * M_PI; 575 } 576 else { 577 // Nominal tracking 578 psiEff = psiNom; 579 if (st.inTurn) { 580 _gpsYawLog += QString().asprintf( 581 "%s gpsYaw TURN-EXIT Mjd=%.6f beta=%6.3f mu=%7.2f" 582 " frozen=%7.2f nominal=%7.2f\n", 583 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI, 584 st.yaw*180.0/M_PI, psiNom*180.0/M_PI); 585 st.inTurn = false; 586 } 587 st.yaw = psiNom; 588 } 589 590 st.yaw = psiEff; 591 st.lastCallMjd = Mjd; 592 st.valid = true; 593 594 return psiEff; 595 } 596 597 // 598 // Orbit-Normal Mode Yaw Angle — shared model for Galileo and BDS 599 // 600 // When |beta| < betaThr the satellite rotates toward yaw = 0 (orbit-normal) 601 // at the block-specific maximum yaw rate. When |beta| >= betaThr it returns 602 // to nominal yaw-steering (psiNom). Transitions are rate-limited in both 603 // directions to match physical satellite behaviour. 604 // 605 // References: 606 // Galileo IOV/FOC: Kouba (2017), Steigenberger et al. (2018) 607 // BDS MEO/IGSO: Dai et al. (2015), Wang et al. (2018) 608 //////////////////////////////////////////////////////////////////////////// 609 double bncAntex::onModeYawAngle(const QString& prn, 610 double betaThr, double psiDotMax, double Mjd, 611 const ColumnVector& xSat, 612 const ColumnVector& vSat, 613 const ColumnVector& xSun) { 614 615 const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 min [days] 616 617 // Inertial velocity 618 ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega; 619 ColumnVector vInert = vSat + crossproduct(Omega, xSat); 620 621 // Orbit geometry 622 ColumnVector h = crossproduct(xSat, vInert); 623 double hNorm = sqrt(DotProduct(h, h)); 624 ColumnVector orbNormal = h / hNorm; 625 double r = sqrt(DotProduct(xSat, xSat)); 626 627 double beta = asin(DotProduct(orbNormal, xSun)); 628 629 ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal; 630 sunProj /= sqrt(DotProduct(sunProj, sunProj)); 631 ColumnVector eX = -1.0 * sunProj; 632 ColumnVector eY = crossproduct(orbNormal, eX); 633 ColumnVector rHat = xSat / r; 634 double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX)); 635 double psiNom = atan2(-tan(beta), sin(mu)); 636 637 // Target yaw: orbit-normal (0) when below threshold, nominal otherwise 638 bool wantsON = (fabs(beta) < betaThr); 639 double psiTarget = wantsON ? 0.0 : psiNom; 640 641 t_onYaw& st = _onYaw[prn]; 642 double callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0; 643 644 if (st.valid && callGap > MAX_CALL_GAP) { 645 _onYawLog += QString().asprintf( 646 "%s onYaw STALE-RESET Mjd=%.6f beta=%5.2f gap=%.1fmin\n", 647 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, callGap*1440.0); 648 st.valid = false; 649 } 650 651 double psiEff; 652 if (!st.valid) { 653 // Cold start: initialise at psiNom regardless of mode 654 psiEff = psiNom; 655 st.inON = false; 656 } 657 else { 658 double dt = callGap * 86400.0; // [s] 659 double dpsi = psiTarget - st.yaw; 660 while (dpsi > M_PI) dpsi -= 2.0 * M_PI; 661 while (dpsi < -M_PI) dpsi += 2.0 * M_PI; 662 double maxDpsi = psiDotMax * dt; 663 664 if (fabs(dpsi) <= maxDpsi) { 665 // Target reached this step 666 psiEff = psiTarget; 667 bool wasON = st.inON; 668 st.inON = wantsON; 669 if (!wasON && wantsON && fabs(st.yaw) < 0.5*M_PI/180.0) { 670 _onYawLog += QString().asprintf( 671 "%s onYaw ON-ENTER Mjd=%.6f beta=%5.2f psiNom=%7.2f\n", 672 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI); 673 } 674 else if (wasON && !wantsON) { 675 _onYawLog += QString().asprintf( 676 "%s onYaw ON-EXIT Mjd=%.6f beta=%5.2f psiNom=%7.2f\n", 677 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI); 678 st.inON = false; 679 } 680 } 681 else { 682 // Still rotating toward target 683 psiEff = st.yaw + (dpsi > 0.0 ? 1.0 : -1.0) * maxDpsi; 684 } 685 } 686 687 while (psiEff > M_PI) psiEff -= 2.0 * M_PI; 688 while (psiEff < -M_PI) psiEff += 2.0 * M_PI; 689 690 st.yaw = psiEff; 691 st.lastCallMjd = Mjd; 692 st.valid = true; 693 694 return psiEff; 695 } 696 697 // 698 //////////////////////////////////////////////////////////////////////////// 699 double bncAntex::galileoYawAngle(const QString& prn, const QString& blockType, 700 double Mjd, 701 const ColumnVector& xSat, 702 const ColumnVector& vSat, 703 const ColumnVector& xSun) { 704 // IOV (type "1") and FOC (type "2"): orbit-normal for |beta| < 2 deg. 705 // Max yaw rate 0.20 deg/s applies to both generations. 706 // Kouba (2017), Steigenberger et al. (2018) 707 Q_UNUSED(blockType); 708 const double betaThr = 2.0 * M_PI / 180.0; 709 const double psiDotMax = 0.20 * M_PI / 180.0; 710 return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun); 711 } 712 713 // 714 //////////////////////////////////////////////////////////////////////////// 715 double bncAntex::bdsYawAngle(const QString& prn, const QString& blockType, 716 double Mjd, 717 const ColumnVector& xSat, 718 const ColumnVector& vSat, 719 const ColumnVector& xSun) { 720 // GEO satellites are always in orbit-normal mode (yaw = 0). 721 if (blockType == "2G" || blockType == "3G-CAST") 722 return 0.0; 723 724 double betaThr, psiDotMax; 725 if (blockType == "2M" || blockType == "2I") { 726 // BDS-2 MEO / IGSO: orbit-normal for |beta| < 4 deg (Dai et al. 2015) 727 betaThr = 4.0 * M_PI / 180.0; 728 psiDotMax = 0.10 * M_PI / 180.0; 729 } 730 else { 731 // BDS-3 (CAST and SECM MEO/IGSO): orbit-normal for |beta| < 3 deg 732 betaThr = 3.0 * M_PI / 180.0; 733 psiDotMax = 0.15 * M_PI / 180.0; 734 } 735 return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun); 736 } 737 738 // 450 739 // Satellite Antenna Offset 451 740 //////////////////////////////////////////////////////////////////////////// 452 741 t_irc bncAntex::satCoMcorrection(const QString& prn, double Mjd, 453 742 const ColumnVector& xSat, 454 const ColumnVector& vSat, ColumnVector& dx) { 743 const ColumnVector& vSat, ColumnVector& dx, 744 e_attMode mode, double externalYaw) { 455 745 456 746 t_frequency::type frqType = t_frequency::dummy; … … 495 785 ColumnVector sy, sx; 496 786 497 // GLONASS: override the nominal Sun-pointing attitude near the 498 // orbit noon/midnight points when the beta angle is small (see 499 // glonassYawAngle() above). Elsewhere GLONASS-M follows the same 500 // nominal law as the other constellations, so the result is 501 // identical to the direct Sun-pointing computation used below. 502 // ----------------------------------------------------------------- 503 if (prn[0] == 'R' && vSat.size() == 3) { 504 double psi = glonassYawAngle(prn, Mjd, xSat, vSat, xSun); 505 506 ColumnVector vInert = vSat; 787 // Determine the satellite body frame orientation. 788 // 789 // ATT_NOMINAL: simple nominal Sun-pointing for all systems. 790 // ATT_COMPUTED or ATT_EXTERNAL: use per-system attitude models. 791 // GLONASS → yaw-fixed model (Dilssner et al. 2011) 792 // GPS → noon/midnight turn model (Kouba 2009/2015) 793 // others → simple nominal Sun-pointing 794 // ATT_EXTERNAL: caller supplies the yaw angle [rad] directly 795 // (velocity-referenced frame, same convention as GLONASS/GPS models). 796 // ----------------------------------------------------------------------- 797 bool useVelocityFrame = false; 798 double psiEff = 0.0; 799 800 if (mode == ATT_COMPUTED && prn[0] == 'R' && vSat.size() == 3) { 801 psiEff = glonassYawAngle(prn, Mjd, xSat, vSat, xSun); 802 useVelocityFrame = true; 803 } 804 else if (mode == ATT_COMPUTED && prn[0] == 'G' && vSat.size() == 3 805 && !map->blockType.isEmpty()) { 806 psiEff = gpsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun); 807 useVelocityFrame = true; 808 } 809 else if (mode == ATT_COMPUTED && prn[0] == 'E' && vSat.size() == 3 810 && !map->blockType.isEmpty()) { 811 psiEff = galileoYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun); 812 useVelocityFrame = true; 813 } 814 else if (mode == ATT_COMPUTED && prn[0] == 'C' && vSat.size() == 3 815 && !map->blockType.isEmpty()) { 816 psiEff = bdsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun); 817 useVelocityFrame = true; 818 } 819 else if (mode == ATT_EXTERNAL && vSat.size() == 3) { 820 psiEff = externalYaw; 821 useVelocityFrame = true; 822 } 823 824 if (useVelocityFrame) { 507 825 ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega; 508 vInert +=crossproduct(Omega, xSat);826 ColumnVector vInert = vSat + crossproduct(Omega, xSat); 509 827 510 828 ColumnVector sy0 = crossproduct(sz, vInert); … … 512 830 ColumnVector sx0 = crossproduct(sy0, sz); 513 831 514 // Rodrigues rotation of (sx0, sy0) around sz by angle psi515 double cosY = cos(psi); 516 double sinY = sin(psi); 832 // Rodrigues rotation of (sx0, sy0) around sz by psiEff 833 double cosY = cos(psiEff); 834 double sinY = sin(psiEff); 517 835 sx = sx0 * cosY + crossproduct(sz, sx0) * sinY; 518 836 sy = sy0 * cosY + crossproduct(sz, sy0) * sinY; 519 837 } 520 838 else { 839 // Nominal Sun-pointing: direct formula (ATT_NOMINAL, or computed 840 // with no block-type-specific model available) 521 841 sy = crossproduct(sz, xSun); 522 842 sy /= sqrt(DotProduct(sy,sy));
Note:
See TracChangeset
for help on using the changeset viewer.
