gnss-js 1.40.0 → 1.41.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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ getGpsLeap
3
+ } from "./chunk-HVXYFUCB.js";
4
+
1
5
  // src/trimble/frame.ts
2
6
  var STX = 2;
3
7
  var ETX = 3;
@@ -50,6 +54,7 @@ var gpsMs = (week, sec) => GPS_EPOCH_MS + week * MS_PER_WEEK + sec * 1e3;
50
54
  var sowOf = (dateMs) => dateMs / 1e3 % SEC_PER_WEEK;
51
55
  var SUB_GPS_EPHEMERIS = 1;
52
56
  var SUB_ION_UTC = 3;
57
+ var SUB_GLO_EPHEMERIS = 9;
53
58
  var SUB_BDS_EPHEMERIS = 21;
54
59
  function readKepler(view, base) {
55
60
  return {
@@ -142,6 +147,38 @@ function decodeBdsEphemeris(view, base) {
142
147
  ...keplerCommon(r)
143
148
  };
144
149
  }
150
+ function decodeGloEphemeris(view, base) {
151
+ const slot = view.getUint8(base + 5);
152
+ if (slot < 1 || slot > 24) return null;
153
+ const week = view.getUint16(base + 6, false);
154
+ const tow = view.getUint32(base + 8, false);
155
+ const gpsTimeMs = gpsMs(week, tow);
156
+ const leapMs = getGpsLeap(new Date(gpsTimeMs)) * 1e3;
157
+ const tocMs = gpsTimeMs - leapMs;
158
+ return {
159
+ system: "R",
160
+ prn: `R${String(slot).padStart(2, "0")}`,
161
+ tocDate: new Date(tocMs),
162
+ // RINEX stores −τn as the clock bias; the record carries τn (ICD sign).
163
+ tauN: -view.getFloat64(base + 113, false),
164
+ gammaN: view.getFloat64(base + 121, false),
165
+ messageFrameTime: (tocMs - GPS_EPOCH_MS) / 1e3 % SEC_PER_WEEK,
166
+ // State vector: metres / m·s⁻¹ / m·s⁻² → km / km·s⁻¹ / km·s⁻².
167
+ x: view.getFloat64(base + 33, false) / 1e3,
168
+ xDot: view.getFloat64(base + 41, false) / 1e3,
169
+ xAcc: view.getFloat64(base + 49, false) / 1e3,
170
+ y: view.getFloat64(base + 57, false) / 1e3,
171
+ yDot: view.getFloat64(base + 65, false) / 1e3,
172
+ yAcc: view.getFloat64(base + 73, false) / 1e3,
173
+ z: view.getFloat64(base + 81, false) / 1e3,
174
+ zDot: view.getFloat64(base + 89, false) / 1e3,
175
+ zAcc: view.getFloat64(base + 97, false) / 1e3,
176
+ health: 0,
177
+ // Bn health bit not located; observed sats are operational
178
+ freqNum: view.getInt8(base + 29)
179
+ // FCN, signed byte (−7…+6)
180
+ };
181
+ }
145
182
  function parseTrimbleNav(data) {
146
183
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
147
184
  const stats = { badChecksum: 0 };
@@ -171,6 +208,13 @@ function parseTrimbleNav(data) {
171
208
  ionoCorrections["GPSA"] = alpha;
172
209
  ionoCorrections["GPSB"] = beta;
173
210
  leapSeconds = Math.trunc(view.getFloat64(p + 94, false));
211
+ } else if (sub === SUB_GLO_EPHEMERIS && f.len >= 125) {
212
+ const eph = decodeGloEphemeris(view, f.start);
213
+ if (!eph) continue;
214
+ const key = eph.tocDate.getTime();
215
+ if (lastIode.get(eph.prn) === key) continue;
216
+ lastIode.set(eph.prn, key);
217
+ ephemerides.push(eph);
174
218
  }
175
219
  }
176
220
  return {
package/dist/index.cjs CHANGED
@@ -9600,6 +9600,7 @@ var gpsMs3 = (week, sec) => GPS_EPOCH_MS14 + week * MS_PER_WEEK9 + sec * 1e3;
9600
9600
  var sowOf4 = (dateMs) => dateMs / 1e3 % SEC_PER_WEEK10;
9601
9601
  var SUB_GPS_EPHEMERIS = 1;
9602
9602
  var SUB_ION_UTC = 3;
9603
+ var SUB_GLO_EPHEMERIS = 9;
9603
9604
  var SUB_BDS_EPHEMERIS = 21;
9604
9605
  function readKepler(view, base) {
9605
9606
  return {
@@ -9692,6 +9693,38 @@ function decodeBdsEphemeris3(view, base) {
9692
9693
  ...keplerCommon(r)
9693
9694
  };
9694
9695
  }
9696
+ function decodeGloEphemeris2(view, base) {
9697
+ const slot = view.getUint8(base + 5);
9698
+ if (slot < 1 || slot > 24) return null;
9699
+ const week = view.getUint16(base + 6, false);
9700
+ const tow = view.getUint32(base + 8, false);
9701
+ const gpsTimeMs = gpsMs3(week, tow);
9702
+ const leapMs = getGpsLeap(new Date(gpsTimeMs)) * 1e3;
9703
+ const tocMs = gpsTimeMs - leapMs;
9704
+ return {
9705
+ system: "R",
9706
+ prn: `R${String(slot).padStart(2, "0")}`,
9707
+ tocDate: new Date(tocMs),
9708
+ // RINEX stores −τn as the clock bias; the record carries τn (ICD sign).
9709
+ tauN: -view.getFloat64(base + 113, false),
9710
+ gammaN: view.getFloat64(base + 121, false),
9711
+ messageFrameTime: (tocMs - GPS_EPOCH_MS14) / 1e3 % SEC_PER_WEEK10,
9712
+ // State vector: metres / m·s⁻¹ / m·s⁻² → km / km·s⁻¹ / km·s⁻².
9713
+ x: view.getFloat64(base + 33, false) / 1e3,
9714
+ xDot: view.getFloat64(base + 41, false) / 1e3,
9715
+ xAcc: view.getFloat64(base + 49, false) / 1e3,
9716
+ y: view.getFloat64(base + 57, false) / 1e3,
9717
+ yDot: view.getFloat64(base + 65, false) / 1e3,
9718
+ yAcc: view.getFloat64(base + 73, false) / 1e3,
9719
+ z: view.getFloat64(base + 81, false) / 1e3,
9720
+ zDot: view.getFloat64(base + 89, false) / 1e3,
9721
+ zAcc: view.getFloat64(base + 97, false) / 1e3,
9722
+ health: 0,
9723
+ // Bn health bit not located; observed sats are operational
9724
+ freqNum: view.getInt8(base + 29)
9725
+ // FCN, signed byte (−7…+6)
9726
+ };
9727
+ }
9695
9728
  function parseTrimbleNav(data) {
9696
9729
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
9697
9730
  const stats = { badChecksum: 0 };
@@ -9721,6 +9754,13 @@ function parseTrimbleNav(data) {
9721
9754
  ionoCorrections["GPSA"] = alpha;
9722
9755
  ionoCorrections["GPSB"] = beta;
9723
9756
  leapSeconds = Math.trunc(view.getFloat64(p + 94, false));
9757
+ } else if (sub === SUB_GLO_EPHEMERIS && f.len >= 125) {
9758
+ const eph = decodeGloEphemeris2(view, f.start);
9759
+ if (!eph) continue;
9760
+ const key = eph.tocDate.getTime();
9761
+ if (lastIode.get(eph.prn) === key) continue;
9762
+ lastIode.set(eph.prn, key);
9763
+ ephemerides.push(eph);
9724
9764
  }
9725
9765
  }
9726
9766
  return {
package/dist/index.js CHANGED
@@ -79,7 +79,7 @@ import {
79
79
  parseTrimble,
80
80
  parseTrimbleNav,
81
81
  trimbleFrames
82
- } from "./chunk-J2E2IKJR.js";
82
+ } from "./chunk-EPATQ3LT.js";
83
83
  import {
84
84
  binexCrc16,
85
85
  binexCrc32,
package/dist/trimble.cjs CHANGED
@@ -66,6 +66,93 @@ function* trimbleFrames(data, stats) {
66
66
  }
67
67
  }
68
68
 
69
+ // src/constants/time.ts
70
+ var MILLISECONDS_IN_SECOND = 1e3;
71
+ var MILLISECONDS_GPS_TAI = 19e3;
72
+ var START_NTP_TIME = /* @__PURE__ */ new Date("1900-01-01T00:00:00Z");
73
+
74
+ // src/time/gnss.ts
75
+ function getTaiDate(date) {
76
+ return new Date(date.getTime() + MILLISECONDS_GPS_TAI);
77
+ }
78
+ function getNtpTime(date) {
79
+ return getTaiDate(date).getTime() - START_NTP_TIME.getTime();
80
+ }
81
+
82
+ // src/time/utc.ts
83
+ var LEAP_SECONDS_TABLE = [
84
+ [3692217600, 37],
85
+ // 1 Jan 2017
86
+ [3644697600, 36],
87
+ // 1 Jul 2015
88
+ [3550089600, 35],
89
+ // 1 Jul 2012
90
+ [3439756800, 34],
91
+ // 1 Jan 2009
92
+ [3345062400, 33],
93
+ // 1 Jan 2006
94
+ [3124137600, 32],
95
+ // 1 Jan 1999
96
+ [3076704e3, 31],
97
+ // 1 Jul 1997
98
+ [3029443200, 30],
99
+ // 1 Jan 1996
100
+ [2982009600, 29],
101
+ // 1 Jul 1994
102
+ [2950473600, 28],
103
+ // 1 Jul 1993
104
+ [2918937600, 27],
105
+ // 1 Jul 1992
106
+ [2871676800, 26],
107
+ // 1 Jan 1991
108
+ [2840140800, 25],
109
+ // 1 Jan 1990
110
+ [2776982400, 24],
111
+ // 1 Jan 1988
112
+ [2698012800, 23],
113
+ // 1 Jul 1985
114
+ [2634854400, 22],
115
+ // 1 Jul 1983
116
+ [2603318400, 21],
117
+ // 1 Jul 1982
118
+ [2571782400, 20],
119
+ // 1 Jul 1981
120
+ [2524521600, 19],
121
+ // 1 Jan 1980
122
+ [2492985600, 18],
123
+ // 1 Jan 1979
124
+ [2461449600, 17],
125
+ // 1 Jan 1978
126
+ [2429913600, 16],
127
+ // 1 Jan 1977
128
+ [2398291200, 15],
129
+ // 1 Jan 1976
130
+ [2366755200, 14],
131
+ // 1 Jan 1975
132
+ [2335219200, 13],
133
+ // 1 Jan 1974
134
+ [2303683200, 12],
135
+ // 1 Jan 1973
136
+ [2287785600, 11],
137
+ // 1 Jul 1972
138
+ [2272060800, 10]
139
+ // 1 Jan 1972
140
+ ];
141
+ function getLeap(date) {
142
+ const ntp_time = getNtpTime(date);
143
+ for (const [timestamp, leapSeconds] of LEAP_SECONDS_TABLE) {
144
+ if (ntp_time / MILLISECONDS_IN_SECOND - leapSeconds >= timestamp) {
145
+ return leapSeconds;
146
+ }
147
+ }
148
+ return 8;
149
+ }
150
+ function getGpsLeap(date) {
151
+ const leap_seconds = getLeap(date);
152
+ if (leap_seconds < 0) return 0;
153
+ return leap_seconds - 19;
154
+ }
155
+
69
156
  // src/trimble/nav.ts
70
157
  var GPS_EPOCH_MS = Date.UTC(1980, 0, 6);
71
158
  var BDT_EPOCH_MS = Date.UTC(2006, 0, 1);
@@ -78,6 +165,7 @@ var gpsMs = (week, sec) => GPS_EPOCH_MS + week * MS_PER_WEEK + sec * 1e3;
78
165
  var sowOf = (dateMs) => dateMs / 1e3 % SEC_PER_WEEK;
79
166
  var SUB_GPS_EPHEMERIS = 1;
80
167
  var SUB_ION_UTC = 3;
168
+ var SUB_GLO_EPHEMERIS = 9;
81
169
  var SUB_BDS_EPHEMERIS = 21;
82
170
  function readKepler(view, base) {
83
171
  return {
@@ -170,6 +258,38 @@ function decodeBdsEphemeris(view, base) {
170
258
  ...keplerCommon(r)
171
259
  };
172
260
  }
261
+ function decodeGloEphemeris(view, base) {
262
+ const slot = view.getUint8(base + 5);
263
+ if (slot < 1 || slot > 24) return null;
264
+ const week = view.getUint16(base + 6, false);
265
+ const tow = view.getUint32(base + 8, false);
266
+ const gpsTimeMs = gpsMs(week, tow);
267
+ const leapMs = getGpsLeap(new Date(gpsTimeMs)) * 1e3;
268
+ const tocMs = gpsTimeMs - leapMs;
269
+ return {
270
+ system: "R",
271
+ prn: `R${String(slot).padStart(2, "0")}`,
272
+ tocDate: new Date(tocMs),
273
+ // RINEX stores −τn as the clock bias; the record carries τn (ICD sign).
274
+ tauN: -view.getFloat64(base + 113, false),
275
+ gammaN: view.getFloat64(base + 121, false),
276
+ messageFrameTime: (tocMs - GPS_EPOCH_MS) / 1e3 % SEC_PER_WEEK,
277
+ // State vector: metres / m·s⁻¹ / m·s⁻² → km / km·s⁻¹ / km·s⁻².
278
+ x: view.getFloat64(base + 33, false) / 1e3,
279
+ xDot: view.getFloat64(base + 41, false) / 1e3,
280
+ xAcc: view.getFloat64(base + 49, false) / 1e3,
281
+ y: view.getFloat64(base + 57, false) / 1e3,
282
+ yDot: view.getFloat64(base + 65, false) / 1e3,
283
+ yAcc: view.getFloat64(base + 73, false) / 1e3,
284
+ z: view.getFloat64(base + 81, false) / 1e3,
285
+ zDot: view.getFloat64(base + 89, false) / 1e3,
286
+ zAcc: view.getFloat64(base + 97, false) / 1e3,
287
+ health: 0,
288
+ // Bn health bit not located; observed sats are operational
289
+ freqNum: view.getInt8(base + 29)
290
+ // FCN, signed byte (−7…+6)
291
+ };
292
+ }
173
293
  function parseTrimbleNav(data) {
174
294
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
175
295
  const stats = { badChecksum: 0 };
@@ -199,6 +319,13 @@ function parseTrimbleNav(data) {
199
319
  ionoCorrections["GPSA"] = alpha;
200
320
  ionoCorrections["GPSB"] = beta;
201
321
  leapSeconds = Math.trunc(view.getFloat64(p + 94, false));
322
+ } else if (sub === SUB_GLO_EPHEMERIS && f.len >= 125) {
323
+ const eph = decodeGloEphemeris(view, f.start);
324
+ if (!eph) continue;
325
+ const key = eph.tocDate.getTime();
326
+ if (lastIode.get(eph.prn) === key) continue;
327
+ lastIode.set(eph.prn, key);
328
+ ephemerides.push(eph);
202
329
  }
203
330
  }
204
331
  return {
@@ -25,8 +25,8 @@ declare function trimbleFrames(data: Uint8Array, stats: {
25
25
 
26
26
  /**
27
27
  * Trimble RT17/RT27 navigation-message decoding: RETSVDATA (0x55)
28
- * GPS ephemeris (subtype 1), BeiDou ephemeris (subtype 21) and ION / UTC
29
- * data (subtype 3).
28
+ * GPS ephemeris (subtype 1), GLONASS ephemeris (subtype 9), BeiDou
29
+ * ephemeris (subtype 21) and ION / UTC data (subtype 3).
30
30
  *
31
31
  * The GPS path is ported field-for-field from RTKLIB's `src/rcv/rt17.c`
32
32
  * (`decode_gps_ephemeris` / `decode_ion_utc_data`, tomojitakasu/RTKLIB,
@@ -42,8 +42,12 @@ declare function trimbleFrames(data: Uint8Array, stats: {
42
42
  * RINEX 3 navigation record and match the NovAtel/SBF ephemeris paths
43
43
  * (GPS: GPS-scale `tocDate`; BeiDou: naive-BDT `tocDate`; angles in rad).
44
44
  *
45
- * Still not decoded: the GLONASS/Galileo/QZSS ephemeris and almanac
46
- * subtypes (RETSVDATA 9/11/13/23/27 on this stream).
45
+ * The subtype→constellation map is confirmed by RTKLIB's rt17.c
46
+ * RETSVDATA table (st[]: 1 = GPS Eph, 9 = GLONASS Eph, 11 = Galileo Eph,
47
+ * 21 = BeiDou Eph) — but RTKLIB only *decodes* subtypes 1 and 3, so the
48
+ * GLONASS/BeiDou field layouts here were reverse-engineered and validated
49
+ * against physics + the 2026-07-27 BRDC. Still not decoded: Galileo
50
+ * (subtype 11) and the QZSS/almanac subtypes (13/23/27 on this stream).
47
51
  */
48
52
 
49
53
  interface TrimbleNavResult {
@@ -62,9 +66,10 @@ interface TrimbleNavResult {
62
66
  badChecksum: number;
63
67
  }
64
68
  /**
65
- * Decode every RETSVDATA navigation message (GPS ephemeris, ION/UTC) in
66
- * a Trimble binary byte stream. Repeated broadcasts of an unchanged GPS
67
- * ephemeris are suppressed by issue of data (IODE), as RTKLIB does.
69
+ * Decode every RETSVDATA navigation message (GPS/BeiDou/GLONASS
70
+ * ephemeris, ION/UTC) in a Trimble binary byte stream. Repeated
71
+ * broadcasts of an unchanged ephemeris are suppressed by issue of data
72
+ * (Keplerian) / reference epoch (GLONASS), as RTKLIB does.
68
73
  */
69
74
  declare function parseTrimbleNav(data: Uint8Array): TrimbleNavResult;
70
75
 
package/dist/trimble.d.ts CHANGED
@@ -25,8 +25,8 @@ declare function trimbleFrames(data: Uint8Array, stats: {
25
25
 
26
26
  /**
27
27
  * Trimble RT17/RT27 navigation-message decoding: RETSVDATA (0x55)
28
- * GPS ephemeris (subtype 1), BeiDou ephemeris (subtype 21) and ION / UTC
29
- * data (subtype 3).
28
+ * GPS ephemeris (subtype 1), GLONASS ephemeris (subtype 9), BeiDou
29
+ * ephemeris (subtype 21) and ION / UTC data (subtype 3).
30
30
  *
31
31
  * The GPS path is ported field-for-field from RTKLIB's `src/rcv/rt17.c`
32
32
  * (`decode_gps_ephemeris` / `decode_ion_utc_data`, tomojitakasu/RTKLIB,
@@ -42,8 +42,12 @@ declare function trimbleFrames(data: Uint8Array, stats: {
42
42
  * RINEX 3 navigation record and match the NovAtel/SBF ephemeris paths
43
43
  * (GPS: GPS-scale `tocDate`; BeiDou: naive-BDT `tocDate`; angles in rad).
44
44
  *
45
- * Still not decoded: the GLONASS/Galileo/QZSS ephemeris and almanac
46
- * subtypes (RETSVDATA 9/11/13/23/27 on this stream).
45
+ * The subtype→constellation map is confirmed by RTKLIB's rt17.c
46
+ * RETSVDATA table (st[]: 1 = GPS Eph, 9 = GLONASS Eph, 11 = Galileo Eph,
47
+ * 21 = BeiDou Eph) — but RTKLIB only *decodes* subtypes 1 and 3, so the
48
+ * GLONASS/BeiDou field layouts here were reverse-engineered and validated
49
+ * against physics + the 2026-07-27 BRDC. Still not decoded: Galileo
50
+ * (subtype 11) and the QZSS/almanac subtypes (13/23/27 on this stream).
47
51
  */
48
52
 
49
53
  interface TrimbleNavResult {
@@ -62,9 +66,10 @@ interface TrimbleNavResult {
62
66
  badChecksum: number;
63
67
  }
64
68
  /**
65
- * Decode every RETSVDATA navigation message (GPS ephemeris, ION/UTC) in
66
- * a Trimble binary byte stream. Repeated broadcasts of an unchanged GPS
67
- * ephemeris are suppressed by issue of data (IODE), as RTKLIB does.
69
+ * Decode every RETSVDATA navigation message (GPS/BeiDou/GLONASS
70
+ * ephemeris, ION/UTC) in a Trimble binary byte stream. Repeated
71
+ * broadcasts of an unchanged ephemeris are suppressed by issue of data
72
+ * (Keplerian) / reference epoch (GLONASS), as RTKLIB does.
68
73
  */
69
74
  declare function parseTrimbleNav(data: Uint8Array): TrimbleNavResult;
70
75
 
package/dist/trimble.js CHANGED
@@ -2,7 +2,9 @@ import {
2
2
  parseTrimble,
3
3
  parseTrimbleNav,
4
4
  trimbleFrames
5
- } from "./chunk-J2E2IKJR.js";
5
+ } from "./chunk-EPATQ3LT.js";
6
+ import "./chunk-HVXYFUCB.js";
7
+ import "./chunk-LEEU5OIO.js";
6
8
  export {
7
9
  parseTrimble,
8
10
  parseTrimbleNav,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.40.0",
3
+ "version": "1.41.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,