gnss-js 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,7 @@ import {
24
24
  crc24q
25
25
  } from "./chunk-YUW33VXK.js";
26
26
  import {
27
+ GPS_PI,
27
28
  GpsLnavAssembler,
28
29
  getBitS,
29
30
  getBitU,
@@ -575,6 +576,173 @@ function parseSbfGpsNav(data) {
575
576
  return { ephemerides, badParity, messages };
576
577
  }
577
578
 
579
+ // src/navbits/navic.ts
580
+ var GPS_EPOCH_MS3 = Date.UTC(1980, 0, 6);
581
+ var SEC_PER_WEEK2 = 604800;
582
+ var DATA = 30;
583
+ function navicSubframeCrcOk(sf) {
584
+ return crc24q(sf, 262) === getBitU(sf, 262, 24);
585
+ }
586
+ function navicSubframeId(sf) {
587
+ return getBitU(sf, 27, 2) + 1;
588
+ }
589
+ function decodeNavicSubframes(sf1, sf2, opts = {}) {
590
+ let i = DATA;
591
+ getBitU(sf1, i, 10);
592
+ i += 10;
593
+ const af0 = getBitS(sf1, i, 22) * 2 ** -31;
594
+ i += 22;
595
+ const af1 = getBitS(sf1, i, 16) * 2 ** -43;
596
+ i += 16;
597
+ const af2 = getBitS(sf1, i, 8) * 2 ** -55;
598
+ i += 8;
599
+ i += 4;
600
+ const tocSec = getBitU(sf1, i, 16) * 16;
601
+ i += 16;
602
+ const tgd = getBitS(sf1, i, 8) * 2 ** -31;
603
+ i += 8;
604
+ const deltaN = getBitS(sf1, i, 22) * 2 ** -41 * GPS_PI;
605
+ i += 22;
606
+ const iode = getBitU(sf1, i, 8);
607
+ i += 8;
608
+ i += 10;
609
+ const l5Health = getBitU(sf1, i, 1);
610
+ i += 1;
611
+ const sHealth = getBitU(sf1, i, 1);
612
+ i += 1;
613
+ const cuc = getBitS(sf1, i, 15) * 2 ** -28;
614
+ i += 15;
615
+ const cus = getBitS(sf1, i, 15) * 2 ** -28;
616
+ i += 15;
617
+ const cic = getBitS(sf1, i, 15) * 2 ** -28;
618
+ i += 15;
619
+ const cis = getBitS(sf1, i, 15) * 2 ** -28;
620
+ i += 15;
621
+ const crc = getBitS(sf1, i, 15) * 2 ** -4;
622
+ i += 15;
623
+ const crs = getBitS(sf1, i, 15) * 2 ** -4;
624
+ i += 15;
625
+ const idot = getBitS(sf1, i, 14) * 2 ** -43 * GPS_PI;
626
+ let j = DATA;
627
+ const m0 = getBitS(sf2, j, 32) * 2 ** -31 * GPS_PI;
628
+ j += 32;
629
+ const toeSec = getBitU(sf2, j, 16) * 16;
630
+ j += 16;
631
+ const e = getBitU(sf2, j, 32) * 2 ** -33;
632
+ j += 32;
633
+ const sqrtA = getBitU(sf2, j, 32) * 2 ** -19;
634
+ j += 32;
635
+ const omega0 = getBitS(sf2, j, 32) * 2 ** -31 * GPS_PI;
636
+ j += 32;
637
+ const omega = getBitS(sf2, j, 32) * 2 ** -31 * GPS_PI;
638
+ j += 32;
639
+ const omegaDot = getBitS(sf2, j, 22) * 2 ** -41 * GPS_PI;
640
+ j += 22;
641
+ const i0 = getBitS(sf2, j, 32) * 2 ** -31 * GPS_PI;
642
+ if (sqrtA <= 0 || e >= 1) return null;
643
+ const week = opts.refWeek ?? Math.floor((Date.now() - GPS_EPOCH_MS3) / 1e3 / SEC_PER_WEEK2);
644
+ const prn = opts.prn ?? "I00";
645
+ const tocDate = new Date(
646
+ GPS_EPOCH_MS3 + (week * SEC_PER_WEEK2 + tocSec) * 1e3
647
+ );
648
+ return {
649
+ system: "I",
650
+ prn,
651
+ tocDate,
652
+ toc: tocDate.getTime() / 1e3 % SEC_PER_WEEK2,
653
+ af0,
654
+ af1,
655
+ af2,
656
+ iode,
657
+ crs,
658
+ deltaN,
659
+ m0,
660
+ cuc,
661
+ e,
662
+ cus,
663
+ sqrtA,
664
+ toe: toeSec,
665
+ cic,
666
+ omega0,
667
+ cis,
668
+ i0,
669
+ crc,
670
+ omega,
671
+ omegaDot,
672
+ idot,
673
+ week,
674
+ svHealth: l5Health << 1 | sHealth,
675
+ tgd
676
+ };
677
+ }
678
+ var NavicAssembler = class {
679
+ sf1 = /* @__PURE__ */ new Map();
680
+ last = /* @__PURE__ */ new Map();
681
+ /**
682
+ * Push one CRC-valid subframe. Subframe 1 opens a master frame (subframe 2
683
+ * immediately follows it in the NavIC frame order), so an ephemeris is only
684
+ * emitted when a subframe 2 pairs with the subframe 1 that opened the *same*
685
+ * frame — never a stale subframe 1 left over across CRC drops.
686
+ */
687
+ push(prn, subframe, refWeek) {
688
+ if (getBitU(subframe, 25, 1)) return null;
689
+ const id = navicSubframeId(subframe);
690
+ if (id === 1) {
691
+ this.sf1.set(prn, subframe);
692
+ return null;
693
+ }
694
+ if (id !== 2) return null;
695
+ const a = this.sf1.get(prn);
696
+ if (!a) return null;
697
+ this.sf1.delete(prn);
698
+ const eph = decodeNavicSubframes(a, subframe, { prn, refWeek });
699
+ if (!eph) return null;
700
+ const key = `${eph.toc}|${eph.iode}`;
701
+ if (this.last.get(prn) === key) return null;
702
+ this.last.set(prn, key);
703
+ return eph;
704
+ }
705
+ };
706
+
707
+ // src/sbf/rawnav-navic.ts
708
+ var WNC_DNU2 = 65535;
709
+ function extractSbfNavicSubframe(view, b) {
710
+ const sf = new Uint8Array(40);
711
+ for (let k = 0; k < 10; k++) {
712
+ const w = view.getUint32(b + 20 + 4 * k, true);
713
+ sf[4 * k] = w >>> 24;
714
+ sf[4 * k + 1] = w >>> 16 & 255;
715
+ sf[4 * k + 2] = w >>> 8 & 255;
716
+ sf[4 * k + 3] = w & 255;
717
+ }
718
+ return sf;
719
+ }
720
+ function feedNavicBlock(view, b, assembler) {
721
+ const prn = svidToPrn(view.getUint8(b + 14));
722
+ if (!prn || prn[0] !== "I") return {};
723
+ const wnc = view.getUint16(b + 12, true);
724
+ if (wnc === WNC_DNU2) return {};
725
+ const sf = extractSbfNavicSubframe(view, b);
726
+ if (!navicSubframeCrcOk(sf)) return { badCrc: true };
727
+ const eph = assembler.push(prn, sf, wnc);
728
+ return eph ? { eph } : {};
729
+ }
730
+ function parseSbfNavic(data) {
731
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
732
+ const ephemerides = [];
733
+ const assembler = new NavicAssembler();
734
+ let badCrc = 0;
735
+ let messages = 0;
736
+ scanSbfFrames(data, view, (id, b, len) => {
737
+ if (id !== 4093 || len < 60) return;
738
+ messages++;
739
+ const r = feedNavicBlock(view, b, assembler);
740
+ if (r.eph) ephemerides.push(r.eph);
741
+ else if (r.badCrc) badCrc++;
742
+ });
743
+ return { ephemerides, badCrc, messages };
744
+ }
745
+
578
746
  // src/sbf/rawnav.ts
579
747
  var CNAV_SIGNAL_OF_BLOCK = {
580
748
  4018: "L2C",
@@ -673,10 +841,10 @@ function isFnavDummy(page) {
673
841
  }
674
842
 
675
843
  // src/sbf/rawnav-bds.ts
676
- var GPS_EPOCH_MS3 = Date.UTC(1980, 0, 6);
844
+ var GPS_EPOCH_MS4 = Date.UTC(1980, 0, 6);
677
845
  var MS_PER_WEEK2 = 7 * 86400 * 1e3;
678
846
  var TOW_DNU = 4294967295;
679
- var WNC_DNU2 = 65535;
847
+ var WNC_DNU3 = 65535;
680
848
  function feedBdsBlock(view, b, assembler) {
681
849
  const prn = svidToPrn(view.getUint8(b + 14));
682
850
  if (!prn || prn[0] !== "C") return {};
@@ -716,7 +884,7 @@ function feedGloBlock(view, b, assembler) {
716
884
  if (!prn || prn[0] !== "R") return {};
717
885
  const towMs = view.getUint32(b + 8, true);
718
886
  const wnc = view.getUint16(b + 12, true);
719
- if (towMs === TOW_DNU || wnc === WNC_DNU2) return {};
887
+ if (towMs === TOW_DNU || wnc === WNC_DNU3) return {};
720
888
  const str2 = new Uint8Array(11);
721
889
  for (let k = 0; k < 3; k++) {
722
890
  const w = view.getUint32(b + 20 + 4 * k, true);
@@ -734,7 +902,7 @@ function feedGloBlock(view, b, assembler) {
734
902
  const eph = assembler.push(
735
903
  prn,
736
904
  str2,
737
- new Date(GPS_EPOCH_MS3 + wnc * MS_PER_WEEK2 + towMs),
905
+ new Date(GPS_EPOCH_MS4 + wnc * MS_PER_WEEK2 + towMs),
738
906
  view.getUint8(b + 18) - 8
739
907
  // FreqNr, offset by 8
740
908
  );
@@ -909,6 +1077,7 @@ function decodeSbfNavigation(data, opts = {}) {
909
1077
  bdsRaw: 0,
910
1078
  sbasRaw: 0,
911
1079
  sbasL5Raw: 0,
1080
+ navicRaw: 0,
912
1081
  badFrames: 0
913
1082
  };
914
1083
  const dfmcByType = {};
@@ -918,6 +1087,7 @@ function decodeSbfNavigation(data, opts = {}) {
918
1087
  const gloAsm = new GloStringAssembler();
919
1088
  const bdsAsm = new BdsAssembler();
920
1089
  const lnavAsm = new GpsLnavAssembler();
1090
+ const navicAsm = new NavicAssembler();
921
1091
  const addEph = (e) => {
922
1092
  if (!e) return;
923
1093
  const key = ephKey(e);
@@ -978,6 +1148,14 @@ function decodeSbfNavigation(data, opts = {}) {
978
1148
  else if (r.badParity) counts.badFrames++;
979
1149
  return;
980
1150
  }
1151
+ case 4093: {
1152
+ if (len < 60) return;
1153
+ counts.navicRaw++;
1154
+ const r = feedNavicBlock(view, b, navicAsm);
1155
+ if (r.eph) addEph(r.eph);
1156
+ else if (r.badCrc) counts.badFrames++;
1157
+ return;
1158
+ }
981
1159
  case 4018:
982
1160
  // GPSRawL2C
983
1161
  case 4019: {
@@ -1598,7 +1776,7 @@ function parseSbfHas(data) {
1598
1776
 
1599
1777
  // src/sbf/index.ts
1600
1778
  var CLIGHT = 299792458;
1601
- var GPS_EPOCH_MS4 = Date.UTC(1980, 0, 6);
1779
+ var GPS_EPOCH_MS5 = Date.UTC(1980, 0, 6);
1602
1780
  var MS_PER_WEEK3 = 7 * 864e5;
1603
1781
  var M3_SYS = ["G", "R", "E", "C", "S", "J", "I"];
1604
1782
  var M3_PR_BASE = [19e6, 19e6, 22e6, 2e7, 34e6, 34e6, 34e6];
@@ -2252,7 +2430,7 @@ function parseSbfMeas(data) {
2252
2430
  const tow = view.getUint32(i + 8, true);
2253
2431
  const wnc = view.getUint16(i + 12, true);
2254
2432
  if (tow !== 4294967295 && wnc !== 65535) {
2255
- ensureEpoch(GPS_EPOCH_MS4 + wnc * MS_PER_WEEK3 + tow);
2433
+ ensureEpoch(GPS_EPOCH_MS5 + wnc * MS_PER_WEEK3 + tow);
2256
2434
  if (id === 4109) decodeMeas3Ranges(i, tow);
2257
2435
  else if (id === 4110) decodeMeas3CN0(i, len);
2258
2436
  else if (id === 4111) decodeMeas3Doppler(i, len);
@@ -2273,6 +2451,9 @@ export {
2273
2451
  parseSbfReceiverSetup,
2274
2452
  parseSbfPvt,
2275
2453
  parseSbfGpsNav,
2454
+ extractSbfNavicSubframe,
2455
+ feedNavicBlock,
2456
+ parseSbfNavic,
2276
2457
  parseSbfCnav,
2277
2458
  parseSbfGalNav,
2278
2459
  parseSbfBdsNav,
@@ -534,6 +534,70 @@ function decodeQzssEphemeris(payload) {
534
534
  fitInterval
535
535
  };
536
536
  }
537
+ function decodeNavicEphemeris(payload) {
538
+ if (payload.length < 60) return null;
539
+ const r = new BitReader(payload);
540
+ r.skip(12);
541
+ const svId = r.readU(6);
542
+ const week = r.readU(10);
543
+ const af0 = r.readS(22) * 2 ** -31;
544
+ const af1 = r.readS(16) * 2 ** -43;
545
+ const af2 = r.readS(8) * 2 ** -55;
546
+ const ura = r.readU(4);
547
+ const toc = r.readU(16) * 16;
548
+ const tgd = r.readS(8) * 2 ** -31;
549
+ const deltaN = r.readS(22) * 2 ** -41 * PI;
550
+ const iode = r.readU(8);
551
+ r.skip(10);
552
+ const l5Health = r.readU(1);
553
+ const sHealth = r.readU(1);
554
+ const cuc = r.readS(15) * 2 ** -28;
555
+ const cus = r.readS(15) * 2 ** -28;
556
+ const cic = r.readS(15) * 2 ** -28;
557
+ const cis = r.readS(15) * 2 ** -28;
558
+ const crc = r.readS(15) * 2 ** -4;
559
+ const crs = r.readS(15) * 2 ** -4;
560
+ const idot = r.readS(14) * 2 ** -43 * PI;
561
+ const m0 = r.readS(32) * 2 ** -31 * PI;
562
+ const toe = r.readU(16) * 16;
563
+ const e = r.readU(32) * 2 ** -33;
564
+ const sqrtA = r.readU(32) * 2 ** -19;
565
+ const omega0 = r.readS(32) * 2 ** -31 * PI;
566
+ const argPerigee = r.readS(32) * 2 ** -31 * PI;
567
+ const omegaDot = r.readS(22) * 2 ** -41 * PI;
568
+ const i0 = r.readS(32) * 2 ** -31 * PI;
569
+ return {
570
+ prn: `I${String(svId).padStart(2, "0")}`,
571
+ constellation: "NavIC",
572
+ health: l5Health << 1 | sHealth,
573
+ lastReceived: Date.now(),
574
+ messageType: 1041,
575
+ week,
576
+ ura,
577
+ toc,
578
+ toe,
579
+ sqrtA,
580
+ eccentricity: e,
581
+ inclination: i0,
582
+ omega0,
583
+ omegaDot,
584
+ argPerigee,
585
+ meanAnomaly: m0,
586
+ deltaN,
587
+ idot,
588
+ crs,
589
+ crc,
590
+ cuc,
591
+ cus,
592
+ cic,
593
+ cis,
594
+ af0,
595
+ af1,
596
+ af2,
597
+ iode,
598
+ tgd
599
+ };
600
+ }
537
601
  function decodeEphemeris(frame) {
538
602
  try {
539
603
  switch (frame.messageType) {
@@ -541,6 +605,8 @@ function decodeEphemeris(frame) {
541
605
  return decodeGpsEphemeris(frame.payload);
542
606
  case 1020:
543
607
  return decodeGlonassEphemeris(frame.payload);
608
+ case 1041:
609
+ return decodeNavicEphemeris(frame.payload);
544
610
  case 1042:
545
611
  return decodeBdsEphemeris(frame.payload);
546
612
  case 1043:
@@ -1649,6 +1649,10 @@ var SbasProcessor = class {
1649
1649
  tlat = 0;
1650
1650
  degr = null;
1651
1651
  mt27 = null;
1652
+ /** MT28 clock-ephemeris covariance per PRN: the upper-triangular Cholesky
1653
+ * factor E (10 elements [E11,E22,E33,E44,E12,E13,E14,E23,E24,E34]) and its
1654
+ * scale exponent. Keyed by PRN; cleared on a mask change. */
1655
+ mt28 = /* @__PURE__ */ new Map();
1652
1656
  ion = [];
1653
1657
  /** GEO PRN(s) whose messages have been ingested. */
1654
1658
  geoPrns = /* @__PURE__ */ new Set();
@@ -1679,6 +1683,8 @@ var SbasProcessor = class {
1679
1683
  return this.decodeMt10(msg) ? type : -1;
1680
1684
  case 27:
1681
1685
  return this.decodeMt27(msg) ? type : -1;
1686
+ case 28:
1687
+ return this.decodeMt28(msg) ? type : -1;
1682
1688
  case 18:
1683
1689
  return this.decodeIgpMask(msg) ? type : -1;
1684
1690
  case 24:
@@ -1705,6 +1711,7 @@ var SbasProcessor = class {
1705
1711
  return true;
1706
1712
  this.iodp = iodp;
1707
1713
  this.sats = prns.map((prn) => ({ prn }));
1714
+ this.mt28.clear();
1708
1715
  this.satIdx.clear();
1709
1716
  this.sats.forEach((s, k) => {
1710
1717
  if (s.prn) this.satIdx.set(s.prn, k);
@@ -1770,7 +1777,9 @@ var SbasProcessor = class {
1770
1777
  iiono: u(117, 9) || 1,
1771
1778
  cionoRamp: u(126, 10) * 5e-6,
1772
1779
  rssUdre: u(136, 1) === 1,
1773
- rssIono: u(137, 1) === 1
1780
+ rssIono: u(137, 1) === 1,
1781
+ ccovariance: u(138, 7) * 0.1
1782
+ // Table B-49: 0..12.7 m, 0.1 m
1774
1783
  };
1775
1784
  return true;
1776
1785
  }
@@ -1822,6 +1831,51 @@ var SbasProcessor = class {
1822
1831
  }
1823
1832
  return best ?? DELTA_UDRE[s.udreOutside] ?? 1;
1824
1833
  }
1834
+ /** MT28 clock-ephemeris covariance (ICAO Annex 10 Vol I §3.5.4.7 / Table B-51):
1835
+ * two satellites per message, each an upper-triangular Cholesky factor E and a
1836
+ * scale exponent that together define the relative clock-ephemeris covariance
1837
+ * used for a geometry-dependent δUDRE (§3.5.5.6.2.5). Slots are indexed into
1838
+ * the current MT1 mask, so a mismatched IODP is dropped. */
1839
+ decodeMt28(msg) {
1840
+ if (this.iodp !== getBitU(msg, 14, 2)) return false;
1841
+ for (const base of [16, 121]) {
1842
+ const slot = getBitU(msg, base, 6);
1843
+ const prn = this.sats[slot]?.prn;
1844
+ const scaleExp = getBitU(msg, base + 6, 3);
1845
+ const e = [
1846
+ getBitU(msg, base + 9, 9),
1847
+ getBitU(msg, base + 18, 9),
1848
+ getBitU(msg, base + 27, 9),
1849
+ getBitU(msg, base + 36, 9),
1850
+ getBitS(msg, base + 45, 10),
1851
+ getBitS(msg, base + 55, 10),
1852
+ getBitS(msg, base + 65, 10),
1853
+ getBitS(msg, base + 75, 10),
1854
+ getBitS(msg, base + 85, 10),
1855
+ getBitS(msg, base + 95, 10)
1856
+ ];
1857
+ if (prn && e.some((v) => v !== 0)) this.mt28.set(prn, { scaleExp, e });
1858
+ }
1859
+ return true;
1860
+ }
1861
+ /** Geometry-dependent δUDRE multiplier from MT28 (§3.5.5.6.2.5):
1862
+ * δUDRE = √(Iᵀ·C·I) + ε_c, with C = Rᵀ·R, R = E·2^(scaleExp−5),
1863
+ * I = [î_user→sat, 1], ε_c = C_covariance·2^(scaleExp−5). Since C = RᵀR,
1864
+ * Iᵀ·C·I = ‖R·I‖², so we form R·I directly. Null when no MT28 for `prn`. */
1865
+ mt28DeltaUdre(prn, los) {
1866
+ const m = this.mt28.get(prn);
1867
+ if (!m) return null;
1868
+ const sf = 2 ** (m.scaleExp - 5);
1869
+ const [e11, e22, e33, e44, e12, e13, e14, e23, e24, e34] = m.e;
1870
+ const [ix, iy, iz] = los;
1871
+ const v0 = (e11 * ix + e12 * iy + e13 * iz + e14) * sf;
1872
+ const v1 = (e22 * iy + e23 * iz + e24) * sf;
1873
+ const v2 = (e33 * iz + e34) * sf;
1874
+ const v3 = e44 * sf;
1875
+ const iCi = v0 * v0 + v1 * v1 + v2 * v2 + v3 * v3;
1876
+ const ec = (this.degr?.ccovariance ?? 0) * sf;
1877
+ return Math.sqrt(iCi) + ec;
1878
+ }
1825
1879
  decodeMixed(msg, week, tow) {
1826
1880
  if (this.iodp !== getBitU(msg, 110, 2)) return false;
1827
1881
  const blk = getBitU(msg, 112, 2);
@@ -2026,7 +2080,7 @@ var SbasProcessor = class {
2026
2080
  * no valid (unexpired) long-term + fast correction is available. Add `dPos`
2027
2081
  * to the satellite ECEF position and `dClkS` to its clock bias.
2028
2082
  */
2029
- satCorrection(prn, week, tow, userLatRad, userLonRad) {
2083
+ satCorrection(prn, week, tow, userLatRad, userLonRad, losUnitEcef) {
2030
2084
  const idx = this.satIdx.get(prn);
2031
2085
  if (idx === void 0) return null;
2032
2086
  const s = this.sats[idx];
@@ -2048,7 +2102,8 @@ var SbasProcessor = class {
2048
2102
  const tf = now - s.fcorr.t0s + this.tlat;
2049
2103
  if (Math.abs(tf) > MAXSBSAGEF || s.fcorr.udre >= 15) return null;
2050
2104
  const prc = s.fcorr.prc;
2051
- const dudre = userLatRad != null && userLonRad != null ? this.deltaUdre(
2105
+ const mt28 = losUnitEcef ? this.mt28DeltaUdre(prn, losUnitEcef) : null;
2106
+ const dudre = mt28 != null ? mt28 : userLatRad != null && userLonRad != null ? this.deltaUdre(
2052
2107
  userLatRad * 180 / Math.PI,
2053
2108
  userLonRad * 180 / Math.PI
2054
2109
  ) : 1;
@@ -3768,7 +3823,10 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3768
3823
  used.push(prn);
3769
3824
  azels.push(azel);
3770
3825
  if (sbas) {
3771
- const sc = sbas.satCorrection(prn, gpsWeek, gpsTow, rxLat, rxLon);
3826
+ const dx = sat.x - x, dy = sat.y - y, dz = sat.z - z;
3827
+ const dr = Math.hypot(dx, dy, dz) || 1;
3828
+ const los = [dx / dr, dy / dr, dz / dr];
3829
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow, rxLat, rxLon, los);
3772
3830
  const si = sbas.ionoDelay(
3773
3831
  gpsWeek,
3774
3832
  gpsTow,