gnss-js 1.16.0 → 1.18.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/ubx.cjs CHANGED
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/ubx/index.ts
21
21
  var ubx_exports = {};
22
22
  __export(ubx_exports, {
23
+ parseUbxCnav: () => parseUbxCnav,
24
+ parseUbxIonoUtc: () => parseUbxIonoUtc,
23
25
  parseUbxNav: () => parseUbxNav,
24
26
  parseUbxRawx: () => parseUbxRawx,
25
27
  ubxFrames: () => ubxFrames
@@ -201,6 +203,30 @@ function decodeGpsLnavFrame(subframes, opts = {}) {
201
203
 
202
204
  // src/ubx/nav.ts
203
205
  var PREAMB_CNAV = 139;
206
+ function readLnavSubframe(view, f) {
207
+ const p = f.payload;
208
+ if (p.length < 8) return null;
209
+ const gnssId = p[0];
210
+ const svId = p[1];
211
+ let prn;
212
+ if (gnssId === 0 && svId >= 1 && svId <= 32) {
213
+ prn = `G${String(svId).padStart(2, "0")}`;
214
+ } else if (gnssId === 5 && svId >= 1 && svId <= 10) {
215
+ if (p.length === 44) return null;
216
+ prn = `J${String(svId).padStart(2, "0")}`;
217
+ } else {
218
+ return null;
219
+ }
220
+ if (p.length < 8 + 40) return null;
221
+ const base = f.payloadStart + 8;
222
+ if (view.getUint32(base, true) >>> 24 === PREAMB_CNAV) return null;
223
+ const buff = new Uint8Array(30);
224
+ for (let k = 0; k < 10; k++) {
225
+ const dwrd = view.getUint32(base + 4 * k, true);
226
+ setBitU(buff, 24 * k, 24, dwrd >>> 6 & 16777215);
227
+ }
228
+ return { prn, gnssId, svId, buff, id: getBitU(buff, 43, 3) };
229
+ }
204
230
  function parseUbxNav(data, opts = {}) {
205
231
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
206
232
  const ephemerides = [];
@@ -222,28 +248,9 @@ function parseUbxNav(data, opts = {}) {
222
248
  const last = /* @__PURE__ */ new Map();
223
249
  for (const f of ubxFrames(data)) {
224
250
  if (f.msgClass !== 2 || f.msgId !== 19) continue;
225
- const p = f.payload;
226
- if (p.length < 8) continue;
227
- const gnssId = p[0];
228
- const svId = p[1];
229
- let prn;
230
- if (gnssId === 0 && svId >= 1 && svId <= 32) {
231
- prn = `G${String(svId).padStart(2, "0")}`;
232
- } else if (gnssId === 5 && svId >= 1 && svId <= 10) {
233
- if (p.length === 44) continue;
234
- prn = `J${String(svId).padStart(2, "0")}`;
235
- } else {
236
- continue;
237
- }
238
- if (p.length < 8 + 40) continue;
239
- const base = f.payloadStart + 8;
240
- if (view.getUint32(base, true) >>> 24 === PREAMB_CNAV) continue;
241
- const buff = new Uint8Array(30);
242
- for (let k = 0; k < 10; k++) {
243
- const dwrd = view.getUint32(base + 4 * k, true);
244
- setBitU(buff, 24 * k, 24, dwrd >>> 6 & 16777215);
245
- }
246
- const id = getBitU(buff, 43, 3);
251
+ const lnav = readLnavSubframe(view, f);
252
+ if (!lnav) continue;
253
+ const { prn, gnssId, svId, buff, id } = lnav;
247
254
  if (id < 1 || id > 5) {
248
255
  badParity++;
249
256
  continue;
@@ -273,8 +280,266 @@ function parseUbxNav(data, opts = {}) {
273
280
  return { ephemerides, badParity };
274
281
  }
275
282
 
276
- // src/ubx/index.ts
283
+ // src/ubx/iono.ts
284
+ function parseUbxIonoUtc(data) {
285
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
286
+ const ionoCorrections = {};
287
+ let leapSeconds = null;
288
+ for (const f of ubxFrames(data)) {
289
+ if (f.msgClass !== 2 || f.msgId !== 19) continue;
290
+ const sf = readLnavSubframe(view, f);
291
+ if (!sf || sf.gnssId !== 0 || sf.id !== 4) continue;
292
+ const b = sf.buff;
293
+ if (getBitU(b, 48, 2) !== 1 || getBitU(b, 50, 6) !== 56) continue;
294
+ ionoCorrections["GPSA"] = [
295
+ getBitS(b, 56, 8) * 2 ** -30,
296
+ getBitS(b, 64, 8) * 2 ** -27,
297
+ getBitS(b, 72, 8) * 2 ** -24,
298
+ getBitS(b, 80, 8) * 2 ** -24
299
+ ];
300
+ ionoCorrections["GPSB"] = [
301
+ getBitS(b, 88, 8) * 2 ** 11,
302
+ getBitS(b, 96, 8) * 2 ** 14,
303
+ getBitS(b, 104, 8) * 2 ** 16,
304
+ getBitS(b, 112, 8) * 2 ** 16
305
+ ];
306
+ leapSeconds = getBitS(b, 192, 8);
307
+ }
308
+ return { ionoCorrections, leapSeconds };
309
+ }
310
+
311
+ // src/navbits/cnav.ts
277
312
  var GPS_EPOCH_MS2 = Date.UTC(1980, 0, 6);
313
+ var SEC_PER_WEEK2 = 7 * 86400;
314
+ var HALF_WEEK = 302400;
315
+ var CNAV_PREAMBLE = 139;
316
+ var CNAV_A_REF = 26559710;
317
+ var CNAV_OMEGA_DOT_REF = -26e-10;
318
+ function crc24q(buff, bitLen) {
319
+ let crc = 0;
320
+ for (let i = 0; i < bitLen; i++) {
321
+ crc ^= (buff[i >> 3] >> 7 - (i & 7) & 1) << 23;
322
+ const msb = crc & 8388608;
323
+ crc = crc << 1 & 16777215;
324
+ if (msb) crc ^= 8801531;
325
+ }
326
+ return crc;
327
+ }
328
+ function cnavCrcOk(msg) {
329
+ if (msg.length < 38) return false;
330
+ return crc24q(msg, 276) === getBitU(msg, 276, 24);
331
+ }
332
+ function getBitU33(b, pos) {
333
+ return getBitU(b, pos, 1) * 2 ** 32 + getBitU(b, pos + 1, 32);
334
+ }
335
+ function getBitS33(b, pos) {
336
+ const u = getBitU33(b, pos);
337
+ return u < 2 ** 32 ? u : u - 2 ** 33;
338
+ }
339
+ function decodeType10(b, tow) {
340
+ return {
341
+ tow,
342
+ week: getBitU(b, 38, 13),
343
+ health: getBitU(b, 51, 3),
344
+ top: getBitU(b, 54, 11) * 300,
345
+ uraEd: getBitS(b, 65, 5),
346
+ toe: getBitU(b, 70, 11) * 300,
347
+ deltaA: getBitS(b, 81, 26) * 2 ** -9,
348
+ aDot: getBitS(b, 107, 25) * 2 ** -21,
349
+ deltaN0: getBitS(b, 132, 17) * 2 ** -44 * GPS_PI,
350
+ deltaN0Dot: getBitS(b, 149, 23) * 2 ** -57 * GPS_PI,
351
+ m0: getBitS33(b, 172) * 2 ** -32 * GPS_PI,
352
+ e: getBitU33(b, 205) * 2 ** -34,
353
+ omega: getBitS33(b, 238) * 2 ** -32 * GPS_PI,
354
+ integrityFlag: getBitU(b, 271, 1) === 1,
355
+ l2cPhasing: getBitU(b, 272, 1) === 1
356
+ };
357
+ }
358
+ function decodeType11(b) {
359
+ return {
360
+ toe: getBitU(b, 38, 11) * 300,
361
+ omega0: getBitS33(b, 49) * 2 ** -32 * GPS_PI,
362
+ i0: getBitS33(b, 82) * 2 ** -32 * GPS_PI,
363
+ deltaOmegaDot: getBitS(b, 115, 17) * 2 ** -44 * GPS_PI,
364
+ i0Dot: getBitS(b, 132, 15) * 2 ** -44 * GPS_PI,
365
+ cis: getBitS(b, 147, 16) * 2 ** -30,
366
+ cic: getBitS(b, 163, 16) * 2 ** -30,
367
+ crs: getBitS(b, 179, 24) * 2 ** -8,
368
+ crc: getBitS(b, 203, 24) * 2 ** -8,
369
+ cus: getBitS(b, 227, 21) * 2 ** -30,
370
+ cuc: getBitS(b, 248, 21) * 2 ** -30
371
+ };
372
+ }
373
+ var isc = (b, pos) => {
374
+ const raw = getBitS(b, pos, 13);
375
+ return raw === -4096 ? null : raw * 2 ** -35;
376
+ };
377
+ function decodeClock(b, msgType) {
378
+ const clock = {
379
+ msgType,
380
+ top: getBitU(b, 38, 11) * 300,
381
+ uraNed0: getBitS(b, 49, 5),
382
+ uraNed1: getBitU(b, 54, 3),
383
+ uraNed2: getBitU(b, 57, 3),
384
+ toc: getBitU(b, 60, 11) * 300,
385
+ af0: getBitS(b, 71, 26) * 2 ** -35,
386
+ af1: getBitS(b, 97, 20) * 2 ** -48,
387
+ af2: getBitS(b, 117, 10) * 2 ** -60
388
+ };
389
+ if (msgType === 30) {
390
+ clock.tgd = isc(b, 127);
391
+ clock.iscL1ca = isc(b, 140);
392
+ clock.iscL2c = isc(b, 153);
393
+ clock.iscL5i5 = isc(b, 166);
394
+ clock.iscL5q5 = isc(b, 179);
395
+ clock.ionoAlpha = [
396
+ getBitS(b, 192, 8) * 2 ** -30,
397
+ getBitS(b, 200, 8) * 2 ** -27,
398
+ getBitS(b, 208, 8) * 2 ** -24,
399
+ getBitS(b, 216, 8) * 2 ** -24
400
+ ];
401
+ clock.ionoBeta = [
402
+ getBitS(b, 224, 8) * 2 ** 11,
403
+ getBitS(b, 232, 8) * 2 ** 14,
404
+ getBitS(b, 240, 8) * 2 ** 16,
405
+ getBitS(b, 248, 8) * 2 ** 16
406
+ ];
407
+ clock.wnOp = getBitU(b, 256, 8);
408
+ }
409
+ return clock;
410
+ }
411
+ function resolveWeek(week, towSec, sec) {
412
+ if (sec < towSec - HALF_WEEK) return week + 1;
413
+ if (sec > towSec + HALF_WEEK) return week - 1;
414
+ return week;
415
+ }
416
+ function gpsDate(week, sec) {
417
+ return new Date(GPS_EPOCH_MS2 + (week * SEC_PER_WEEK2 + sec) * 1e3);
418
+ }
419
+ var CnavAssembler = class {
420
+ sats = /* @__PURE__ */ new Map();
421
+ /**
422
+ * Push one 300-bit CNAV message (38+ bytes, bit 0 = first bit of
423
+ * the preamble). Returns the newly completed ephemeris, or null.
424
+ * Messages that are not MT10/MT11/MT30-37, or whose preamble/PRN
425
+ * are out of range, are ignored.
426
+ */
427
+ push(msg) {
428
+ if (msg.length < 38 || getBitU(msg, 0, 8) !== CNAV_PREAMBLE) return null;
429
+ const prn = getBitU(msg, 8, 6);
430
+ if (prn < 1 || prn > 32) return null;
431
+ const type = getBitU(msg, 14, 6);
432
+ const tow = getBitU(msg, 20, 17) * 6;
433
+ let sat = this.sats.get(prn);
434
+ if (!sat) {
435
+ sat = {};
436
+ this.sats.set(prn, sat);
437
+ }
438
+ if (type === 10) sat.t10 = decodeType10(msg, tow);
439
+ else if (type === 11) sat.t11 = decodeType11(msg);
440
+ else if (type >= 30 && type <= 37) {
441
+ sat.clock = decodeClock(msg, type);
442
+ if (type === 30) sat.extras = sat.clock;
443
+ } else return null;
444
+ return this.tryEmit(prn, sat);
445
+ }
446
+ tryEmit(prn, sat) {
447
+ const { t10, t11, clock, extras } = sat;
448
+ if (!t10 || !t11 || !clock) return null;
449
+ if (t10.toe !== t11.toe || clock.toc !== t10.toe) return null;
450
+ const key = `${t10.week}:${t10.toe}:${clock.af0}:${clock.af1}`;
451
+ if (key === sat.lastKey) return null;
452
+ sat.lastKey = key;
453
+ const weekToe = resolveWeek(t10.week, t10.tow, t10.toe);
454
+ const a = CNAV_A_REF + t10.deltaA;
455
+ return {
456
+ system: "G",
457
+ prn: `G${String(prn).padStart(2, "0")}`,
458
+ week: t10.week,
459
+ health: t10.health,
460
+ uraEd: t10.uraEd,
461
+ uraNed0: clock.uraNed0,
462
+ uraNed1: clock.uraNed1,
463
+ uraNed2: clock.uraNed2,
464
+ top: t10.top,
465
+ toe: t10.toe,
466
+ toeDate: gpsDate(weekToe, t10.toe),
467
+ a,
468
+ deltaA: t10.deltaA,
469
+ aDot: t10.aDot,
470
+ deltaN0: t10.deltaN0,
471
+ deltaN0Dot: t10.deltaN0Dot,
472
+ m0: t10.m0,
473
+ e: t10.e,
474
+ omega: t10.omega,
475
+ omega0: t11.omega0,
476
+ i0: t11.i0,
477
+ omegaDot: CNAV_OMEGA_DOT_REF * GPS_PI + t11.deltaOmegaDot,
478
+ deltaOmegaDot: t11.deltaOmegaDot,
479
+ i0Dot: t11.i0Dot,
480
+ cis: t11.cis,
481
+ cic: t11.cic,
482
+ crs: t11.crs,
483
+ crc: t11.crc,
484
+ cus: t11.cus,
485
+ cuc: t11.cuc,
486
+ toc: clock.toc,
487
+ tocDate: gpsDate(weekToe, clock.toc),
488
+ af0: clock.af0,
489
+ af1: clock.af1,
490
+ af2: clock.af2,
491
+ clockMsgType: clock.msgType,
492
+ tgd: extras?.tgd ?? null,
493
+ iscL1ca: extras?.iscL1ca ?? null,
494
+ iscL2c: extras?.iscL2c ?? null,
495
+ iscL5i5: extras?.iscL5i5 ?? null,
496
+ iscL5q5: extras?.iscL5q5 ?? null,
497
+ ...extras?.ionoAlpha && { ionoAlpha: extras.ionoAlpha },
498
+ ...extras?.ionoBeta && { ionoBeta: extras.ionoBeta },
499
+ ...extras?.wnOp !== void 0 && { wnOp: extras.wnOp },
500
+ integrityFlag: t10.integrityFlag,
501
+ l2cPhasing: t10.l2cPhasing,
502
+ tow: t10.tow
503
+ };
504
+ }
505
+ };
506
+
507
+ // src/ubx/cnav.ts
508
+ function parseUbxCnav(data) {
509
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
510
+ const assembler = new CnavAssembler();
511
+ const ephemerides = [];
512
+ let badCrc = 0;
513
+ let messages = 0;
514
+ for (const f of ubxFrames(data)) {
515
+ if (f.msgClass !== 2 || f.msgId !== 19) continue;
516
+ const p = f.payload;
517
+ if (p.length < 8 + 40) continue;
518
+ if (p[0] !== 0 || p[2] !== 3 && p[2] !== 4) continue;
519
+ const svId = p[1];
520
+ if (svId < 1 || svId > 32) continue;
521
+ messages++;
522
+ const msg = new Uint8Array(40);
523
+ const base = f.payloadStart + 8;
524
+ for (let k = 0; k < 10; k++) {
525
+ const w = view.getUint32(base + 4 * k, true);
526
+ msg[4 * k] = w >>> 24;
527
+ msg[4 * k + 1] = w >>> 16 & 255;
528
+ msg[4 * k + 2] = w >>> 8 & 255;
529
+ msg[4 * k + 3] = w & 255;
530
+ }
531
+ if (!cnavCrcOk(msg)) {
532
+ badCrc++;
533
+ continue;
534
+ }
535
+ const eph = assembler.push(msg);
536
+ if (eph) ephemerides.push(eph);
537
+ }
538
+ return { ephemerides, badCrc, messages };
539
+ }
540
+
541
+ // src/ubx/index.ts
542
+ var GPS_EPOCH_MS3 = Date.UTC(1980, 0, 6);
278
543
  var MS_PER_WEEK = 7 * 864e5;
279
544
  var SIGNALS = {
280
545
  0: { 0: ["G", "1C"], 3: ["G", "2X"], 4: ["G", "2X"] },
@@ -357,7 +622,7 @@ function parseUbxRawx(data) {
357
622
  if (!codes.includes(sig[1])) codes.push(sig[1]);
358
623
  }
359
624
  epochs.push({
360
- timeMs: GPS_EPOCH_MS2 + week * MS_PER_WEEK + Math.round(rcvTow * 1e3),
625
+ timeMs: GPS_EPOCH_MS3 + week * MS_PER_WEEK + Math.round(rcvTow * 1e3),
361
626
  leapS: leapValid ? leapS : null,
362
627
  meas
363
628
  });
@@ -367,6 +632,8 @@ function parseUbxRawx(data) {
367
632
  }
368
633
  // Annotate the CommonJS export names for ESM import in node:
369
634
  0 && (module.exports = {
635
+ parseUbxCnav,
636
+ parseUbxIonoUtc,
370
637
  parseUbxNav,
371
638
  parseUbxRawx,
372
639
  ubxFrames
package/dist/ubx.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { E as Ephemeris } from './nav-BAI1a9vK.cjs';
2
+ import { C as CnavEphemeris } from './cnav-CRns0ECn.cjs';
2
3
 
3
4
  /**
4
5
  * u-blox UBX framing shared by the measurement and navigation decoders:
@@ -46,7 +47,9 @@ declare function ubxFrames(data: Uint8Array, stats?: {
46
47
  *
47
48
  * Other constellations in RXM-SFRBX (Galileo I/NAV, BDS D1/D2, GLONASS
48
49
  * strings, SBAS) and GPS/QZSS CNAV are out of scope and skipped
49
- * silently, as are LNAV subframes 4/5 (iono/UTC/almanac pages).
50
+ * silently. LNAV subframes 4/5 are skipped here except that
51
+ * `parseUbxIonoUtc` (./iono.ts) decodes the subframe-4 page-18
52
+ * iono/UTC parameters through the shared `readLnavSubframe` helper.
50
53
  */
51
54
 
52
55
  interface UbxNavOptions {
@@ -77,6 +80,74 @@ interface UbxNavResult {
77
80
  */
78
81
  declare function parseUbxNav(data: Uint8Array, opts?: UbxNavOptions): UbxNavResult;
79
82
 
83
+ /**
84
+ * u-blox UBX ionosphere / UTC decoding: the GPS LNAV subframe 4
85
+ * page 18 broadcast (Klobuchar coefficients + GPS-UTC parameters)
86
+ * from RXM-SFRBX.
87
+ *
88
+ * Ported from RTKLIB demo5 (rtklibexplorer fork, src/rcvraw.c:
89
+ * decode_frame_ion / decode_frame_utc, Copyright (c) 2007-2020
90
+ * T. Takasu, BSD-2-Clause); frame extraction is shared with
91
+ * `parseUbxNav` through `readLnavSubframe` (./nav.ts).
92
+ *
93
+ * Scope is GPS satellites (gnssId 0) only: QZSS also broadcasts
94
+ * page-18-format iono parameters, but those are the separate
95
+ * wide-area/Japan-area QZSS coefficient set, not the GPS one a RINEX
96
+ * `GPSA`/`GPSB` header carries. Scale factors follow IS-GPS-200
97
+ * §20.3.3.5.1 (alpha_n in s/semicircle^n, beta_n in s/semicircle^n) —
98
+ * the same semicircle-based units the RINEX nav header prints, so the
99
+ * output matches `parseNavFile` on a converted header. Like
100
+ * `parseUbxNav`, the decoder never consults the system clock (nothing
101
+ * here needs a week reference).
102
+ */
103
+ interface UbxIonoUtcResult {
104
+ /**
105
+ * Iono coefficient sets keyed like `NavHeader.ionoCorrections`:
106
+ * `GPSA` (alpha_0..3) and `GPSB` (beta_0..3). The page repeats
107
+ * (nominally every 12.5 min per satellite); the last broadcast in
108
+ * the stream wins.
109
+ */
110
+ ionoCorrections: Record<string, number[]>;
111
+ /** GPS-UTC ΔtLS from the last page-18 broadcast, if any. */
112
+ leapSeconds: number | null;
113
+ }
114
+ /**
115
+ * Decode every GPS LNAV subframe 4 page 18 (data ID 1, SV ID 56) in a
116
+ * UBX byte stream (RXM-SFRBX) into Klobuchar iono coefficients and the
117
+ * current leap-second count.
118
+ */
119
+ declare function parseUbxIonoUtc(data: Uint8Array): UbxIonoUtcResult;
120
+
121
+ /**
122
+ * u-blox UBX raw CNAV decoding: GPS L2C CNAV messages from RXM-SFRBX
123
+ * (class 0x02, id 0x13), gnssId 0 with sigId 3 (L2 CL) or 4 (L2 CM).
124
+ *
125
+ * Word packing: the u-blox interface description delivers the 300-bit
126
+ * CNAV message as 10 32-bit `dwrd` words (little-endian in the
127
+ * stream) with the message bits left-justified MSB-first — message
128
+ * bit 0 is bit 31 of dwrd[0], and the low 20 bits of dwrd[9] are
129
+ * padding. Verified empirically on a ZED-F9P capture: this unpacking
130
+ * yields a valid CRC-24Q (and the 0x8B preamble in the top byte of
131
+ * dwrd[0], the same property `readLnavSubframe` in ./nav uses to skip
132
+ * CNAV frames) on effectively every message. RTKLIB demo5 does not
133
+ * decode CNAV from u-blox receivers; the field decoding lives in
134
+ * `src/navbits/cnav.ts`, working from IS-GPS-200 directly.
135
+ */
136
+
137
+ interface UbxCnavResult {
138
+ /** Assembled CNAV ephemerides in stream order, repeats suppressed. */
139
+ ephemerides: CnavEphemeris[];
140
+ /** Raw messages whose CRC-24Q check failed (dropped). */
141
+ badCrc: number;
142
+ /** Total GPS L2C RXM-SFRBX messages seen (checksum-valid frames). */
143
+ messages: number;
144
+ }
145
+ /**
146
+ * Decode every GPS L2C CNAV message in a UBX byte stream (RXM-SFRBX,
147
+ * gnssId 0, sigId 3/4) and assemble them into ephemerides.
148
+ */
149
+ declare function parseUbxCnav(data: Uint8Array): UbxCnavResult;
150
+
80
151
  /**
81
152
  * u-blox UBX raw-measurement decoding (RXM-RAWX) — the path from a
82
153
  * receiver log to RINEX-grade observables.
@@ -133,4 +204,4 @@ interface UbxParseResult {
133
204
  */
134
205
  declare function parseUbxRawx(data: Uint8Array): UbxParseResult;
135
206
 
136
- export { type UbxFrame, type UbxMeasurement, type UbxNavOptions, type UbxNavResult, type UbxParseResult, type UbxRawxEpoch, parseUbxNav, parseUbxRawx, ubxFrames };
207
+ export { type UbxCnavResult, type UbxFrame, type UbxIonoUtcResult, type UbxMeasurement, type UbxNavOptions, type UbxNavResult, type UbxParseResult, type UbxRawxEpoch, parseUbxCnav, parseUbxIonoUtc, parseUbxNav, parseUbxRawx, ubxFrames };
package/dist/ubx.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { E as Ephemeris } from './nav-BAI1a9vK.js';
2
+ import { C as CnavEphemeris } from './cnav-CRns0ECn.js';
2
3
 
3
4
  /**
4
5
  * u-blox UBX framing shared by the measurement and navigation decoders:
@@ -46,7 +47,9 @@ declare function ubxFrames(data: Uint8Array, stats?: {
46
47
  *
47
48
  * Other constellations in RXM-SFRBX (Galileo I/NAV, BDS D1/D2, GLONASS
48
49
  * strings, SBAS) and GPS/QZSS CNAV are out of scope and skipped
49
- * silently, as are LNAV subframes 4/5 (iono/UTC/almanac pages).
50
+ * silently. LNAV subframes 4/5 are skipped here except that
51
+ * `parseUbxIonoUtc` (./iono.ts) decodes the subframe-4 page-18
52
+ * iono/UTC parameters through the shared `readLnavSubframe` helper.
50
53
  */
51
54
 
52
55
  interface UbxNavOptions {
@@ -77,6 +80,74 @@ interface UbxNavResult {
77
80
  */
78
81
  declare function parseUbxNav(data: Uint8Array, opts?: UbxNavOptions): UbxNavResult;
79
82
 
83
+ /**
84
+ * u-blox UBX ionosphere / UTC decoding: the GPS LNAV subframe 4
85
+ * page 18 broadcast (Klobuchar coefficients + GPS-UTC parameters)
86
+ * from RXM-SFRBX.
87
+ *
88
+ * Ported from RTKLIB demo5 (rtklibexplorer fork, src/rcvraw.c:
89
+ * decode_frame_ion / decode_frame_utc, Copyright (c) 2007-2020
90
+ * T. Takasu, BSD-2-Clause); frame extraction is shared with
91
+ * `parseUbxNav` through `readLnavSubframe` (./nav.ts).
92
+ *
93
+ * Scope is GPS satellites (gnssId 0) only: QZSS also broadcasts
94
+ * page-18-format iono parameters, but those are the separate
95
+ * wide-area/Japan-area QZSS coefficient set, not the GPS one a RINEX
96
+ * `GPSA`/`GPSB` header carries. Scale factors follow IS-GPS-200
97
+ * §20.3.3.5.1 (alpha_n in s/semicircle^n, beta_n in s/semicircle^n) —
98
+ * the same semicircle-based units the RINEX nav header prints, so the
99
+ * output matches `parseNavFile` on a converted header. Like
100
+ * `parseUbxNav`, the decoder never consults the system clock (nothing
101
+ * here needs a week reference).
102
+ */
103
+ interface UbxIonoUtcResult {
104
+ /**
105
+ * Iono coefficient sets keyed like `NavHeader.ionoCorrections`:
106
+ * `GPSA` (alpha_0..3) and `GPSB` (beta_0..3). The page repeats
107
+ * (nominally every 12.5 min per satellite); the last broadcast in
108
+ * the stream wins.
109
+ */
110
+ ionoCorrections: Record<string, number[]>;
111
+ /** GPS-UTC ΔtLS from the last page-18 broadcast, if any. */
112
+ leapSeconds: number | null;
113
+ }
114
+ /**
115
+ * Decode every GPS LNAV subframe 4 page 18 (data ID 1, SV ID 56) in a
116
+ * UBX byte stream (RXM-SFRBX) into Klobuchar iono coefficients and the
117
+ * current leap-second count.
118
+ */
119
+ declare function parseUbxIonoUtc(data: Uint8Array): UbxIonoUtcResult;
120
+
121
+ /**
122
+ * u-blox UBX raw CNAV decoding: GPS L2C CNAV messages from RXM-SFRBX
123
+ * (class 0x02, id 0x13), gnssId 0 with sigId 3 (L2 CL) or 4 (L2 CM).
124
+ *
125
+ * Word packing: the u-blox interface description delivers the 300-bit
126
+ * CNAV message as 10 32-bit `dwrd` words (little-endian in the
127
+ * stream) with the message bits left-justified MSB-first — message
128
+ * bit 0 is bit 31 of dwrd[0], and the low 20 bits of dwrd[9] are
129
+ * padding. Verified empirically on a ZED-F9P capture: this unpacking
130
+ * yields a valid CRC-24Q (and the 0x8B preamble in the top byte of
131
+ * dwrd[0], the same property `readLnavSubframe` in ./nav uses to skip
132
+ * CNAV frames) on effectively every message. RTKLIB demo5 does not
133
+ * decode CNAV from u-blox receivers; the field decoding lives in
134
+ * `src/navbits/cnav.ts`, working from IS-GPS-200 directly.
135
+ */
136
+
137
+ interface UbxCnavResult {
138
+ /** Assembled CNAV ephemerides in stream order, repeats suppressed. */
139
+ ephemerides: CnavEphemeris[];
140
+ /** Raw messages whose CRC-24Q check failed (dropped). */
141
+ badCrc: number;
142
+ /** Total GPS L2C RXM-SFRBX messages seen (checksum-valid frames). */
143
+ messages: number;
144
+ }
145
+ /**
146
+ * Decode every GPS L2C CNAV message in a UBX byte stream (RXM-SFRBX,
147
+ * gnssId 0, sigId 3/4) and assemble them into ephemerides.
148
+ */
149
+ declare function parseUbxCnav(data: Uint8Array): UbxCnavResult;
150
+
80
151
  /**
81
152
  * u-blox UBX raw-measurement decoding (RXM-RAWX) — the path from a
82
153
  * receiver log to RINEX-grade observables.
@@ -133,4 +204,4 @@ interface UbxParseResult {
133
204
  */
134
205
  declare function parseUbxRawx(data: Uint8Array): UbxParseResult;
135
206
 
136
- export { type UbxFrame, type UbxMeasurement, type UbxNavOptions, type UbxNavResult, type UbxParseResult, type UbxRawxEpoch, parseUbxNav, parseUbxRawx, ubxFrames };
207
+ export { type UbxCnavResult, type UbxFrame, type UbxIonoUtcResult, type UbxMeasurement, type UbxNavOptions, type UbxNavResult, type UbxParseResult, type UbxRawxEpoch, parseUbxCnav, parseUbxIonoUtc, parseUbxNav, parseUbxRawx, ubxFrames };
package/dist/ubx.js CHANGED
@@ -1,10 +1,15 @@
1
1
  import {
2
+ parseUbxCnav,
3
+ parseUbxIonoUtc,
2
4
  parseUbxNav,
3
5
  parseUbxRawx,
4
6
  ubxFrames
5
- } from "./chunk-6CX6EXRK.js";
6
- import "./chunk-4LVD4DYL.js";
7
+ } from "./chunk-FEGLXNWA.js";
8
+ import "./chunk-Y5IWUPJQ.js";
9
+ import "./chunk-IQ5OIMQD.js";
7
10
  export {
11
+ parseUbxCnav,
12
+ parseUbxIonoUtc,
8
13
  parseUbxNav,
9
14
  parseUbxRawx,
10
15
  ubxFrames
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "Comprehensive GNSS library for JavaScript — time scales, coordinates, RINEX parsing, RTCM3 decoding, orbit computation, and signal analysis",
5
5
  "type": "module",
6
6
  "sideEffects": false,