gnss-js 2.12.0 → 2.13.1

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/README.md CHANGED
@@ -14,7 +14,7 @@ Everything is exposed through subpath exports (`gnss-js/rinex`, `gnss-js/positio
14
14
 
15
15
  ## What's inside
16
16
 
17
- - **Formats in** — RINEX 2/3/4 obs + nav (plain, Hatanaka `.crx`, `.gz`), SP3, RINEX CLK, IONEX, SINEX bias; RTCM3 (MSM + legacy obs, ephemeris, SSR/IGS-SSR, station); RTCM 2.x; and the receiver binaries SBF, UBX, NovAtel OEM4, Trimble, BINEX.
17
+ - **Formats in** — RINEX 2/3/4 obs + nav (plain, Hatanaka `.crx`, `.gz`), SP3, RINEX CLK, IONEX, SINEX bias; RTCM3 (MSM + legacy obs, ephemeris, SSR/IGS-SSR, station); RTCM 2.x; and the receiver binaries SBF, UBX, NovAtel OEM4, Trimble, BINEX; Android raw GNSS logs (GnssLogger).
18
18
  - **Formats out** — RINEX 2/3/4 observation + navigation writers and Hatanaka (CRINEX) compression.
19
19
  - **Navigation** — Keplerian/GLONASS ephemeris handling plus raw-frame decoding of GPS LNAV/CNAV, Galileo I/NAV + F/NAV, BeiDou D1/D2 and B-CNAV1/2/3 (B1C/B2a/B2b), GLONASS strings, and Galileo HAS (E6 C/NAV) corrections.
20
20
  - **Authentication** — Galileo OSNMA: the full chain of trust from the Merkle root through ECDSA-signed TESLA keys to per-satellite MAC tags, flagging navigation data Galileo did not sign.
@@ -94,12 +94,15 @@ import { parseUbxRawx, parseUbxRawNav } from 'gnss-js/ubx'; // u-blox
94
94
  import { parseNovatelRange, parseNovatelNav } from 'gnss-js/novatel'; // NovAtel OEM4
95
95
  import { parseTrimble, parseTrimbleNav } from 'gnss-js/trimble'; // Trimble
96
96
  import { parseBinex } from 'gnss-js/binex'; // BINEX
97
+ import { parseAndroidGnssLog } from 'gnss-js/android'; // Android GnssLogger text logs
97
98
 
98
99
  const sbf = parseSbfMeas(bytes); // measurement epochs + counts
99
100
  const nav = parseSbfNav(bytes); // GPS/GAL/BDS/GLO ephemeris from raw blocks
100
101
  const has = parseSbfHas(bytes); // Galileo HAS (E6 C/NAV) SSR corrections
101
102
  ```
102
103
 
104
+ Android phones log raw measurements (`android.location.GnssMeasurement`) through Google's GnssLogger app. `parseAndroidGnssLog(text)` rebuilds pseudoranges, carrier phase, Doppler and C/N0 per signal (GPS, GLONASS, Galileo, BeiDou, QZSS, NavIC, SBAS; L1/E1/B1 and L5/E5a/B2a), in the same epoch shape as the receiver decoders, plus the phone's own position fixes. It matches Google's gps-measurement-tools pseudoranges exactly.
105
+
103
106
  ### Galileo OSNMA (navigation message authentication)
104
107
 
105
108
  `OsnmaReceiver` takes Galileo E1-B I/NAV pages and verifies them the way the OSNMA SIS ICD v1.1 prescribes: public key against the Merkle root (built in, or broadcast in DSM-PKR), the DSM-KROOT's ECDSA signature, every TESLA chain key back to the root key, and the MAC tags over each satellite's ephemeris/clock (ADKD 0/12) and timing parameters (ADKD 4). A tag that fails means the data differ from what Galileo signed. Validated against the official ICD test vectors and live receivers.
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/android/index.ts
21
+ var android_exports = {};
22
+ __export(android_exports, {
23
+ isAndroidGnssLog: () => isAndroidGnssLog,
24
+ parseAndroidGnssLog: () => parseAndroidGnssLog
25
+ });
26
+ module.exports = __toCommonJS(android_exports);
27
+
28
+ // src/constants/gnss.ts
29
+ var C_LIGHT = 299792458;
30
+
31
+ // src/android/index.ts
32
+ var NS_PER_WEEK = 604800000000000n;
33
+ var GPS_EPOCH_MS = Date.UTC(1980, 0, 6);
34
+ var BDT_MINUS_GPST_S = -14;
35
+ var GLO_RANGE = [159e7, 161e7];
36
+ var GLO_G1 = 1602e6;
37
+ var GLO_G1_STEP = 562500;
38
+ var STATE_CODE_LOCK = 1;
39
+ var STATE_TOW_DECODED = 8;
40
+ var STATE_MSEC_AMBIGUOUS = 16;
41
+ var STATE_GLO_TOD_DECODED = 128;
42
+ var STATE_GAL_E1BC_CODE_LOCK = 1024;
43
+ var STATE_GAL_E1C_2ND_CODE_LOCK = 2048;
44
+ var STATE_GAL_E1B_PAGE_SYNC = 4096;
45
+ var STATE_TOW_KNOWN = 16384;
46
+ var STATE_GLO_TOD_KNOWN = 32768;
47
+ var ADR_VALID = 1;
48
+ var ADR_RESET = 2;
49
+ var ADR_CYCLE_SLIP = 4;
50
+ var ADR_HALF_CYCLE_RESOLVED = 8;
51
+ var MAX_PR_UNC_NS = 500;
52
+ var MAX_ADR_UNC_M = 0.1;
53
+ var SYSTEM = {
54
+ 1: "G",
55
+ 2: "S",
56
+ 3: "R",
57
+ 4: "J",
58
+ 5: "C",
59
+ 6: "E",
60
+ 7: "I"
61
+ };
62
+ var BANDS = {
63
+ G: [
64
+ [1, 157542e4],
65
+ [2, 12276e5],
66
+ [5, 117645e4]
67
+ ],
68
+ S: [
69
+ [1, 157542e4],
70
+ [5, 117645e4]
71
+ ],
72
+ J: [
73
+ [1, 157542e4],
74
+ [2, 12276e5],
75
+ [5, 117645e4],
76
+ [6, 127875e4]
77
+ ],
78
+ E: [
79
+ [1, 157542e4],
80
+ [5, 117645e4],
81
+ [7, 120714e4],
82
+ [6, 127875e4]
83
+ ],
84
+ C: [
85
+ [2, 1561098e3],
86
+ [1, 157542e4],
87
+ [5, 117645e4],
88
+ [7, 120714e4],
89
+ [6, 126852e4]
90
+ ],
91
+ I: [[5, 117645e4]]
92
+ };
93
+ var DEFAULT_ATTR = {
94
+ G1: "C",
95
+ G2: "L",
96
+ G5: "Q",
97
+ S1: "C",
98
+ S5: "I",
99
+ J1: "C",
100
+ J2: "L",
101
+ J5: "Q",
102
+ E1: "C",
103
+ E5: "Q",
104
+ E7: "Q",
105
+ E6: "C",
106
+ C2: "I",
107
+ C1: "P",
108
+ C5: "P",
109
+ C7: "I",
110
+ C6: "I",
111
+ I5: "A",
112
+ R1: "C",
113
+ R2: "C"
114
+ };
115
+ function prnOf(sys, svid) {
116
+ let n = svid;
117
+ if (sys === "S")
118
+ n = svid - 100;
119
+ else if (sys === "J")
120
+ n = svid - 192;
121
+ else if (sys === "R" && svid > 24) return null;
122
+ if (n < 1 || n > 99) return null;
123
+ return sys + String(n).padStart(2, "0");
124
+ }
125
+ function bandOf(sys, freqHz) {
126
+ if (sys === "R") {
127
+ if (freqHz >= GLO_RANGE[0] && freqHz <= GLO_RANGE[1]) {
128
+ const k = Math.round((freqHz - GLO_G1) / GLO_G1_STEP);
129
+ return { band: 1, freq: GLO_G1 + k * GLO_G1_STEP, gloChannel: k };
130
+ }
131
+ return null;
132
+ }
133
+ for (const [band, f] of BANDS[sys] ?? [])
134
+ if (Math.abs(freqHz - f) < 1e6) return { band, freq: f, gloChannel: null };
135
+ return null;
136
+ }
137
+ var cells = (line) => line.split(",");
138
+ function parseAndroidGnssLog(text) {
139
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
140
+ const messageCounts = {};
141
+ const fixes = [];
142
+ let device = null;
143
+ let rawCols = null;
144
+ let fixCols = null;
145
+ let leapSeconds = null;
146
+ let rejected = 0;
147
+ const groups = [];
148
+ let curKey = "";
149
+ for (const line of lines) {
150
+ if (!line) continue;
151
+ if (line.startsWith("#")) {
152
+ const body = line.replace(/^#\s*/, "");
153
+ if (body.startsWith("Raw,"))
154
+ rawCols = new Map(cells(body).map((c, i) => [c.trim(), i]));
155
+ else if (body.startsWith("Fix,"))
156
+ fixCols = new Map(cells(body).map((c, i) => [c.trim(), i]));
157
+ else if (/Manufacturer/.test(body)) {
158
+ const m = (k) => new RegExp(`${k}:\\s*([^:]*?)(?:\\s+\\w+:|$)`).exec(body)?.[1]?.trim() ?? "";
159
+ device = {
160
+ manufacturer: m("Manufacturer"),
161
+ model: m("Model"),
162
+ version: m("Version").replace(/,$/, "")
163
+ };
164
+ }
165
+ continue;
166
+ }
167
+ const type = line.slice(0, line.indexOf(","));
168
+ if (!type) continue;
169
+ messageCounts[type] = (messageCounts[type] ?? 0) + 1;
170
+ if (type === "Raw" && rawCols) {
171
+ const r = cells(line);
172
+ const key = r[rawCols.get("TimeNanos")] ?? "";
173
+ if (key !== curKey) {
174
+ groups.push([]);
175
+ curKey = key;
176
+ }
177
+ groups[groups.length - 1].push(r);
178
+ } else if (type === "Fix" && fixCols) {
179
+ const r = cells(line);
180
+ const g = (k) => {
181
+ const i = fixCols.get(k);
182
+ const v = i === void 0 ? "" : (r[i] ?? "").trim();
183
+ return v === "" ? null : Number(v);
184
+ };
185
+ const lat = g("LatitudeDegrees");
186
+ const lon = g("LongitudeDegrees");
187
+ const t = g("UnixTimeMillis");
188
+ if (lat != null && lon != null && t != null)
189
+ fixes.push({
190
+ utcMs: t,
191
+ provider: (r[fixCols.get("Provider") ?? 1] ?? "").trim(),
192
+ latDeg: lat,
193
+ lonDeg: lon,
194
+ altM: g("AltitudeMeters"),
195
+ accuracyM: g("AccuracyMeters")
196
+ });
197
+ }
198
+ }
199
+ const epochs = [];
200
+ const obsCodes = {};
201
+ if (!rawCols) {
202
+ return {
203
+ epochs,
204
+ fixes,
205
+ device,
206
+ messageCounts,
207
+ obsCodes,
208
+ rejected,
209
+ leapSeconds
210
+ };
211
+ }
212
+ const col = rawCols;
213
+ const str = (r, k) => {
214
+ const i = col.get(k);
215
+ return i === void 0 ? "" : (r[i] ?? "").trim();
216
+ };
217
+ const num = (r, k) => {
218
+ const v = str(r, k);
219
+ return v === "" ? null : Number(v);
220
+ };
221
+ let segment = null;
222
+ let fullBias = 0n;
223
+ let bias = 0;
224
+ const lockStart = /* @__PURE__ */ new Map();
225
+ for (const rows of groups) {
226
+ const r0 = rows[0];
227
+ const timeNanosStr = str(r0, "TimeNanos");
228
+ const fullBiasStr = str(r0, "FullBiasNanos");
229
+ if (!timeNanosStr || !fullBiasStr) {
230
+ rejected += rows.length;
231
+ continue;
232
+ }
233
+ const disc = str(r0, "HardwareClockDiscontinuityCount");
234
+ if (disc !== segment) {
235
+ segment = disc;
236
+ fullBias = BigInt(fullBiasStr.split(".")[0]);
237
+ bias = num(r0, "BiasNanos") ?? 0;
238
+ }
239
+ const leap = num(r0, "LeapSecond");
240
+ if (leap != null && !Number.isNaN(leap)) leapSeconds = leap;
241
+ const rxNs = BigInt(timeNanosStr.split(".")[0]) - fullBias;
242
+ const week = Number(rxNs / NS_PER_WEEK);
243
+ const rxWeekNs = Number(rxNs % NS_PER_WEEK);
244
+ const epochWeekNs = Math.round((rxWeekNs - bias) / 100) * 100;
245
+ const toffNs = rxWeekNs - epochWeekNs - bias;
246
+ const timeMs = GPS_EPOCH_MS + week * 6048e5 + epochWeekNs / 1e6;
247
+ const leapNow = leapSeconds ?? 18;
248
+ const meas = [];
249
+ for (const r of rows) {
250
+ const sys = SYSTEM[num(r, "ConstellationType") ?? 1];
251
+ const svid = num(r, "Svid");
252
+ if (!sys || svid == null) {
253
+ rejected++;
254
+ continue;
255
+ }
256
+ const prn = prnOf(sys, svid);
257
+ const freqHz = num(r, "CarrierFrequencyHz") ?? 157542e4;
258
+ const b = prn ? bandOf(sys, freqHz) : null;
259
+ const state = num(r, "State") ?? 0;
260
+ const towOk = sys === "R" ? (state & (STATE_GLO_TOD_DECODED | STATE_GLO_TOD_KNOWN)) !== 0 : (state & (STATE_TOW_DECODED | STATE_TOW_KNOWN)) !== 0;
261
+ const codeLock = (state & (STATE_CODE_LOCK | STATE_GAL_E1BC_CODE_LOCK)) !== 0;
262
+ const prUnc = num(r, "ReceivedSvTimeUncertaintyNanos");
263
+ const svTimeNs = num(r, "ReceivedSvTimeNanos");
264
+ if (!prn || !b || !towOk || !codeLock || (state & STATE_MSEC_AMBIGUOUS) !== 0 || svTimeNs == null || prUnc != null && prUnc > MAX_PR_UNC_NS) {
265
+ rejected++;
266
+ continue;
267
+ }
268
+ const subNs = bias + (num(r, "TimeOffsetNanos") ?? 0);
269
+ let tauNs;
270
+ if (sys === "R") {
271
+ const DAY = 864e11;
272
+ const tTxNs = svTimeNs - 108e11 + leapNow * 1e9;
273
+ tauNs = ((rxWeekNs % DAY - tTxNs) % DAY + DAY) % DAY;
274
+ if (tauNs > DAY / 2) tauNs -= DAY;
275
+ } else {
276
+ const tTxNs = svTimeNs - (sys === "C" ? BDT_MINUS_GPST_S * 1e9 : 0);
277
+ tauNs = rxWeekNs - tTxNs;
278
+ if (tauNs > 3024e11) tauNs -= 6048e11;
279
+ else if (tauNs < -3024e11) tauNs += 6048e11;
280
+ }
281
+ tauNs -= subNs;
282
+ if (!(tauNs > 6e7 && tauNs < 16e7)) {
283
+ rejected++;
284
+ continue;
285
+ }
286
+ const pr = (tauNs - toffNs) * 1e-9 * C_LIGHT;
287
+ const attrCell = str(r, "CodeType");
288
+ let attr = attrCell || DEFAULT_ATTR[sys + b.band] || "C";
289
+ if (!attrCell && sys === "E" && b.band === 1 && (state & STATE_GAL_E1C_2ND_CODE_LOCK) === 0 && (state & STATE_GAL_E1B_PAGE_SYNC) !== 0)
290
+ attr = "B";
291
+ const code = `${b.band}${attr}`;
292
+ const lambda = C_LIGHT / b.freq;
293
+ const adrState = num(r, "AccumulatedDeltaRangeState") ?? 0;
294
+ const adr = num(r, "AccumulatedDeltaRangeMeters");
295
+ const adrUnc = num(r, "AccumulatedDeltaRangeUncertaintyMeters");
296
+ const lockKey = prn + code;
297
+ let cp = null;
298
+ if ((adrState & ADR_VALID) !== 0 && adr != null && adr !== 0 && (adrUnc == null || adrUnc < MAX_ADR_UNC_M)) {
299
+ cp = adr / lambda - toffNs * 1e-9 * b.freq;
300
+ if ((adrState & (ADR_RESET | ADR_CYCLE_SLIP)) !== 0 || !lockStart.has(lockKey))
301
+ lockStart.set(lockKey, timeMs);
302
+ } else lockStart.delete(lockKey);
303
+ const prr = num(r, "PseudorangeRateMetersPerSecond");
304
+ meas.push({
305
+ prn,
306
+ code,
307
+ pr,
308
+ cp,
309
+ doppler: prr == null ? null : -prr / lambda,
310
+ cn0: num(r, "Cn0DbHz"),
311
+ halfCycleAmbiguous: cp != null && (adrState & ADR_HALF_CYCLE_RESOLVED) === 0,
312
+ lockTimeMs: cp != null ? timeMs - lockStart.get(lockKey) : 0,
313
+ gloChannel: b.gloChannel
314
+ });
315
+ const list = obsCodes[sys] ??= [];
316
+ if (!list.includes(code)) list.push(code);
317
+ }
318
+ if (meas.length > 0) epochs.push({ timeMs, meas });
319
+ }
320
+ return {
321
+ epochs,
322
+ fixes,
323
+ device,
324
+ messageCounts,
325
+ obsCodes,
326
+ rejected,
327
+ leapSeconds
328
+ };
329
+ }
330
+ function isAndroidGnssLog(text) {
331
+ const head = text.slice(0, 16384);
332
+ return /^#\s*Raw,/m.test(head) || /^Raw,\d+,\d+,/m.test(head);
333
+ }
334
+ // Annotate the CommonJS export names for ESM import in node:
335
+ 0 && (module.exports = {
336
+ isAndroidGnssLog,
337
+ parseAndroidGnssLog
338
+ });
@@ -0,0 +1,68 @@
1
+ /** One satellite signal at an epoch, in the shape of the other receiver
2
+ * decoders (`ubx`, `sbf`…). */
3
+ interface AndroidMeasurement {
4
+ /** RINEX PRN, e.g. "G04", "R11", "J02". */
5
+ prn: string;
6
+ /** RINEX band + attribute, e.g. "1C", "5Q", "2I". */
7
+ code: string;
8
+ /** Pseudorange (m). */
9
+ pr: number;
10
+ /** Carrier phase (cycles), null when not tracked/valid. */
11
+ cp: number | null;
12
+ /** Doppler (Hz). */
13
+ doppler: number | null;
14
+ /** C/N0 (dB-Hz). */
15
+ cn0: number | null;
16
+ /** Carrier phase may be off by half a cycle (not half-cycle resolved). */
17
+ halfCycleAmbiguous: boolean;
18
+ /** Continuous carrier-lock time (ms): 0 at a slip/reset, then growing —
19
+ * the same loss-of-lock signal the other decoders give. */
20
+ lockTimeMs: number;
21
+ /** GLONASS frequency channel (−7…+6), from the carrier frequency. */
22
+ gloChannel: number | null;
23
+ }
24
+ interface AndroidEpoch {
25
+ /** Receiver epoch (GPS-scale ms — the RINEX-parser convention). */
26
+ timeMs: number;
27
+ meas: AndroidMeasurement[];
28
+ }
29
+ /** The phone's own position fix (`Fix,` rows). */
30
+ interface AndroidFix {
31
+ /** UTC ms. */
32
+ utcMs: number;
33
+ /** 'GPS' (GNSS chip), 'FLP' (fused), 'NLP' (network)… */
34
+ provider: string;
35
+ latDeg: number;
36
+ lonDeg: number;
37
+ /** Altitude (m) as reported (WGS84 ellipsoidal on most devices). */
38
+ altM: number | null;
39
+ /** Horizontal accuracy (m, 68%). */
40
+ accuracyM: number | null;
41
+ }
42
+ interface AndroidLogResult {
43
+ epochs: AndroidEpoch[];
44
+ fixes: AndroidFix[];
45
+ /** Device line (`# Version: … Manufacturer: … Model: …`), if present. */
46
+ device: {
47
+ manufacturer: string;
48
+ model: string;
49
+ version: string;
50
+ } | null;
51
+ /** Rows per record type ('Raw', 'Fix', 'Status', 'Nav'…). */
52
+ messageCounts: Record<string, number>;
53
+ /** Observation codes per system letter, first-seen order. */
54
+ obsCodes: Record<string, string[]>;
55
+ /** Raw rows dropped (no code lock / ambiguous time / bad values). */
56
+ rejected: number;
57
+ /** GPS − UTC leap seconds from the log, if reported. */
58
+ leapSeconds: number | null;
59
+ }
60
+ /**
61
+ * Parse a GnssLogger text log. Missing `CarrierFrequencyHz` (very old logs)
62
+ * means GPS/Galileo/QZSS L1.
63
+ */
64
+ declare function parseAndroidGnssLog(text: string): AndroidLogResult;
65
+ /** Whether `text` looks like a GnssLogger log (first few KB). */
66
+ declare function isAndroidGnssLog(text: string): boolean;
67
+
68
+ export { type AndroidEpoch, type AndroidFix, type AndroidLogResult, type AndroidMeasurement, isAndroidGnssLog, parseAndroidGnssLog };
@@ -0,0 +1,68 @@
1
+ /** One satellite signal at an epoch, in the shape of the other receiver
2
+ * decoders (`ubx`, `sbf`…). */
3
+ interface AndroidMeasurement {
4
+ /** RINEX PRN, e.g. "G04", "R11", "J02". */
5
+ prn: string;
6
+ /** RINEX band + attribute, e.g. "1C", "5Q", "2I". */
7
+ code: string;
8
+ /** Pseudorange (m). */
9
+ pr: number;
10
+ /** Carrier phase (cycles), null when not tracked/valid. */
11
+ cp: number | null;
12
+ /** Doppler (Hz). */
13
+ doppler: number | null;
14
+ /** C/N0 (dB-Hz). */
15
+ cn0: number | null;
16
+ /** Carrier phase may be off by half a cycle (not half-cycle resolved). */
17
+ halfCycleAmbiguous: boolean;
18
+ /** Continuous carrier-lock time (ms): 0 at a slip/reset, then growing —
19
+ * the same loss-of-lock signal the other decoders give. */
20
+ lockTimeMs: number;
21
+ /** GLONASS frequency channel (−7…+6), from the carrier frequency. */
22
+ gloChannel: number | null;
23
+ }
24
+ interface AndroidEpoch {
25
+ /** Receiver epoch (GPS-scale ms — the RINEX-parser convention). */
26
+ timeMs: number;
27
+ meas: AndroidMeasurement[];
28
+ }
29
+ /** The phone's own position fix (`Fix,` rows). */
30
+ interface AndroidFix {
31
+ /** UTC ms. */
32
+ utcMs: number;
33
+ /** 'GPS' (GNSS chip), 'FLP' (fused), 'NLP' (network)… */
34
+ provider: string;
35
+ latDeg: number;
36
+ lonDeg: number;
37
+ /** Altitude (m) as reported (WGS84 ellipsoidal on most devices). */
38
+ altM: number | null;
39
+ /** Horizontal accuracy (m, 68%). */
40
+ accuracyM: number | null;
41
+ }
42
+ interface AndroidLogResult {
43
+ epochs: AndroidEpoch[];
44
+ fixes: AndroidFix[];
45
+ /** Device line (`# Version: … Manufacturer: … Model: …`), if present. */
46
+ device: {
47
+ manufacturer: string;
48
+ model: string;
49
+ version: string;
50
+ } | null;
51
+ /** Rows per record type ('Raw', 'Fix', 'Status', 'Nav'…). */
52
+ messageCounts: Record<string, number>;
53
+ /** Observation codes per system letter, first-seen order. */
54
+ obsCodes: Record<string, string[]>;
55
+ /** Raw rows dropped (no code lock / ambiguous time / bad values). */
56
+ rejected: number;
57
+ /** GPS − UTC leap seconds from the log, if reported. */
58
+ leapSeconds: number | null;
59
+ }
60
+ /**
61
+ * Parse a GnssLogger text log. Missing `CarrierFrequencyHz` (very old logs)
62
+ * means GPS/Galileo/QZSS L1.
63
+ */
64
+ declare function parseAndroidGnssLog(text: string): AndroidLogResult;
65
+ /** Whether `text` looks like a GnssLogger log (first few KB). */
66
+ declare function isAndroidGnssLog(text: string): boolean;
67
+
68
+ export { type AndroidEpoch, type AndroidFix, type AndroidLogResult, type AndroidMeasurement, isAndroidGnssLog, parseAndroidGnssLog };
@@ -0,0 +1,311 @@
1
+ import {
2
+ C_LIGHT
3
+ } from "./chunk-FIEWO4J4.js";
4
+
5
+ // src/android/index.ts
6
+ var NS_PER_WEEK = 604800000000000n;
7
+ var GPS_EPOCH_MS = Date.UTC(1980, 0, 6);
8
+ var BDT_MINUS_GPST_S = -14;
9
+ var GLO_RANGE = [159e7, 161e7];
10
+ var GLO_G1 = 1602e6;
11
+ var GLO_G1_STEP = 562500;
12
+ var STATE_CODE_LOCK = 1;
13
+ var STATE_TOW_DECODED = 8;
14
+ var STATE_MSEC_AMBIGUOUS = 16;
15
+ var STATE_GLO_TOD_DECODED = 128;
16
+ var STATE_GAL_E1BC_CODE_LOCK = 1024;
17
+ var STATE_GAL_E1C_2ND_CODE_LOCK = 2048;
18
+ var STATE_GAL_E1B_PAGE_SYNC = 4096;
19
+ var STATE_TOW_KNOWN = 16384;
20
+ var STATE_GLO_TOD_KNOWN = 32768;
21
+ var ADR_VALID = 1;
22
+ var ADR_RESET = 2;
23
+ var ADR_CYCLE_SLIP = 4;
24
+ var ADR_HALF_CYCLE_RESOLVED = 8;
25
+ var MAX_PR_UNC_NS = 500;
26
+ var MAX_ADR_UNC_M = 0.1;
27
+ var SYSTEM = {
28
+ 1: "G",
29
+ 2: "S",
30
+ 3: "R",
31
+ 4: "J",
32
+ 5: "C",
33
+ 6: "E",
34
+ 7: "I"
35
+ };
36
+ var BANDS = {
37
+ G: [
38
+ [1, 157542e4],
39
+ [2, 12276e5],
40
+ [5, 117645e4]
41
+ ],
42
+ S: [
43
+ [1, 157542e4],
44
+ [5, 117645e4]
45
+ ],
46
+ J: [
47
+ [1, 157542e4],
48
+ [2, 12276e5],
49
+ [5, 117645e4],
50
+ [6, 127875e4]
51
+ ],
52
+ E: [
53
+ [1, 157542e4],
54
+ [5, 117645e4],
55
+ [7, 120714e4],
56
+ [6, 127875e4]
57
+ ],
58
+ C: [
59
+ [2, 1561098e3],
60
+ [1, 157542e4],
61
+ [5, 117645e4],
62
+ [7, 120714e4],
63
+ [6, 126852e4]
64
+ ],
65
+ I: [[5, 117645e4]]
66
+ };
67
+ var DEFAULT_ATTR = {
68
+ G1: "C",
69
+ G2: "L",
70
+ G5: "Q",
71
+ S1: "C",
72
+ S5: "I",
73
+ J1: "C",
74
+ J2: "L",
75
+ J5: "Q",
76
+ E1: "C",
77
+ E5: "Q",
78
+ E7: "Q",
79
+ E6: "C",
80
+ C2: "I",
81
+ C1: "P",
82
+ C5: "P",
83
+ C7: "I",
84
+ C6: "I",
85
+ I5: "A",
86
+ R1: "C",
87
+ R2: "C"
88
+ };
89
+ function prnOf(sys, svid) {
90
+ let n = svid;
91
+ if (sys === "S")
92
+ n = svid - 100;
93
+ else if (sys === "J")
94
+ n = svid - 192;
95
+ else if (sys === "R" && svid > 24) return null;
96
+ if (n < 1 || n > 99) return null;
97
+ return sys + String(n).padStart(2, "0");
98
+ }
99
+ function bandOf(sys, freqHz) {
100
+ if (sys === "R") {
101
+ if (freqHz >= GLO_RANGE[0] && freqHz <= GLO_RANGE[1]) {
102
+ const k = Math.round((freqHz - GLO_G1) / GLO_G1_STEP);
103
+ return { band: 1, freq: GLO_G1 + k * GLO_G1_STEP, gloChannel: k };
104
+ }
105
+ return null;
106
+ }
107
+ for (const [band, f] of BANDS[sys] ?? [])
108
+ if (Math.abs(freqHz - f) < 1e6) return { band, freq: f, gloChannel: null };
109
+ return null;
110
+ }
111
+ var cells = (line) => line.split(",");
112
+ function parseAndroidGnssLog(text) {
113
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
114
+ const messageCounts = {};
115
+ const fixes = [];
116
+ let device = null;
117
+ let rawCols = null;
118
+ let fixCols = null;
119
+ let leapSeconds = null;
120
+ let rejected = 0;
121
+ const groups = [];
122
+ let curKey = "";
123
+ for (const line of lines) {
124
+ if (!line) continue;
125
+ if (line.startsWith("#")) {
126
+ const body = line.replace(/^#\s*/, "");
127
+ if (body.startsWith("Raw,"))
128
+ rawCols = new Map(cells(body).map((c, i) => [c.trim(), i]));
129
+ else if (body.startsWith("Fix,"))
130
+ fixCols = new Map(cells(body).map((c, i) => [c.trim(), i]));
131
+ else if (/Manufacturer/.test(body)) {
132
+ const m = (k) => new RegExp(`${k}:\\s*([^:]*?)(?:\\s+\\w+:|$)`).exec(body)?.[1]?.trim() ?? "";
133
+ device = {
134
+ manufacturer: m("Manufacturer"),
135
+ model: m("Model"),
136
+ version: m("Version").replace(/,$/, "")
137
+ };
138
+ }
139
+ continue;
140
+ }
141
+ const type = line.slice(0, line.indexOf(","));
142
+ if (!type) continue;
143
+ messageCounts[type] = (messageCounts[type] ?? 0) + 1;
144
+ if (type === "Raw" && rawCols) {
145
+ const r = cells(line);
146
+ const key = r[rawCols.get("TimeNanos")] ?? "";
147
+ if (key !== curKey) {
148
+ groups.push([]);
149
+ curKey = key;
150
+ }
151
+ groups[groups.length - 1].push(r);
152
+ } else if (type === "Fix" && fixCols) {
153
+ const r = cells(line);
154
+ const g = (k) => {
155
+ const i = fixCols.get(k);
156
+ const v = i === void 0 ? "" : (r[i] ?? "").trim();
157
+ return v === "" ? null : Number(v);
158
+ };
159
+ const lat = g("LatitudeDegrees");
160
+ const lon = g("LongitudeDegrees");
161
+ const t = g("UnixTimeMillis");
162
+ if (lat != null && lon != null && t != null)
163
+ fixes.push({
164
+ utcMs: t,
165
+ provider: (r[fixCols.get("Provider") ?? 1] ?? "").trim(),
166
+ latDeg: lat,
167
+ lonDeg: lon,
168
+ altM: g("AltitudeMeters"),
169
+ accuracyM: g("AccuracyMeters")
170
+ });
171
+ }
172
+ }
173
+ const epochs = [];
174
+ const obsCodes = {};
175
+ if (!rawCols) {
176
+ return {
177
+ epochs,
178
+ fixes,
179
+ device,
180
+ messageCounts,
181
+ obsCodes,
182
+ rejected,
183
+ leapSeconds
184
+ };
185
+ }
186
+ const col = rawCols;
187
+ const str = (r, k) => {
188
+ const i = col.get(k);
189
+ return i === void 0 ? "" : (r[i] ?? "").trim();
190
+ };
191
+ const num = (r, k) => {
192
+ const v = str(r, k);
193
+ return v === "" ? null : Number(v);
194
+ };
195
+ let segment = null;
196
+ let fullBias = 0n;
197
+ let bias = 0;
198
+ const lockStart = /* @__PURE__ */ new Map();
199
+ for (const rows of groups) {
200
+ const r0 = rows[0];
201
+ const timeNanosStr = str(r0, "TimeNanos");
202
+ const fullBiasStr = str(r0, "FullBiasNanos");
203
+ if (!timeNanosStr || !fullBiasStr) {
204
+ rejected += rows.length;
205
+ continue;
206
+ }
207
+ const disc = str(r0, "HardwareClockDiscontinuityCount");
208
+ if (disc !== segment) {
209
+ segment = disc;
210
+ fullBias = BigInt(fullBiasStr.split(".")[0]);
211
+ bias = num(r0, "BiasNanos") ?? 0;
212
+ }
213
+ const leap = num(r0, "LeapSecond");
214
+ if (leap != null && !Number.isNaN(leap)) leapSeconds = leap;
215
+ const rxNs = BigInt(timeNanosStr.split(".")[0]) - fullBias;
216
+ const week = Number(rxNs / NS_PER_WEEK);
217
+ const rxWeekNs = Number(rxNs % NS_PER_WEEK);
218
+ const epochWeekNs = Math.round((rxWeekNs - bias) / 100) * 100;
219
+ const toffNs = rxWeekNs - epochWeekNs - bias;
220
+ const timeMs = GPS_EPOCH_MS + week * 6048e5 + epochWeekNs / 1e6;
221
+ const leapNow = leapSeconds ?? 18;
222
+ const meas = [];
223
+ for (const r of rows) {
224
+ const sys = SYSTEM[num(r, "ConstellationType") ?? 1];
225
+ const svid = num(r, "Svid");
226
+ if (!sys || svid == null) {
227
+ rejected++;
228
+ continue;
229
+ }
230
+ const prn = prnOf(sys, svid);
231
+ const freqHz = num(r, "CarrierFrequencyHz") ?? 157542e4;
232
+ const b = prn ? bandOf(sys, freqHz) : null;
233
+ const state = num(r, "State") ?? 0;
234
+ const towOk = sys === "R" ? (state & (STATE_GLO_TOD_DECODED | STATE_GLO_TOD_KNOWN)) !== 0 : (state & (STATE_TOW_DECODED | STATE_TOW_KNOWN)) !== 0;
235
+ const codeLock = (state & (STATE_CODE_LOCK | STATE_GAL_E1BC_CODE_LOCK)) !== 0;
236
+ const prUnc = num(r, "ReceivedSvTimeUncertaintyNanos");
237
+ const svTimeNs = num(r, "ReceivedSvTimeNanos");
238
+ if (!prn || !b || !towOk || !codeLock || (state & STATE_MSEC_AMBIGUOUS) !== 0 || svTimeNs == null || prUnc != null && prUnc > MAX_PR_UNC_NS) {
239
+ rejected++;
240
+ continue;
241
+ }
242
+ const subNs = bias + (num(r, "TimeOffsetNanos") ?? 0);
243
+ let tauNs;
244
+ if (sys === "R") {
245
+ const DAY = 864e11;
246
+ const tTxNs = svTimeNs - 108e11 + leapNow * 1e9;
247
+ tauNs = ((rxWeekNs % DAY - tTxNs) % DAY + DAY) % DAY;
248
+ if (tauNs > DAY / 2) tauNs -= DAY;
249
+ } else {
250
+ const tTxNs = svTimeNs - (sys === "C" ? BDT_MINUS_GPST_S * 1e9 : 0);
251
+ tauNs = rxWeekNs - tTxNs;
252
+ if (tauNs > 3024e11) tauNs -= 6048e11;
253
+ else if (tauNs < -3024e11) tauNs += 6048e11;
254
+ }
255
+ tauNs -= subNs;
256
+ if (!(tauNs > 6e7 && tauNs < 16e7)) {
257
+ rejected++;
258
+ continue;
259
+ }
260
+ const pr = (tauNs - toffNs) * 1e-9 * C_LIGHT;
261
+ const attrCell = str(r, "CodeType");
262
+ let attr = attrCell || DEFAULT_ATTR[sys + b.band] || "C";
263
+ if (!attrCell && sys === "E" && b.band === 1 && (state & STATE_GAL_E1C_2ND_CODE_LOCK) === 0 && (state & STATE_GAL_E1B_PAGE_SYNC) !== 0)
264
+ attr = "B";
265
+ const code = `${b.band}${attr}`;
266
+ const lambda = C_LIGHT / b.freq;
267
+ const adrState = num(r, "AccumulatedDeltaRangeState") ?? 0;
268
+ const adr = num(r, "AccumulatedDeltaRangeMeters");
269
+ const adrUnc = num(r, "AccumulatedDeltaRangeUncertaintyMeters");
270
+ const lockKey = prn + code;
271
+ let cp = null;
272
+ if ((adrState & ADR_VALID) !== 0 && adr != null && adr !== 0 && (adrUnc == null || adrUnc < MAX_ADR_UNC_M)) {
273
+ cp = adr / lambda - toffNs * 1e-9 * b.freq;
274
+ if ((adrState & (ADR_RESET | ADR_CYCLE_SLIP)) !== 0 || !lockStart.has(lockKey))
275
+ lockStart.set(lockKey, timeMs);
276
+ } else lockStart.delete(lockKey);
277
+ const prr = num(r, "PseudorangeRateMetersPerSecond");
278
+ meas.push({
279
+ prn,
280
+ code,
281
+ pr,
282
+ cp,
283
+ doppler: prr == null ? null : -prr / lambda,
284
+ cn0: num(r, "Cn0DbHz"),
285
+ halfCycleAmbiguous: cp != null && (adrState & ADR_HALF_CYCLE_RESOLVED) === 0,
286
+ lockTimeMs: cp != null ? timeMs - lockStart.get(lockKey) : 0,
287
+ gloChannel: b.gloChannel
288
+ });
289
+ const list = obsCodes[sys] ??= [];
290
+ if (!list.includes(code)) list.push(code);
291
+ }
292
+ if (meas.length > 0) epochs.push({ timeMs, meas });
293
+ }
294
+ return {
295
+ epochs,
296
+ fixes,
297
+ device,
298
+ messageCounts,
299
+ obsCodes,
300
+ rejected,
301
+ leapSeconds
302
+ };
303
+ }
304
+ function isAndroidGnssLog(text) {
305
+ const head = text.slice(0, 16384);
306
+ return /^#\s*Raw,/m.test(head) || /^Raw,\d+,\d+,/m.test(head);
307
+ }
308
+ export {
309
+ isAndroidGnssLog,
310
+ parseAndroidGnssLog
311
+ };
@@ -3144,7 +3144,32 @@ function sagnacRotate(x, y, z, travelS) {
3144
3144
  const s = Math.sin(theta);
3145
3145
  return [x * c + y * s, -x * s + y * c, z];
3146
3146
  }
3147
- function satStateAtEmission(source, prn, recvTimeMs, rcv) {
3147
+ function satStateAtEmission(source, prn, recvTimeMs, rcv, pseudorangeM) {
3148
+ if (pseudorangeM !== void 0 && pseudorangeM > 1e6) {
3149
+ let tEmit = recvTimeMs - pseudorangeM / C_LIGHT * 1e3;
3150
+ const s0 = source.satState(prn, tEmit);
3151
+ if (!s0) return null;
3152
+ tEmit -= s0.clkS * 1e3;
3153
+ const s = source.satState(prn, tEmit);
3154
+ if (!s) return null;
3155
+ let travelS2 = 0.075;
3156
+ let pos = [s.x, s.y, s.z];
3157
+ for (let iter = 0; iter < 3; iter++) {
3158
+ pos = sagnacRotate(s.x, s.y, s.z, travelS2);
3159
+ travelS2 = Math.hypot(pos[0] - rcv[0], pos[1] - rcv[1], pos[2] - rcv[2]) / C_LIGHT;
3160
+ }
3161
+ const velDotPos2 = s.x * s.vx + s.y * s.vy + s.z * s.vz;
3162
+ const relS2 = -2 * velDotPos2 / (C_LIGHT * C_LIGHT);
3163
+ return {
3164
+ state: {
3165
+ x: pos[0],
3166
+ y: pos[1],
3167
+ z: pos[2],
3168
+ clkM: C_LIGHT * (s.clkS + relS2)
3169
+ },
3170
+ travelS: travelS2
3171
+ };
3172
+ }
3148
3173
  let travelS = 0.075;
3149
3174
  let sx = 0;
3150
3175
  let sy = 0;
@@ -3377,7 +3402,7 @@ var PppEngine = class {
3377
3402
  const o = applySatBiases(source, raw, epoch.timeMs);
3378
3403
  if (!o) continue;
3379
3404
  const slip = detectSlip(o, ei);
3380
- const sat = satStateAtEmission(source, o.prn, epoch.timeMs, rcv);
3405
+ const sat = satStateAtEmission(source, o.prn, epoch.timeMs, rcv, o.c1);
3381
3406
  if (!sat) continue;
3382
3407
  const [elRad, azRad] = getAer(
3383
3408
  sat.state.x,
package/dist/index.cjs CHANGED
@@ -17013,7 +17013,32 @@ function sagnacRotate(x, y, z, travelS) {
17013
17013
  const s = Math.sin(theta);
17014
17014
  return [x * c + y * s, -x * s + y * c, z];
17015
17015
  }
17016
- function satStateAtEmission(source, prn, recvTimeMs, rcv) {
17016
+ function satStateAtEmission(source, prn, recvTimeMs, rcv, pseudorangeM) {
17017
+ if (pseudorangeM !== void 0 && pseudorangeM > 1e6) {
17018
+ let tEmit = recvTimeMs - pseudorangeM / C_LIGHT * 1e3;
17019
+ const s0 = source.satState(prn, tEmit);
17020
+ if (!s0) return null;
17021
+ tEmit -= s0.clkS * 1e3;
17022
+ const s = source.satState(prn, tEmit);
17023
+ if (!s) return null;
17024
+ let travelS2 = 0.075;
17025
+ let pos = [s.x, s.y, s.z];
17026
+ for (let iter = 0; iter < 3; iter++) {
17027
+ pos = sagnacRotate(s.x, s.y, s.z, travelS2);
17028
+ travelS2 = Math.hypot(pos[0] - rcv[0], pos[1] - rcv[1], pos[2] - rcv[2]) / C_LIGHT;
17029
+ }
17030
+ const velDotPos2 = s.x * s.vx + s.y * s.vy + s.z * s.vz;
17031
+ const relS2 = -2 * velDotPos2 / (C_LIGHT * C_LIGHT);
17032
+ return {
17033
+ state: {
17034
+ x: pos[0],
17035
+ y: pos[1],
17036
+ z: pos[2],
17037
+ clkM: C_LIGHT * (s.clkS + relS2)
17038
+ },
17039
+ travelS: travelS2
17040
+ };
17041
+ }
17017
17042
  let travelS = 0.075;
17018
17043
  let sx = 0;
17019
17044
  let sy = 0;
@@ -17246,7 +17271,7 @@ var PppEngine = class {
17246
17271
  const o = applySatBiases(source, raw, epoch.timeMs);
17247
17272
  if (!o) continue;
17248
17273
  const slip = detectSlip(o, ei);
17249
- const sat = satStateAtEmission(source, o.prn, epoch.timeMs, rcv);
17274
+ const sat = satStateAtEmission(source, o.prn, epoch.timeMs, rcv, o.c1);
17250
17275
  if (!sat) continue;
17251
17276
  const [elRad, azRad] = getAer(
17252
17277
  sat.state.x,
package/dist/index.js CHANGED
@@ -218,7 +218,7 @@ import {
218
218
  sunEcef,
219
219
  toRtkEpoch,
220
220
  wlWavelength
221
- } from "./chunk-RWFJEVLJ.js";
221
+ } from "./chunk-PQLPPFIK.js";
222
222
  import "./chunk-H3AKFD25.js";
223
223
  import {
224
224
  EPHEMERIS_VALIDITY_MS,
@@ -3753,7 +3753,32 @@ function sagnacRotate(x, y, z, travelS) {
3753
3753
  const s = Math.sin(theta);
3754
3754
  return [x * c + y * s, -x * s + y * c, z];
3755
3755
  }
3756
- function satStateAtEmission(source, prn, recvTimeMs, rcv) {
3756
+ function satStateAtEmission(source, prn, recvTimeMs, rcv, pseudorangeM) {
3757
+ if (pseudorangeM !== void 0 && pseudorangeM > 1e6) {
3758
+ let tEmit = recvTimeMs - pseudorangeM / C_LIGHT * 1e3;
3759
+ const s0 = source.satState(prn, tEmit);
3760
+ if (!s0) return null;
3761
+ tEmit -= s0.clkS * 1e3;
3762
+ const s = source.satState(prn, tEmit);
3763
+ if (!s) return null;
3764
+ let travelS2 = 0.075;
3765
+ let pos = [s.x, s.y, s.z];
3766
+ for (let iter = 0; iter < 3; iter++) {
3767
+ pos = sagnacRotate(s.x, s.y, s.z, travelS2);
3768
+ travelS2 = Math.hypot(pos[0] - rcv[0], pos[1] - rcv[1], pos[2] - rcv[2]) / C_LIGHT;
3769
+ }
3770
+ const velDotPos2 = s.x * s.vx + s.y * s.vy + s.z * s.vz;
3771
+ const relS2 = -2 * velDotPos2 / (C_LIGHT * C_LIGHT);
3772
+ return {
3773
+ state: {
3774
+ x: pos[0],
3775
+ y: pos[1],
3776
+ z: pos[2],
3777
+ clkM: C_LIGHT * (s.clkS + relS2)
3778
+ },
3779
+ travelS: travelS2
3780
+ };
3781
+ }
3757
3782
  let travelS = 0.075;
3758
3783
  let sx = 0;
3759
3784
  let sy = 0;
@@ -3986,7 +4011,7 @@ var PppEngine = class {
3986
4011
  const o = applySatBiases(source, raw, epoch.timeMs);
3987
4012
  if (!o) continue;
3988
4013
  const slip = detectSlip(o, ei);
3989
- const sat = satStateAtEmission(source, o.prn, epoch.timeMs, rcv);
4014
+ const sat = satStateAtEmission(source, o.prn, epoch.timeMs, rcv, o.c1);
3990
4015
  if (!sat) continue;
3991
4016
  const [elRad, azRad] = getAer(
3992
4017
  sat.state.x,
@@ -41,7 +41,7 @@ import {
41
41
  sunEcef,
42
42
  toRtkEpoch,
43
43
  wlWavelength
44
- } from "./chunk-RWFJEVLJ.js";
44
+ } from "./chunk-PQLPPFIK.js";
45
45
  import "./chunk-H3AKFD25.js";
46
46
  import "./chunk-7T5DLVZL.js";
47
47
  import "./chunk-HVXYFUCB.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "2.12.0",
3
+ "version": "2.13.1",
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,
@@ -109,6 +109,11 @@
109
109
  "types": "./dist/osnma.d.ts",
110
110
  "import": "./dist/osnma.js",
111
111
  "require": "./dist/osnma.cjs"
112
+ },
113
+ "./android": {
114
+ "types": "./dist/android.d.ts",
115
+ "import": "./dist/android.js",
116
+ "require": "./dist/android.cjs"
112
117
  }
113
118
  },
114
119
  "main": "./dist/index.cjs",