gnss-js 1.35.0 → 1.37.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.
package/dist/sbf.cjs CHANGED
@@ -20,11 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/sbf/index.ts
21
21
  var sbf_exports = {};
22
22
  __export(sbf_exports, {
23
+ decodeSbfNavigation: () => decodeSbfNavigation,
23
24
  parseSbfAlmanac: () => parseSbfAlmanac,
24
25
  parseSbfBdsNav: () => parseSbfBdsNav,
25
26
  parseSbfCnav: () => parseSbfCnav,
26
27
  parseSbfGalNav: () => parseSbfGalNav,
27
28
  parseSbfGloNav: () => parseSbfGloNav,
29
+ parseSbfGpsNav: () => parseSbfGpsNav,
28
30
  parseSbfHas: () => parseSbfHas,
29
31
  parseSbfIonoUtc: () => parseSbfIonoUtc,
30
32
  parseSbfMeas: () => parseSbfMeas,
@@ -544,6 +546,199 @@ function getBitS(buff, pos, len) {
544
546
  if (len <= 0 || bits < 2 ** (len - 1)) return bits;
545
547
  return bits - 2 ** len;
546
548
  }
549
+ function setBitU(buff, pos, len, data) {
550
+ let mask = 2 ** (len - 1);
551
+ for (let i = pos; i < pos + len; i++, mask /= 2) {
552
+ if (data >= mask) {
553
+ buff[i >> 3] |= 1 << 7 - (i & 7);
554
+ data -= mask;
555
+ } else {
556
+ buff[i >> 3] &= ~(1 << 7 - (i & 7));
557
+ }
558
+ }
559
+ }
560
+ var GpsLnavAssembler = class _GpsLnavAssembler {
561
+ buffers = /* @__PURE__ */ new Map();
562
+ last = /* @__PURE__ */ new Map();
563
+ /** Subframe ID (1–5) from the HOW word of a parity-stripped subframe. */
564
+ static subframeId(subframe) {
565
+ return getBitU(subframe, 43, 3);
566
+ }
567
+ /**
568
+ * Push one parity-stripped 30-byte LNAV subframe for `prn`, resolving
569
+ * the 10-bit week against `refWeek`. See {@link LnavPush} for outcomes.
570
+ */
571
+ push(prn, subframe, refWeek) {
572
+ const id = _GpsLnavAssembler.subframeId(subframe);
573
+ if (id < 1 || id > 3) return { kind: "skipped" };
574
+ let sf = this.buffers.get(prn);
575
+ if (!sf) {
576
+ sf = { buf: new Uint8Array(90), have: 0 };
577
+ this.buffers.set(prn, sf);
578
+ }
579
+ sf.buf.set(subframe, (id - 1) * 30);
580
+ sf.have |= 1 << id - 1;
581
+ if (id !== 3 || sf.have !== 7) return { kind: "incomplete" };
582
+ const eph = decodeGpsLnavFrame(sf.buf, { prn, refWeek });
583
+ if (!eph) return { kind: "decodeFailed" };
584
+ const prev = this.last.get(prn);
585
+ if (prev && prev.iode === eph.iode && prev.tocDate.getTime() === eph.tocDate.getTime()) {
586
+ return { kind: "duplicate" };
587
+ }
588
+ this.last.set(prn, eph);
589
+ return { kind: "eph", eph };
590
+ }
591
+ };
592
+ function decodeGpsLnavFrame(subframes, opts = {}) {
593
+ if (subframes.length < 90) return null;
594
+ const b = subframes;
595
+ let i = 24;
596
+ const tow1 = getBitU(b, i, 17) * 6;
597
+ i += 17 + 2;
598
+ const id1 = getBitU(b, i, 3);
599
+ i += 3 + 2;
600
+ const week10 = getBitU(b, i, 10);
601
+ i += 10;
602
+ i += 2;
603
+ i += 4;
604
+ const svHealth = getBitU(b, i, 6);
605
+ i += 6;
606
+ const iodc0 = getBitU(b, i, 2);
607
+ i += 2;
608
+ i += 1 + 87;
609
+ const tgdRaw = getBitS(b, i, 8);
610
+ i += 8;
611
+ const iodc1 = getBitU(b, i, 8);
612
+ i += 8;
613
+ const tocSec = getBitU(b, i, 16) * 16;
614
+ i += 16;
615
+ const af2 = getBitS(b, i, 8) * 2 ** -55;
616
+ i += 8;
617
+ const af1 = getBitS(b, i, 16) * 2 ** -43;
618
+ i += 16;
619
+ const af0 = getBitS(b, i, 22) * 2 ** -31;
620
+ i = 240 + 24;
621
+ i += 17 + 2;
622
+ const id2 = getBitU(b, i, 3);
623
+ i += 3 + 2;
624
+ const iode = getBitU(b, i, 8);
625
+ i += 8;
626
+ const crs = getBitS(b, i, 16) * 2 ** -5;
627
+ i += 16;
628
+ const deltaN = getBitS(b, i, 16) * 2 ** -43 * GPS_PI;
629
+ i += 16;
630
+ const m0 = getBitS(b, i, 32) * 2 ** -31 * GPS_PI;
631
+ i += 32;
632
+ const cuc = getBitS(b, i, 16) * 2 ** -29;
633
+ i += 16;
634
+ const e = getBitU(b, i, 32) * 2 ** -33;
635
+ i += 32;
636
+ const cus = getBitS(b, i, 16) * 2 ** -29;
637
+ i += 16;
638
+ const sqrtA = getBitU(b, i, 32) * 2 ** -19;
639
+ i += 32;
640
+ const toes = getBitU(b, i, 16) * 16;
641
+ i = 480 + 24;
642
+ i += 17 + 2;
643
+ const id3 = getBitU(b, i, 3);
644
+ i += 3 + 2;
645
+ const cic = getBitS(b, i, 16) * 2 ** -29;
646
+ i += 16;
647
+ const omega0 = getBitS(b, i, 32) * 2 ** -31 * GPS_PI;
648
+ i += 32;
649
+ const cis = getBitS(b, i, 16) * 2 ** -29;
650
+ i += 16;
651
+ const i0 = getBitS(b, i, 32) * 2 ** -31 * GPS_PI;
652
+ i += 32;
653
+ const crc = getBitS(b, i, 16) * 2 ** -5;
654
+ i += 16;
655
+ const omega = getBitS(b, i, 32) * 2 ** -31 * GPS_PI;
656
+ i += 32;
657
+ const omegaDot = getBitS(b, i, 24) * 2 ** -43 * GPS_PI;
658
+ i += 24;
659
+ const iode3 = getBitU(b, i, 8);
660
+ i += 8;
661
+ const idot = getBitS(b, i, 14) * 2 ** -43 * GPS_PI;
662
+ const iodc = iodc0 * 256 + iodc1;
663
+ if (id1 !== 1 || id2 !== 2 || id3 !== 3) return null;
664
+ if (iode3 !== iode || iode !== (iodc & 255)) return null;
665
+ const ref = opts.refWeek ?? Math.floor((Date.now() - GPS_EPOCH_MS2) / 1e3 / SEC_PER_WEEK2);
666
+ let week = week10 + 1024 * Math.round((ref - week10) / 1024);
667
+ if (toes < tow1 - 302400) week++;
668
+ else if (toes > tow1 + 302400) week--;
669
+ const prn = opts.prn ?? "G00";
670
+ const tocDate = new Date(
671
+ GPS_EPOCH_MS2 + (week * SEC_PER_WEEK2 + tocSec) * 1e3
672
+ );
673
+ return {
674
+ system: prn[0] === "J" ? "J" : "G",
675
+ prn,
676
+ tocDate,
677
+ // Same seconds-of-week convention as parseNavFile (rinex/nav.ts).
678
+ toc: tocDate.getTime() / 1e3 % SEC_PER_WEEK2,
679
+ af0,
680
+ af1,
681
+ af2,
682
+ iode,
683
+ crs,
684
+ deltaN,
685
+ m0,
686
+ cuc,
687
+ e,
688
+ cus,
689
+ sqrtA,
690
+ toe: toes,
691
+ cic,
692
+ omega0,
693
+ cis,
694
+ i0,
695
+ crc,
696
+ omega,
697
+ omegaDot,
698
+ idot,
699
+ week,
700
+ svHealth,
701
+ tgd: tgdRaw === -128 ? 0 : tgdRaw * 2 ** -31
702
+ // IS-GPS-200: -128 reserved
703
+ };
704
+ }
705
+
706
+ // src/sbf/rawnav-gps.ts
707
+ var WNC_DNU = 65535;
708
+ function extractSbfLnavSubframe(view, b) {
709
+ const sf = new Uint8Array(30);
710
+ for (let k = 0; k < 10; k++) {
711
+ const w = view.getUint32(b + 20 + 4 * k, true);
712
+ setBitU(sf, 24 * k, 24, w >>> 6 & 16777215);
713
+ }
714
+ return sf;
715
+ }
716
+ function feedGpsLnavBlock(view, b, assembler) {
717
+ const prn = svidToPrn(view.getUint8(b + 14));
718
+ if (!prn || prn[0] !== "G" && prn[0] !== "J") return {};
719
+ if (view.getUint8(b + 15) !== 1) return {};
720
+ const wnc = view.getUint16(b + 12, true);
721
+ if (wnc === WNC_DNU) return {};
722
+ const r = assembler.push(prn, extractSbfLnavSubframe(view, b), wnc);
723
+ if (r.kind === "eph") return { eph: r.eph };
724
+ if (r.kind === "decodeFailed") return { badParity: true };
725
+ return {};
726
+ }
727
+ function parseSbfGpsNav(data) {
728
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
729
+ const ephemerides = [];
730
+ const assembler = new GpsLnavAssembler();
731
+ let badParity = 0;
732
+ let messages = 0;
733
+ scanSbfFrames(data, view, (id, b, len) => {
734
+ if (id !== 4017 && id !== 4066 || len < 60) return;
735
+ messages++;
736
+ const r = feedGpsLnavBlock(view, b, assembler);
737
+ if (r.eph) ephemerides.push(r.eph);
738
+ else if (r.badParity) badParity++;
739
+ });
740
+ return { ephemerides, badParity, messages };
741
+ }
547
742
 
548
743
  // src/navbits/cnav.ts
549
744
  var GPS_EPOCH_MS3 = Date.UTC(1980, 0, 6);
@@ -742,37 +937,40 @@ var CnavAssembler = class {
742
937
  };
743
938
 
744
939
  // src/sbf/rawnav.ts
745
- var SIGNAL_OF_BLOCK = {
940
+ var CNAV_SIGNAL_OF_BLOCK = {
746
941
  4018: "L2C",
747
942
  4019: "L5"
748
943
  };
944
+ function newCnavAssemblers() {
945
+ return { L2C: new CnavAssembler(), L5: new CnavAssembler() };
946
+ }
947
+ function feedCnavBlock(view, b, id, asm) {
948
+ const signal = CNAV_SIGNAL_OF_BLOCK[id];
949
+ if (!signal) return {};
950
+ const msg = new Uint8Array(40);
951
+ for (let k = 0; k < 10; k++) {
952
+ const w = view.getUint32(b + 20 + 4 * k, true);
953
+ msg[4 * k] = w >>> 24;
954
+ msg[4 * k + 1] = w >>> 16 & 255;
955
+ msg[4 * k + 2] = w >>> 8 & 255;
956
+ msg[4 * k + 3] = w & 255;
957
+ }
958
+ if (!cnavCrcOk(msg)) return { badCrc: true };
959
+ const eph = asm[signal].push(msg);
960
+ return eph ? { eph: { ...eph, signal } } : {};
961
+ }
749
962
  function parseSbfCnav(data) {
750
963
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
751
964
  const ephemerides = [];
752
- const assemblers = {
753
- L2C: new CnavAssembler(),
754
- L5: new CnavAssembler()
755
- };
965
+ const asm = newCnavAssemblers();
756
966
  let badCrc = 0;
757
967
  let messages = 0;
758
968
  scanSbfFrames(data, view, (id, b, len) => {
759
- const signal = SIGNAL_OF_BLOCK[id];
760
- if (!signal || len < 60) return;
969
+ if (!CNAV_SIGNAL_OF_BLOCK[id] || len < 60) return;
761
970
  messages++;
762
- const msg = new Uint8Array(40);
763
- for (let k = 0; k < 10; k++) {
764
- const w = view.getUint32(b + 20 + 4 * k, true);
765
- msg[4 * k] = w >>> 24;
766
- msg[4 * k + 1] = w >>> 16 & 255;
767
- msg[4 * k + 2] = w >>> 8 & 255;
768
- msg[4 * k + 3] = w & 255;
769
- }
770
- if (!cnavCrcOk(msg)) {
771
- badCrc++;
772
- return;
773
- }
774
- const eph = assemblers[signal].push(msg);
775
- if (eph) ephemerides.push({ ...eph, signal });
971
+ const r = feedCnavBlock(view, b, id, asm);
972
+ if (r.eph) ephemerides.push(r.eph);
973
+ else if (r.badCrc) badCrc++;
776
974
  });
777
975
  return { ephemerides, badCrc, messages };
778
976
  }
@@ -1088,50 +1286,51 @@ var GalFnavAssembler = class {
1088
1286
  // src/sbf/rawnav-gal.ts
1089
1287
  var INAV_SOURCES = [17, 21, 22];
1090
1288
  var FNAV_SOURCES = [20, 22];
1289
+ function newGalAssemblers() {
1290
+ return { inav: new GalInavAssembler(), fnav: new GalFnavAssembler() };
1291
+ }
1292
+ function feedGalBlock(data, view, b, id, asm) {
1293
+ const prnStr = svidToPrn(data[b + 14]);
1294
+ if (!prnStr || prnStr[0] !== "E") return {};
1295
+ const prn = parseInt(prnStr.slice(1), 10);
1296
+ const source = data[b + 17] & 31;
1297
+ const page = new Uint8Array(32);
1298
+ for (let k = 0; k < 8; k++) {
1299
+ const w = view.getUint32(b + 20 + 4 * k, true);
1300
+ page[4 * k] = w >>> 24;
1301
+ page[4 * k + 1] = w >>> 16 & 255;
1302
+ page[4 * k + 2] = w >>> 8 & 255;
1303
+ page[4 * k + 3] = w & 255;
1304
+ }
1305
+ let eph;
1306
+ let src;
1307
+ if (id === 4023) {
1308
+ src = "inav";
1309
+ if (!INAV_SOURCES.includes(source)) return {};
1310
+ if (getBitU2(page, 0) !== 0 || getBitU2(page, 114) !== 1) return {};
1311
+ if (!galInavPageCrcOk(page)) return { badCrc: true };
1312
+ eph = asm.inav.push(prn, page);
1313
+ } else {
1314
+ src = "fnav";
1315
+ if (!FNAV_SOURCES.includes(source)) return {};
1316
+ if (isFnavDummy(page)) return {};
1317
+ if (!galFnavPageCrcOk(page)) return { badCrc: true };
1318
+ eph = asm.fnav.push(prn, page);
1319
+ }
1320
+ return eph ? { eph: { ...eph, source: src } } : {};
1321
+ }
1091
1322
  function parseSbfGalNav(data) {
1092
1323
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1093
1324
  const ephemerides = [];
1094
- const inav = new GalInavAssembler();
1095
- const fnav = new GalFnavAssembler();
1325
+ const asm = newGalAssemblers();
1096
1326
  let badCrc = 0;
1097
1327
  let messages = 0;
1098
1328
  scanSbfFrames(data, view, (id, b, len) => {
1099
1329
  if (id !== 4022 && id !== 4023 || len < 52) return;
1100
1330
  messages++;
1101
- const prnStr = svidToPrn(data[b + 14]);
1102
- if (!prnStr || prnStr[0] !== "E") return;
1103
- const prn = parseInt(prnStr.slice(1), 10);
1104
- const source = data[b + 17] & 31;
1105
- const page = new Uint8Array(32);
1106
- for (let k = 0; k < 8; k++) {
1107
- const w = view.getUint32(b + 20 + 4 * k, true);
1108
- page[4 * k] = w >>> 24;
1109
- page[4 * k + 1] = w >>> 16 & 255;
1110
- page[4 * k + 2] = w >>> 8 & 255;
1111
- page[4 * k + 3] = w & 255;
1112
- }
1113
- let eph;
1114
- let src;
1115
- if (id === 4023) {
1116
- src = "inav";
1117
- if (!INAV_SOURCES.includes(source)) return;
1118
- if (getBitU2(page, 0) !== 0 || getBitU2(page, 114) !== 1) return;
1119
- if (!galInavPageCrcOk(page)) {
1120
- badCrc++;
1121
- return;
1122
- }
1123
- eph = inav.push(prn, page);
1124
- } else {
1125
- src = "fnav";
1126
- if (!FNAV_SOURCES.includes(source)) return;
1127
- if (isFnavDummy(page)) return;
1128
- if (!galFnavPageCrcOk(page)) {
1129
- badCrc++;
1130
- return;
1131
- }
1132
- eph = fnav.push(prn, page);
1133
- }
1134
- if (eph) ephemerides.push({ ...eph, source: src });
1331
+ const r = feedGalBlock(data, view, b, id, asm);
1332
+ if (r.eph) ephemerides.push(r.eph);
1333
+ else if (r.badCrc) badCrc++;
1135
1334
  });
1136
1335
  return { ephemerides, badCrc, messages };
1137
1336
  }
@@ -1587,7 +1786,26 @@ var GloStringAssembler = class {
1587
1786
  var GPS_EPOCH_MS6 = Date.UTC(1980, 0, 6);
1588
1787
  var MS_PER_WEEK3 = 7 * 86400 * 1e3;
1589
1788
  var TOW_DNU = 4294967295;
1590
- var WNC_DNU = 65535;
1789
+ var WNC_DNU2 = 65535;
1790
+ function feedBdsBlock(view, b, assembler) {
1791
+ const prn = svidToPrn(view.getUint8(b + 14));
1792
+ if (!prn || prn[0] !== "C") return {};
1793
+ const sf = new Uint8Array(38);
1794
+ for (let k = 0; k < 10; k++) {
1795
+ const w = view.getUint32(b + 20 + 4 * k, true);
1796
+ sf[4 * k] = w >>> 24;
1797
+ if (k < 9) {
1798
+ sf[4 * k + 1] = w >>> 16 & 255;
1799
+ sf[4 * k + 2] = w >>> 8 & 255;
1800
+ sf[4 * k + 3] = w & 255;
1801
+ } else {
1802
+ sf[37] = w >>> 16 & 240;
1803
+ }
1804
+ }
1805
+ if (!bdsSubframeParityOk(sf)) return { badCrc: true };
1806
+ const eph = assembler.push(prn, sf);
1807
+ return eph ? { eph } : {};
1808
+ }
1591
1809
  function parseSbfBdsNav(data) {
1592
1810
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1593
1811
  const ephemerides = [];
@@ -1597,29 +1815,41 @@ function parseSbfBdsNav(data) {
1597
1815
  scanSbfFrames(data, view, (id, b, len) => {
1598
1816
  if (id !== 4047 || len < 60) return;
1599
1817
  messages++;
1600
- const prn = svidToPrn(view.getUint8(b + 14));
1601
- if (!prn || prn[0] !== "C") return;
1602
- const sf = new Uint8Array(38);
1603
- for (let k = 0; k < 10; k++) {
1604
- const w = view.getUint32(b + 20 + 4 * k, true);
1605
- sf[4 * k] = w >>> 24;
1606
- if (k < 9) {
1607
- sf[4 * k + 1] = w >>> 16 & 255;
1608
- sf[4 * k + 2] = w >>> 8 & 255;
1609
- sf[4 * k + 3] = w & 255;
1610
- } else {
1611
- sf[37] = w >>> 16 & 240;
1612
- }
1613
- }
1614
- if (!bdsSubframeParityOk(sf)) {
1615
- badCrc++;
1616
- return;
1617
- }
1618
- const eph = assembler.push(prn, sf);
1619
- if (eph) ephemerides.push(eph);
1818
+ const r = feedBdsBlock(view, b, assembler);
1819
+ if (r.eph) ephemerides.push(r.eph);
1820
+ else if (r.badCrc) badCrc++;
1620
1821
  });
1621
1822
  return { ephemerides, badCrc, messages };
1622
1823
  }
1824
+ function feedGloBlock(view, b, assembler) {
1825
+ const prn = svidToPrn(view.getUint8(b + 14));
1826
+ if (!prn || prn[0] !== "R") return {};
1827
+ const towMs = view.getUint32(b + 8, true);
1828
+ const wnc = view.getUint16(b + 12, true);
1829
+ if (towMs === TOW_DNU || wnc === WNC_DNU2) return {};
1830
+ const str = new Uint8Array(11);
1831
+ for (let k = 0; k < 3; k++) {
1832
+ const w = view.getUint32(b + 20 + 4 * k, true);
1833
+ str[4 * k] = w >>> 24;
1834
+ if (k < 2) {
1835
+ str[4 * k + 1] = w >>> 16 & 255;
1836
+ str[4 * k + 2] = w >>> 8 & 255;
1837
+ str[4 * k + 3] = w & 255;
1838
+ } else {
1839
+ str[9] = w >>> 16 & 255;
1840
+ str[10] = w >>> 8 & 248;
1841
+ }
1842
+ }
1843
+ if (!testGloString(str)) return { badCrc: true };
1844
+ const eph = assembler.push(
1845
+ prn,
1846
+ str,
1847
+ new Date(GPS_EPOCH_MS6 + wnc * MS_PER_WEEK3 + towMs),
1848
+ view.getUint8(b + 18) - 8
1849
+ // FreqNr, offset by 8
1850
+ );
1851
+ return eph ? { eph } : {};
1852
+ }
1623
1853
  function parseSbfGloNav(data) {
1624
1854
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1625
1855
  const ephemerides = [];
@@ -1629,38 +1859,161 @@ function parseSbfGloNav(data) {
1629
1859
  scanSbfFrames(data, view, (id, b, len) => {
1630
1860
  if (id !== 4026 || len < 32) return;
1631
1861
  messages++;
1632
- const prn = svidToPrn(view.getUint8(b + 14));
1633
- if (!prn || prn[0] !== "R") return;
1634
- const towMs = view.getUint32(b + 8, true);
1635
- const wnc = view.getUint16(b + 12, true);
1636
- if (towMs === TOW_DNU || wnc === WNC_DNU) return;
1637
- const str = new Uint8Array(11);
1638
- for (let k = 0; k < 3; k++) {
1639
- const w = view.getUint32(b + 20 + 4 * k, true);
1640
- str[4 * k] = w >>> 24;
1641
- if (k < 2) {
1642
- str[4 * k + 1] = w >>> 16 & 255;
1643
- str[4 * k + 2] = w >>> 8 & 255;
1644
- str[4 * k + 3] = w & 255;
1645
- } else {
1646
- str[9] = w >>> 16 & 255;
1647
- str[10] = w >>> 8 & 248;
1648
- }
1862
+ const r = feedGloBlock(view, b, assembler);
1863
+ if (r.eph) ephemerides.push(r.eph);
1864
+ else if (r.badCrc) badCrc++;
1865
+ });
1866
+ return { ephemerides, badCrc, messages };
1867
+ }
1868
+
1869
+ // src/sbf/navigation.ts
1870
+ var F4_DNU3 = -2e10;
1871
+ function ephKey(e) {
1872
+ const source = "source" in e ? e.source : "";
1873
+ return `${e.prn}|${e.tocDate.getTime()}|${source}`;
1874
+ }
1875
+ function decodeSbfNavigation(data) {
1876
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1877
+ const ephemerides = [];
1878
+ const seen = /* @__PURE__ */ new Set();
1879
+ const cnav = [];
1880
+ const ionoCorrections = {};
1881
+ let leapSeconds = null;
1882
+ const counts = {
1883
+ decodedNav: 0,
1884
+ gpsLnavRaw: 0,
1885
+ cnavRaw: 0,
1886
+ galRaw: 0,
1887
+ gloRaw: 0,
1888
+ bdsRaw: 0,
1889
+ badFrames: 0
1890
+ };
1891
+ const cnavAsm = newCnavAssemblers();
1892
+ const galAsm = newGalAssemblers();
1893
+ const gloAsm = new GloStringAssembler();
1894
+ const bdsAsm = new BdsAssembler();
1895
+ const lnavAsm = new GpsLnavAssembler();
1896
+ const addEph = (e) => {
1897
+ if (!e) return;
1898
+ const key = ephKey(e);
1899
+ if (seen.has(key)) return;
1900
+ seen.add(key);
1901
+ ephemerides.push(e);
1902
+ };
1903
+ const f4s = (b, n) => {
1904
+ const out = [];
1905
+ for (let k = 0; k < n; k++) {
1906
+ const v = view.getFloat32(b + 4 * k, true);
1907
+ if (v === F4_DNU3) return null;
1908
+ out.push(v);
1649
1909
  }
1650
- if (!testGloString(str)) {
1651
- badCrc++;
1652
- return;
1910
+ return out;
1911
+ };
1912
+ scanSbfFrames(data, view, (id, b, len) => {
1913
+ switch (id) {
1914
+ /* ---- decoded ephemeris blocks ---- */
1915
+ case 5891:
1916
+ if (len >= 140) {
1917
+ counts.decodedNav++;
1918
+ addEph(decodeGpsQzsNav(view, b, "G"));
1919
+ }
1920
+ return;
1921
+ case 4095:
1922
+ if (len >= 140) {
1923
+ counts.decodedNav++;
1924
+ addEph(decodeGpsQzsNav(view, b, "J"));
1925
+ }
1926
+ return;
1927
+ case 4002:
1928
+ if (len >= 149) {
1929
+ counts.decodedNav++;
1930
+ addEph(decodeGalNav(view, b));
1931
+ }
1932
+ return;
1933
+ case 4081:
1934
+ if (len >= 140) {
1935
+ counts.decodedNav++;
1936
+ addEph(decodeBdsNav(view, b));
1937
+ }
1938
+ return;
1939
+ case 4004:
1940
+ if (len >= 96) {
1941
+ counts.decodedNav++;
1942
+ addEph(decodeGloNav(view, b));
1943
+ }
1944
+ return;
1945
+ /* ---- raw navigation-frame blocks ---- */
1946
+ case 4017:
1947
+ // GPSRawCA
1948
+ case 4066: {
1949
+ if (len < 60) return;
1950
+ counts.gpsLnavRaw++;
1951
+ const r = feedGpsLnavBlock(view, b, lnavAsm);
1952
+ if (r.eph) addEph(r.eph);
1953
+ else if (r.badParity) counts.badFrames++;
1954
+ return;
1955
+ }
1956
+ case 4018:
1957
+ // GPSRawL2C
1958
+ case 4019: {
1959
+ if (len < 60) return;
1960
+ counts.cnavRaw++;
1961
+ const r = feedCnavBlock(view, b, id, cnavAsm);
1962
+ if (r.eph) cnav.push(r.eph);
1963
+ else if (r.badCrc) counts.badFrames++;
1964
+ return;
1965
+ }
1966
+ case 4022:
1967
+ // GALRawFNAV
1968
+ case 4023: {
1969
+ if (len < 52) return;
1970
+ counts.galRaw++;
1971
+ const r = feedGalBlock(data, view, b, id, galAsm);
1972
+ if (r.eph) addEph(r.eph);
1973
+ else if (r.badCrc) counts.badFrames++;
1974
+ return;
1975
+ }
1976
+ case 4026: {
1977
+ if (len < 32) return;
1978
+ counts.gloRaw++;
1979
+ const r = feedGloBlock(view, b, gloAsm);
1980
+ if (r.eph) addEph(r.eph);
1981
+ else if (r.badCrc) counts.badFrames++;
1982
+ return;
1983
+ }
1984
+ case 4047: {
1985
+ if (len < 60) return;
1986
+ counts.bdsRaw++;
1987
+ const r = feedBdsBlock(view, b, bdsAsm);
1988
+ if (r.eph) addEph(r.eph);
1989
+ else if (r.badCrc) counts.badFrames++;
1990
+ return;
1991
+ }
1992
+ /* ---- iono / UTC ---- */
1993
+ case 5893:
1994
+ // GPSIon
1995
+ case 4120: {
1996
+ if (len < 48) return;
1997
+ const alpha = f4s(b + 16, 4);
1998
+ const beta = f4s(b + 32, 4);
1999
+ if (!alpha || !beta) return;
2000
+ const sys = id === 5893 ? "GPS" : "BDS";
2001
+ ionoCorrections[`${sys}A`] = alpha;
2002
+ ionoCorrections[`${sys}B`] = beta;
2003
+ return;
2004
+ }
2005
+ case 4030: {
2006
+ if (len < 28) return;
2007
+ const ai = f4s(b + 16, 3);
2008
+ if (ai) ionoCorrections["GAL"] = ai;
2009
+ return;
2010
+ }
2011
+ case 5894:
2012
+ if (len >= 37) leapSeconds = view.getInt8(b + 33);
2013
+ return;
1653
2014
  }
1654
- const eph = assembler.push(
1655
- prn,
1656
- str,
1657
- new Date(GPS_EPOCH_MS6 + wnc * MS_PER_WEEK3 + towMs),
1658
- view.getUint8(b + 18) - 8
1659
- // FreqNr, offset by 8
1660
- );
1661
- if (eph) ephemerides.push(eph);
1662
2015
  });
1663
- return { ephemerides, badCrc, messages };
2016
+ return { ephemerides, cnav, ionoCorrections, leapSeconds, counts };
1664
2017
  }
1665
2018
 
1666
2019
  // src/navbits/rs255.ts
@@ -2855,11 +3208,13 @@ function parseSbfMeas(data) {
2855
3208
  }
2856
3209
  // Annotate the CommonJS export names for ESM import in node:
2857
3210
  0 && (module.exports = {
3211
+ decodeSbfNavigation,
2858
3212
  parseSbfAlmanac,
2859
3213
  parseSbfBdsNav,
2860
3214
  parseSbfCnav,
2861
3215
  parseSbfGalNav,
2862
3216
  parseSbfGloNav,
3217
+ parseSbfGpsNav,
2863
3218
  parseSbfHas,
2864
3219
  parseSbfIonoUtc,
2865
3220
  parseSbfMeas,