gnss-js 1.36.0 → 1.38.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,15 +20,18 @@ 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,
31
33
  parseSbfNav: () => parseSbfNav,
34
+ parseSbfReceiverSetup: () => parseSbfReceiverSetup,
32
35
  sbfCrc16: () => crc16,
33
36
  scanSbfFrames: () => scanSbfFrames
34
37
  });
@@ -528,6 +531,74 @@ function parseSbfIonoUtc(data) {
528
531
  return { ionoCorrections, leapSeconds };
529
532
  }
530
533
 
534
+ // src/constants/wgs84.ts
535
+ var WGS84_SEMI_MAJOR_AXIS = 6378137;
536
+ var WGS84_FLATTENING = 1 / 298.257223563;
537
+ var WGS84_ECCENTRICITY_SQUARED = 0.006694379990197;
538
+
539
+ // src/coordinates/ecef.ts
540
+ function geodeticToEcef(lat, lon, h) {
541
+ const N = WGS84_SEMI_MAJOR_AXIS / Math.sqrt(1 - WGS84_ECCENTRICITY_SQUARED * Math.sin(lat) ** 2);
542
+ const x = (N + h) * Math.cos(lat) * Math.cos(lon);
543
+ const y = (N + h) * Math.cos(lat) * Math.sin(lon);
544
+ const z = ((1 - WGS84_ECCENTRICITY_SQUARED) * N + h) * Math.sin(lat);
545
+ return [x, y, z];
546
+ }
547
+
548
+ // src/sbf/setup.ts
549
+ var F8_DNU = -2e10;
550
+ var F4_DNU3 = -2e10;
551
+ function str(data, off, n) {
552
+ let end = off;
553
+ const stop = off + n;
554
+ while (end < stop && data[end] !== 0) end++;
555
+ let s = "";
556
+ for (let i = off; i < end; i++) s += String.fromCharCode(data[i]);
557
+ return s.trim();
558
+ }
559
+ function parseSbfReceiverSetup(data) {
560
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
561
+ let out = null;
562
+ scanSbfFrames(data, view, (id, b, len) => {
563
+ if (id !== 5902 || len < 256) return;
564
+ const setup = {
565
+ markerName: str(data, b + 16, 60),
566
+ markerNumber: str(data, b + 76, 20),
567
+ observer: str(data, b + 96, 20),
568
+ agency: str(data, b + 116, 40),
569
+ rxSerialNumber: str(data, b + 156, 20),
570
+ rxName: str(data, b + 176, 20),
571
+ rxVersion: str(data, b + 196, 20),
572
+ antSerialNumber: str(data, b + 216, 20),
573
+ antType: str(data, b + 236, 20),
574
+ deltaH: len >= 268 ? view.getFloat32(b + 256, true) : 0,
575
+ deltaE: len >= 268 ? view.getFloat32(b + 260, true) : 0,
576
+ deltaN: len >= 268 ? view.getFloat32(b + 264, true) : 0,
577
+ markerType: len >= 288 ? str(data, b + 268, 20) : null,
578
+ gnssFwVersion: len >= 328 ? str(data, b + 288, 40) : null,
579
+ productName: len >= 368 ? str(data, b + 328, 40) : null,
580
+ latitude: null,
581
+ longitude: null,
582
+ height: null,
583
+ position: null,
584
+ stationCode: len >= 398 ? str(data, b + 388, 10) : null
585
+ };
586
+ if (len >= 388) {
587
+ const lat = view.getFloat64(b + 368, true);
588
+ const lon = view.getFloat64(b + 376, true);
589
+ const h = view.getFloat32(b + 384, true);
590
+ if (lat !== F8_DNU && lon !== F8_DNU && h !== F4_DNU3) {
591
+ setup.latitude = lat;
592
+ setup.longitude = lon;
593
+ setup.height = h;
594
+ setup.position = geodeticToEcef(lat, lon, h);
595
+ }
596
+ }
597
+ out = setup;
598
+ });
599
+ return out;
600
+ }
601
+
531
602
  // src/navbits/index.ts
532
603
  var GPS_PI = 3.1415926535898;
533
604
  var GPS_EPOCH_MS2 = Date.UTC(1980, 0, 6);
@@ -544,6 +615,199 @@ function getBitS(buff, pos, len) {
544
615
  if (len <= 0 || bits < 2 ** (len - 1)) return bits;
545
616
  return bits - 2 ** len;
546
617
  }
618
+ function setBitU(buff, pos, len, data) {
619
+ let mask = 2 ** (len - 1);
620
+ for (let i = pos; i < pos + len; i++, mask /= 2) {
621
+ if (data >= mask) {
622
+ buff[i >> 3] |= 1 << 7 - (i & 7);
623
+ data -= mask;
624
+ } else {
625
+ buff[i >> 3] &= ~(1 << 7 - (i & 7));
626
+ }
627
+ }
628
+ }
629
+ var GpsLnavAssembler = class _GpsLnavAssembler {
630
+ buffers = /* @__PURE__ */ new Map();
631
+ last = /* @__PURE__ */ new Map();
632
+ /** Subframe ID (1–5) from the HOW word of a parity-stripped subframe. */
633
+ static subframeId(subframe) {
634
+ return getBitU(subframe, 43, 3);
635
+ }
636
+ /**
637
+ * Push one parity-stripped 30-byte LNAV subframe for `prn`, resolving
638
+ * the 10-bit week against `refWeek`. See {@link LnavPush} for outcomes.
639
+ */
640
+ push(prn, subframe, refWeek) {
641
+ const id = _GpsLnavAssembler.subframeId(subframe);
642
+ if (id < 1 || id > 3) return { kind: "skipped" };
643
+ let sf = this.buffers.get(prn);
644
+ if (!sf) {
645
+ sf = { buf: new Uint8Array(90), have: 0 };
646
+ this.buffers.set(prn, sf);
647
+ }
648
+ sf.buf.set(subframe, (id - 1) * 30);
649
+ sf.have |= 1 << id - 1;
650
+ if (id !== 3 || sf.have !== 7) return { kind: "incomplete" };
651
+ const eph = decodeGpsLnavFrame(sf.buf, { prn, refWeek });
652
+ if (!eph) return { kind: "decodeFailed" };
653
+ const prev = this.last.get(prn);
654
+ if (prev && prev.iode === eph.iode && prev.tocDate.getTime() === eph.tocDate.getTime()) {
655
+ return { kind: "duplicate" };
656
+ }
657
+ this.last.set(prn, eph);
658
+ return { kind: "eph", eph };
659
+ }
660
+ };
661
+ function decodeGpsLnavFrame(subframes, opts = {}) {
662
+ if (subframes.length < 90) return null;
663
+ const b = subframes;
664
+ let i = 24;
665
+ const tow1 = getBitU(b, i, 17) * 6;
666
+ i += 17 + 2;
667
+ const id1 = getBitU(b, i, 3);
668
+ i += 3 + 2;
669
+ const week10 = getBitU(b, i, 10);
670
+ i += 10;
671
+ i += 2;
672
+ i += 4;
673
+ const svHealth = getBitU(b, i, 6);
674
+ i += 6;
675
+ const iodc0 = getBitU(b, i, 2);
676
+ i += 2;
677
+ i += 1 + 87;
678
+ const tgdRaw = getBitS(b, i, 8);
679
+ i += 8;
680
+ const iodc1 = getBitU(b, i, 8);
681
+ i += 8;
682
+ const tocSec = getBitU(b, i, 16) * 16;
683
+ i += 16;
684
+ const af2 = getBitS(b, i, 8) * 2 ** -55;
685
+ i += 8;
686
+ const af1 = getBitS(b, i, 16) * 2 ** -43;
687
+ i += 16;
688
+ const af0 = getBitS(b, i, 22) * 2 ** -31;
689
+ i = 240 + 24;
690
+ i += 17 + 2;
691
+ const id2 = getBitU(b, i, 3);
692
+ i += 3 + 2;
693
+ const iode = getBitU(b, i, 8);
694
+ i += 8;
695
+ const crs = getBitS(b, i, 16) * 2 ** -5;
696
+ i += 16;
697
+ const deltaN = getBitS(b, i, 16) * 2 ** -43 * GPS_PI;
698
+ i += 16;
699
+ const m0 = getBitS(b, i, 32) * 2 ** -31 * GPS_PI;
700
+ i += 32;
701
+ const cuc = getBitS(b, i, 16) * 2 ** -29;
702
+ i += 16;
703
+ const e = getBitU(b, i, 32) * 2 ** -33;
704
+ i += 32;
705
+ const cus = getBitS(b, i, 16) * 2 ** -29;
706
+ i += 16;
707
+ const sqrtA = getBitU(b, i, 32) * 2 ** -19;
708
+ i += 32;
709
+ const toes = getBitU(b, i, 16) * 16;
710
+ i = 480 + 24;
711
+ i += 17 + 2;
712
+ const id3 = getBitU(b, i, 3);
713
+ i += 3 + 2;
714
+ const cic = getBitS(b, i, 16) * 2 ** -29;
715
+ i += 16;
716
+ const omega0 = getBitS(b, i, 32) * 2 ** -31 * GPS_PI;
717
+ i += 32;
718
+ const cis = getBitS(b, i, 16) * 2 ** -29;
719
+ i += 16;
720
+ const i0 = getBitS(b, i, 32) * 2 ** -31 * GPS_PI;
721
+ i += 32;
722
+ const crc = getBitS(b, i, 16) * 2 ** -5;
723
+ i += 16;
724
+ const omega = getBitS(b, i, 32) * 2 ** -31 * GPS_PI;
725
+ i += 32;
726
+ const omegaDot = getBitS(b, i, 24) * 2 ** -43 * GPS_PI;
727
+ i += 24;
728
+ const iode3 = getBitU(b, i, 8);
729
+ i += 8;
730
+ const idot = getBitS(b, i, 14) * 2 ** -43 * GPS_PI;
731
+ const iodc = iodc0 * 256 + iodc1;
732
+ if (id1 !== 1 || id2 !== 2 || id3 !== 3) return null;
733
+ if (iode3 !== iode || iode !== (iodc & 255)) return null;
734
+ const ref = opts.refWeek ?? Math.floor((Date.now() - GPS_EPOCH_MS2) / 1e3 / SEC_PER_WEEK2);
735
+ let week = week10 + 1024 * Math.round((ref - week10) / 1024);
736
+ if (toes < tow1 - 302400) week++;
737
+ else if (toes > tow1 + 302400) week--;
738
+ const prn = opts.prn ?? "G00";
739
+ const tocDate = new Date(
740
+ GPS_EPOCH_MS2 + (week * SEC_PER_WEEK2 + tocSec) * 1e3
741
+ );
742
+ return {
743
+ system: prn[0] === "J" ? "J" : "G",
744
+ prn,
745
+ tocDate,
746
+ // Same seconds-of-week convention as parseNavFile (rinex/nav.ts).
747
+ toc: tocDate.getTime() / 1e3 % SEC_PER_WEEK2,
748
+ af0,
749
+ af1,
750
+ af2,
751
+ iode,
752
+ crs,
753
+ deltaN,
754
+ m0,
755
+ cuc,
756
+ e,
757
+ cus,
758
+ sqrtA,
759
+ toe: toes,
760
+ cic,
761
+ omega0,
762
+ cis,
763
+ i0,
764
+ crc,
765
+ omega,
766
+ omegaDot,
767
+ idot,
768
+ week,
769
+ svHealth,
770
+ tgd: tgdRaw === -128 ? 0 : tgdRaw * 2 ** -31
771
+ // IS-GPS-200: -128 reserved
772
+ };
773
+ }
774
+
775
+ // src/sbf/rawnav-gps.ts
776
+ var WNC_DNU = 65535;
777
+ function extractSbfLnavSubframe(view, b) {
778
+ const sf = new Uint8Array(30);
779
+ for (let k = 0; k < 10; k++) {
780
+ const w = view.getUint32(b + 20 + 4 * k, true);
781
+ setBitU(sf, 24 * k, 24, w >>> 6 & 16777215);
782
+ }
783
+ return sf;
784
+ }
785
+ function feedGpsLnavBlock(view, b, assembler) {
786
+ const prn = svidToPrn(view.getUint8(b + 14));
787
+ if (!prn || prn[0] !== "G" && prn[0] !== "J") return {};
788
+ if (view.getUint8(b + 15) !== 1) return {};
789
+ const wnc = view.getUint16(b + 12, true);
790
+ if (wnc === WNC_DNU) return {};
791
+ const r = assembler.push(prn, extractSbfLnavSubframe(view, b), wnc);
792
+ if (r.kind === "eph") return { eph: r.eph };
793
+ if (r.kind === "decodeFailed") return { badParity: true };
794
+ return {};
795
+ }
796
+ function parseSbfGpsNav(data) {
797
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
798
+ const ephemerides = [];
799
+ const assembler = new GpsLnavAssembler();
800
+ let badParity = 0;
801
+ let messages = 0;
802
+ scanSbfFrames(data, view, (id, b, len) => {
803
+ if (id !== 4017 && id !== 4066 || len < 60) return;
804
+ messages++;
805
+ const r = feedGpsLnavBlock(view, b, assembler);
806
+ if (r.eph) ephemerides.push(r.eph);
807
+ else if (r.badParity) badParity++;
808
+ });
809
+ return { ephemerides, badParity, messages };
810
+ }
547
811
 
548
812
  // src/navbits/cnav.ts
549
813
  var GPS_EPOCH_MS3 = Date.UTC(1980, 0, 6);
@@ -742,37 +1006,40 @@ var CnavAssembler = class {
742
1006
  };
743
1007
 
744
1008
  // src/sbf/rawnav.ts
745
- var SIGNAL_OF_BLOCK = {
1009
+ var CNAV_SIGNAL_OF_BLOCK = {
746
1010
  4018: "L2C",
747
1011
  4019: "L5"
748
1012
  };
1013
+ function newCnavAssemblers() {
1014
+ return { L2C: new CnavAssembler(), L5: new CnavAssembler() };
1015
+ }
1016
+ function feedCnavBlock(view, b, id, asm) {
1017
+ const signal = CNAV_SIGNAL_OF_BLOCK[id];
1018
+ if (!signal) return {};
1019
+ const msg = new Uint8Array(40);
1020
+ for (let k = 0; k < 10; k++) {
1021
+ const w = view.getUint32(b + 20 + 4 * k, true);
1022
+ msg[4 * k] = w >>> 24;
1023
+ msg[4 * k + 1] = w >>> 16 & 255;
1024
+ msg[4 * k + 2] = w >>> 8 & 255;
1025
+ msg[4 * k + 3] = w & 255;
1026
+ }
1027
+ if (!cnavCrcOk(msg)) return { badCrc: true };
1028
+ const eph = asm[signal].push(msg);
1029
+ return eph ? { eph: { ...eph, signal } } : {};
1030
+ }
749
1031
  function parseSbfCnav(data) {
750
1032
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
751
1033
  const ephemerides = [];
752
- const assemblers = {
753
- L2C: new CnavAssembler(),
754
- L5: new CnavAssembler()
755
- };
1034
+ const asm = newCnavAssemblers();
756
1035
  let badCrc = 0;
757
1036
  let messages = 0;
758
1037
  scanSbfFrames(data, view, (id, b, len) => {
759
- const signal = SIGNAL_OF_BLOCK[id];
760
- if (!signal || len < 60) return;
1038
+ if (!CNAV_SIGNAL_OF_BLOCK[id] || len < 60) return;
761
1039
  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 });
1040
+ const r = feedCnavBlock(view, b, id, asm);
1041
+ if (r.eph) ephemerides.push(r.eph);
1042
+ else if (r.badCrc) badCrc++;
776
1043
  });
777
1044
  return { ephemerides, badCrc, messages };
778
1045
  }
@@ -1088,50 +1355,51 @@ var GalFnavAssembler = class {
1088
1355
  // src/sbf/rawnav-gal.ts
1089
1356
  var INAV_SOURCES = [17, 21, 22];
1090
1357
  var FNAV_SOURCES = [20, 22];
1358
+ function newGalAssemblers() {
1359
+ return { inav: new GalInavAssembler(), fnav: new GalFnavAssembler() };
1360
+ }
1361
+ function feedGalBlock(data, view, b, id, asm) {
1362
+ const prnStr = svidToPrn(data[b + 14]);
1363
+ if (!prnStr || prnStr[0] !== "E") return {};
1364
+ const prn = parseInt(prnStr.slice(1), 10);
1365
+ const source = data[b + 17] & 31;
1366
+ const page = new Uint8Array(32);
1367
+ for (let k = 0; k < 8; k++) {
1368
+ const w = view.getUint32(b + 20 + 4 * k, true);
1369
+ page[4 * k] = w >>> 24;
1370
+ page[4 * k + 1] = w >>> 16 & 255;
1371
+ page[4 * k + 2] = w >>> 8 & 255;
1372
+ page[4 * k + 3] = w & 255;
1373
+ }
1374
+ let eph;
1375
+ let src;
1376
+ if (id === 4023) {
1377
+ src = "inav";
1378
+ if (!INAV_SOURCES.includes(source)) return {};
1379
+ if (getBitU2(page, 0) !== 0 || getBitU2(page, 114) !== 1) return {};
1380
+ if (!galInavPageCrcOk(page)) return { badCrc: true };
1381
+ eph = asm.inav.push(prn, page);
1382
+ } else {
1383
+ src = "fnav";
1384
+ if (!FNAV_SOURCES.includes(source)) return {};
1385
+ if (isFnavDummy(page)) return {};
1386
+ if (!galFnavPageCrcOk(page)) return { badCrc: true };
1387
+ eph = asm.fnav.push(prn, page);
1388
+ }
1389
+ return eph ? { eph: { ...eph, source: src } } : {};
1390
+ }
1091
1391
  function parseSbfGalNav(data) {
1092
1392
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1093
1393
  const ephemerides = [];
1094
- const inav = new GalInavAssembler();
1095
- const fnav = new GalFnavAssembler();
1394
+ const asm = newGalAssemblers();
1096
1395
  let badCrc = 0;
1097
1396
  let messages = 0;
1098
1397
  scanSbfFrames(data, view, (id, b, len) => {
1099
1398
  if (id !== 4022 && id !== 4023 || len < 52) return;
1100
1399
  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 });
1400
+ const r = feedGalBlock(data, view, b, id, asm);
1401
+ if (r.eph) ephemerides.push(r.eph);
1402
+ else if (r.badCrc) badCrc++;
1135
1403
  });
1136
1404
  return { ephemerides, badCrc, messages };
1137
1405
  }
@@ -1558,9 +1826,9 @@ var GloStringAssembler = class {
1558
1826
  * Returns the newly completed ephemeris, or null. The decoded
1559
1827
  * string-4 slot number must match `prn`, or nothing is emitted.
1560
1828
  */
1561
- push(prn, str, time, freqNum = 0) {
1562
- if (str.length < GLO_STRING_BYTES) return null;
1563
- const m = getBitU(str, 1, 4);
1829
+ push(prn, str2, time, freqNum = 0) {
1830
+ if (str2.length < GLO_STRING_BYTES) return null;
1831
+ const m = getBitU(str2, 1, 4);
1564
1832
  if (m < 1) return null;
1565
1833
  const sec = Math.floor(time.getTime() / 1e3);
1566
1834
  let sat = this.sats.get(prn);
@@ -1572,7 +1840,7 @@ var GloStringAssembler = class {
1572
1840
  sat.batchSec = sec;
1573
1841
  }
1574
1842
  if (m > 4) return null;
1575
- sat.buf.set(str.subarray(0, GLO_STRING_BYTES), (m - 1) * 10);
1843
+ sat.buf.set(str2.subarray(0, GLO_STRING_BYTES), (m - 1) * 10);
1576
1844
  if (m !== 4) return null;
1577
1845
  const eph = decodeGloStrings(sat.buf, time, { freqNum });
1578
1846
  if (!eph || eph.prn !== prn) return null;
@@ -1587,7 +1855,26 @@ var GloStringAssembler = class {
1587
1855
  var GPS_EPOCH_MS6 = Date.UTC(1980, 0, 6);
1588
1856
  var MS_PER_WEEK3 = 7 * 86400 * 1e3;
1589
1857
  var TOW_DNU = 4294967295;
1590
- var WNC_DNU = 65535;
1858
+ var WNC_DNU2 = 65535;
1859
+ function feedBdsBlock(view, b, assembler) {
1860
+ const prn = svidToPrn(view.getUint8(b + 14));
1861
+ if (!prn || prn[0] !== "C") return {};
1862
+ const sf = new Uint8Array(38);
1863
+ for (let k = 0; k < 10; k++) {
1864
+ const w = view.getUint32(b + 20 + 4 * k, true);
1865
+ sf[4 * k] = w >>> 24;
1866
+ if (k < 9) {
1867
+ sf[4 * k + 1] = w >>> 16 & 255;
1868
+ sf[4 * k + 2] = w >>> 8 & 255;
1869
+ sf[4 * k + 3] = w & 255;
1870
+ } else {
1871
+ sf[37] = w >>> 16 & 240;
1872
+ }
1873
+ }
1874
+ if (!bdsSubframeParityOk(sf)) return { badCrc: true };
1875
+ const eph = assembler.push(prn, sf);
1876
+ return eph ? { eph } : {};
1877
+ }
1591
1878
  function parseSbfBdsNav(data) {
1592
1879
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1593
1880
  const ephemerides = [];
@@ -1597,29 +1884,41 @@ function parseSbfBdsNav(data) {
1597
1884
  scanSbfFrames(data, view, (id, b, len) => {
1598
1885
  if (id !== 4047 || len < 60) return;
1599
1886
  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);
1887
+ const r = feedBdsBlock(view, b, assembler);
1888
+ if (r.eph) ephemerides.push(r.eph);
1889
+ else if (r.badCrc) badCrc++;
1620
1890
  });
1621
1891
  return { ephemerides, badCrc, messages };
1622
1892
  }
1893
+ function feedGloBlock(view, b, assembler) {
1894
+ const prn = svidToPrn(view.getUint8(b + 14));
1895
+ if (!prn || prn[0] !== "R") return {};
1896
+ const towMs = view.getUint32(b + 8, true);
1897
+ const wnc = view.getUint16(b + 12, true);
1898
+ if (towMs === TOW_DNU || wnc === WNC_DNU2) return {};
1899
+ const str2 = new Uint8Array(11);
1900
+ for (let k = 0; k < 3; k++) {
1901
+ const w = view.getUint32(b + 20 + 4 * k, true);
1902
+ str2[4 * k] = w >>> 24;
1903
+ if (k < 2) {
1904
+ str2[4 * k + 1] = w >>> 16 & 255;
1905
+ str2[4 * k + 2] = w >>> 8 & 255;
1906
+ str2[4 * k + 3] = w & 255;
1907
+ } else {
1908
+ str2[9] = w >>> 16 & 255;
1909
+ str2[10] = w >>> 8 & 248;
1910
+ }
1911
+ }
1912
+ if (!testGloString(str2)) return { badCrc: true };
1913
+ const eph = assembler.push(
1914
+ prn,
1915
+ str2,
1916
+ new Date(GPS_EPOCH_MS6 + wnc * MS_PER_WEEK3 + towMs),
1917
+ view.getUint8(b + 18) - 8
1918
+ // FreqNr, offset by 8
1919
+ );
1920
+ return eph ? { eph } : {};
1921
+ }
1623
1922
  function parseSbfGloNav(data) {
1624
1923
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1625
1924
  const ephemerides = [];
@@ -1629,38 +1928,161 @@ function parseSbfGloNav(data) {
1629
1928
  scanSbfFrames(data, view, (id, b, len) => {
1630
1929
  if (id !== 4026 || len < 32) return;
1631
1930
  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
- }
1931
+ const r = feedGloBlock(view, b, assembler);
1932
+ if (r.eph) ephemerides.push(r.eph);
1933
+ else if (r.badCrc) badCrc++;
1934
+ });
1935
+ return { ephemerides, badCrc, messages };
1936
+ }
1937
+
1938
+ // src/sbf/navigation.ts
1939
+ var F4_DNU4 = -2e10;
1940
+ function ephKey(e) {
1941
+ const source = "source" in e ? e.source : "";
1942
+ return `${e.prn}|${e.tocDate.getTime()}|${source}`;
1943
+ }
1944
+ function decodeSbfNavigation(data) {
1945
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1946
+ const ephemerides = [];
1947
+ const seen = /* @__PURE__ */ new Set();
1948
+ const cnav = [];
1949
+ const ionoCorrections = {};
1950
+ let leapSeconds = null;
1951
+ const counts = {
1952
+ decodedNav: 0,
1953
+ gpsLnavRaw: 0,
1954
+ cnavRaw: 0,
1955
+ galRaw: 0,
1956
+ gloRaw: 0,
1957
+ bdsRaw: 0,
1958
+ badFrames: 0
1959
+ };
1960
+ const cnavAsm = newCnavAssemblers();
1961
+ const galAsm = newGalAssemblers();
1962
+ const gloAsm = new GloStringAssembler();
1963
+ const bdsAsm = new BdsAssembler();
1964
+ const lnavAsm = new GpsLnavAssembler();
1965
+ const addEph = (e) => {
1966
+ if (!e) return;
1967
+ const key = ephKey(e);
1968
+ if (seen.has(key)) return;
1969
+ seen.add(key);
1970
+ ephemerides.push(e);
1971
+ };
1972
+ const f4s = (b, n) => {
1973
+ const out = [];
1974
+ for (let k = 0; k < n; k++) {
1975
+ const v = view.getFloat32(b + 4 * k, true);
1976
+ if (v === F4_DNU4) return null;
1977
+ out.push(v);
1649
1978
  }
1650
- if (!testGloString(str)) {
1651
- badCrc++;
1652
- return;
1979
+ return out;
1980
+ };
1981
+ scanSbfFrames(data, view, (id, b, len) => {
1982
+ switch (id) {
1983
+ /* ---- decoded ephemeris blocks ---- */
1984
+ case 5891:
1985
+ if (len >= 140) {
1986
+ counts.decodedNav++;
1987
+ addEph(decodeGpsQzsNav(view, b, "G"));
1988
+ }
1989
+ return;
1990
+ case 4095:
1991
+ if (len >= 140) {
1992
+ counts.decodedNav++;
1993
+ addEph(decodeGpsQzsNav(view, b, "J"));
1994
+ }
1995
+ return;
1996
+ case 4002:
1997
+ if (len >= 149) {
1998
+ counts.decodedNav++;
1999
+ addEph(decodeGalNav(view, b));
2000
+ }
2001
+ return;
2002
+ case 4081:
2003
+ if (len >= 140) {
2004
+ counts.decodedNav++;
2005
+ addEph(decodeBdsNav(view, b));
2006
+ }
2007
+ return;
2008
+ case 4004:
2009
+ if (len >= 96) {
2010
+ counts.decodedNav++;
2011
+ addEph(decodeGloNav(view, b));
2012
+ }
2013
+ return;
2014
+ /* ---- raw navigation-frame blocks ---- */
2015
+ case 4017:
2016
+ // GPSRawCA
2017
+ case 4066: {
2018
+ if (len < 60) return;
2019
+ counts.gpsLnavRaw++;
2020
+ const r = feedGpsLnavBlock(view, b, lnavAsm);
2021
+ if (r.eph) addEph(r.eph);
2022
+ else if (r.badParity) counts.badFrames++;
2023
+ return;
2024
+ }
2025
+ case 4018:
2026
+ // GPSRawL2C
2027
+ case 4019: {
2028
+ if (len < 60) return;
2029
+ counts.cnavRaw++;
2030
+ const r = feedCnavBlock(view, b, id, cnavAsm);
2031
+ if (r.eph) cnav.push(r.eph);
2032
+ else if (r.badCrc) counts.badFrames++;
2033
+ return;
2034
+ }
2035
+ case 4022:
2036
+ // GALRawFNAV
2037
+ case 4023: {
2038
+ if (len < 52) return;
2039
+ counts.galRaw++;
2040
+ const r = feedGalBlock(data, view, b, id, galAsm);
2041
+ if (r.eph) addEph(r.eph);
2042
+ else if (r.badCrc) counts.badFrames++;
2043
+ return;
2044
+ }
2045
+ case 4026: {
2046
+ if (len < 32) return;
2047
+ counts.gloRaw++;
2048
+ const r = feedGloBlock(view, b, gloAsm);
2049
+ if (r.eph) addEph(r.eph);
2050
+ else if (r.badCrc) counts.badFrames++;
2051
+ return;
2052
+ }
2053
+ case 4047: {
2054
+ if (len < 60) return;
2055
+ counts.bdsRaw++;
2056
+ const r = feedBdsBlock(view, b, bdsAsm);
2057
+ if (r.eph) addEph(r.eph);
2058
+ else if (r.badCrc) counts.badFrames++;
2059
+ return;
2060
+ }
2061
+ /* ---- iono / UTC ---- */
2062
+ case 5893:
2063
+ // GPSIon
2064
+ case 4120: {
2065
+ if (len < 48) return;
2066
+ const alpha = f4s(b + 16, 4);
2067
+ const beta = f4s(b + 32, 4);
2068
+ if (!alpha || !beta) return;
2069
+ const sys = id === 5893 ? "GPS" : "BDS";
2070
+ ionoCorrections[`${sys}A`] = alpha;
2071
+ ionoCorrections[`${sys}B`] = beta;
2072
+ return;
2073
+ }
2074
+ case 4030: {
2075
+ if (len < 28) return;
2076
+ const ai = f4s(b + 16, 3);
2077
+ if (ai) ionoCorrections["GAL"] = ai;
2078
+ return;
2079
+ }
2080
+ case 5894:
2081
+ if (len >= 37) leapSeconds = view.getInt8(b + 33);
2082
+ return;
1653
2083
  }
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
2084
  });
1663
- return { ephemerides, badCrc, messages };
2085
+ return { ephemerides, cnav, ionoCorrections, leapSeconds, counts };
1664
2086
  }
1665
2087
 
1666
2088
  // src/navbits/rs255.ts
@@ -2855,15 +3277,18 @@ function parseSbfMeas(data) {
2855
3277
  }
2856
3278
  // Annotate the CommonJS export names for ESM import in node:
2857
3279
  0 && (module.exports = {
3280
+ decodeSbfNavigation,
2858
3281
  parseSbfAlmanac,
2859
3282
  parseSbfBdsNav,
2860
3283
  parseSbfCnav,
2861
3284
  parseSbfGalNav,
2862
3285
  parseSbfGloNav,
3286
+ parseSbfGpsNav,
2863
3287
  parseSbfHas,
2864
3288
  parseSbfIonoUtc,
2865
3289
  parseSbfMeas,
2866
3290
  parseSbfNav,
3291
+ parseSbfReceiverSetup,
2867
3292
  sbfCrc16,
2868
3293
  scanSbfFrames
2869
3294
  });