gnss-js 2.11.0 → 2.13.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/README.md +19 -1
- package/dist/analysis.cjs +24 -25
- package/dist/analysis.js +2 -2
- package/dist/android.cjs +338 -0
- package/dist/android.d.cts +68 -0
- package/dist/android.d.ts +68 -0
- package/dist/android.js +311 -0
- package/dist/{clk-DGVBGRzA.d.cts → bias-sinex-OGMw9xEZ.d.cts} +66 -1
- package/dist/{clk-DGVBGRzA.d.ts → bias-sinex-OGMw9xEZ.d.ts} +66 -1
- package/dist/{chunk-MDO77CCC.js → chunk-2DJ4XENO.js} +45 -0
- package/dist/{chunk-IQHQHUOB.js → chunk-2KQMCJ5Z.js} +5 -3
- package/dist/chunk-E3Y6YKQM.js +65 -0
- package/dist/{chunk-3U5AX7PY.js → chunk-FEABDV5P.js} +24 -25
- package/dist/{chunk-RFPR4LKM.js → chunk-HWHD5E6V.js} +19 -75
- package/dist/{chunk-KUSUN7DP.js → chunk-MFCPYUEI.js} +1 -1
- package/dist/{chunk-SSWFX3P3.js → chunk-NIJ6WFQJ.js} +6 -321
- package/dist/{chunk-UMINQTMK.js → chunk-RWFJEVLJ.js} +589 -295
- package/dist/chunk-TWGZTRBL.js +323 -0
- package/dist/index.cjs +655 -316
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -9
- package/dist/osnma.cjs +1336 -0
- package/dist/osnma.d.cts +273 -0
- package/dist/osnma.d.ts +273 -0
- package/dist/osnma.js +1216 -0
- package/dist/positioning.cjs +585 -291
- package/dist/positioning.d.cts +114 -4
- package/dist/positioning.d.ts +114 -4
- package/dist/positioning.js +1 -1
- package/dist/rinex.cjs +70 -25
- package/dist/rinex.d.cts +2 -50
- package/dist/rinex.d.ts +2 -50
- package/dist/rinex.js +4 -2
- package/dist/sbf.js +7 -4
- package/dist/ubx.js +3 -2
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -14,9 +14,10 @@ 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
|
+
- **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.
|
|
20
21
|
- **Orbits** — satellite position/velocity, Doppler, DOP, azimuth/elevation, pass prediction and almanac propagation.
|
|
21
22
|
- **Positioning** — SPP, static and kinematic float PPP, PPP-AR (LAMBDA + wide-/narrow-lane FCB estimation), post-processed RTK, DGNSS, and SBAS/DFMC augmentation.
|
|
22
23
|
- **Quality** — multipath, cycle slips, completeness and slant-TEC ionosphere analysis, with SINEX-DCB calibration.
|
|
@@ -93,12 +94,29 @@ import { parseUbxRawx, parseUbxRawNav } from 'gnss-js/ubx'; // u-blox
|
|
|
93
94
|
import { parseNovatelRange, parseNovatelNav } from 'gnss-js/novatel'; // NovAtel OEM4
|
|
94
95
|
import { parseTrimble, parseTrimbleNav } from 'gnss-js/trimble'; // Trimble
|
|
95
96
|
import { parseBinex } from 'gnss-js/binex'; // BINEX
|
|
97
|
+
import { parseAndroidGnssLog } from 'gnss-js/android'; // Android GnssLogger text logs
|
|
96
98
|
|
|
97
99
|
const sbf = parseSbfMeas(bytes); // measurement epochs + counts
|
|
98
100
|
const nav = parseSbfNav(bytes); // GPS/GAL/BDS/GLO ephemeris from raw blocks
|
|
99
101
|
const has = parseSbfHas(bytes); // Galileo HAS (E6 C/NAV) SSR corrections
|
|
100
102
|
```
|
|
101
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
|
+
|
|
106
|
+
### Galileo OSNMA (navigation message authentication)
|
|
107
|
+
|
|
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.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { OsnmaReceiver, verifySbfOsnma } from 'gnss-js/osnma';
|
|
112
|
+
|
|
113
|
+
const s = verifySbfOsnma(sbfBytes); // whole capture
|
|
114
|
+
s.chains; // verified TESLA chains; s.tags → { ok, failed, ... }
|
|
115
|
+
s.satellites; // per PRN: ced.authenticatedIodNav, timing.lastAuthenticated, failures
|
|
116
|
+
|
|
117
|
+
const rx = new OsnmaReceiver(); // live: rx.pushPage(svid, wn, tow, page); rx.status()
|
|
118
|
+
```
|
|
119
|
+
|
|
102
120
|
### RTCM (RTCM3, RTCM2, SSR)
|
|
103
121
|
|
|
104
122
|
```ts
|
package/dist/analysis.cjs
CHANGED
|
@@ -1113,20 +1113,14 @@ function applyIonoDcb(iono, satDcb) {
|
|
|
1113
1113
|
// src/rinex/crx.ts
|
|
1114
1114
|
function crxRepair(old, diff) {
|
|
1115
1115
|
const chars = old.split("");
|
|
1116
|
-
|
|
1116
|
+
const n = Math.min(old.length, diff.length);
|
|
1117
|
+
for (let i = 0; i < n; i++) {
|
|
1117
1118
|
const dc = diff[i];
|
|
1118
|
-
if (dc === " ")
|
|
1119
|
-
|
|
1120
|
-
chars[i] = " ";
|
|
1121
|
-
} else {
|
|
1122
|
-
chars[i] = dc;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
if (diff.length > chars.length) {
|
|
1126
|
-
for (let i = chars.length; i < diff.length; i++) {
|
|
1127
|
-
chars[i] = diff[i] === "&" ? " " : diff[i];
|
|
1128
|
-
}
|
|
1119
|
+
if (dc === " ") continue;
|
|
1120
|
+
chars[i] = dc === "&" ? " " : dc;
|
|
1129
1121
|
}
|
|
1122
|
+
for (let i = old.length; i < diff.length; i++)
|
|
1123
|
+
chars[i] = diff[i] === "&" ? " " : diff[i];
|
|
1130
1124
|
return chars.join("");
|
|
1131
1125
|
}
|
|
1132
1126
|
function parseCrxDataLine(line, ntype) {
|
|
@@ -1371,6 +1365,15 @@ function readObsValue(line, index) {
|
|
|
1371
1365
|
const v = parseFloat(raw);
|
|
1372
1366
|
return isNaN(v) ? null : v;
|
|
1373
1367
|
}
|
|
1368
|
+
function normalizePrn(id) {
|
|
1369
|
+
let sys = id[0];
|
|
1370
|
+
let num = id.substring(1);
|
|
1371
|
+
if (sys === " " || /\d/.test(sys)) {
|
|
1372
|
+
num = id;
|
|
1373
|
+
sys = "G";
|
|
1374
|
+
}
|
|
1375
|
+
return sys + num.replace(/\s/g, "").padStart(2, "0");
|
|
1376
|
+
}
|
|
1374
1377
|
function parseEpochLine3(line) {
|
|
1375
1378
|
if (line[0] !== ">") return null;
|
|
1376
1379
|
const yr = parseInt(line.substring(2, 6));
|
|
@@ -1520,7 +1523,7 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
|
|
|
1520
1523
|
function processSatLineV3(line) {
|
|
1521
1524
|
if (!header || line.length < 3) return;
|
|
1522
1525
|
const sys = line[0];
|
|
1523
|
-
const prn = line.substring(0, 3);
|
|
1526
|
+
const prn = normalizePrn(line.substring(0, 3));
|
|
1524
1527
|
trackSat(sys, prn);
|
|
1525
1528
|
const obsLine = line.substring(3);
|
|
1526
1529
|
const codes = header.obsTypes[sys] ?? [];
|
|
@@ -1545,10 +1548,8 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
|
|
|
1545
1548
|
let v2ObsAccum = [];
|
|
1546
1549
|
function processSatLineV2(line) {
|
|
1547
1550
|
if (!header || v2SatIndex >= v2SatIds.length) return;
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1550
|
-
if (sys === " " || /\d/.test(sys)) sys = "G";
|
|
1551
|
-
const prn = sys + satId.substring(1).padStart(2, "0");
|
|
1551
|
+
const prn = normalizePrn(v2SatIds[v2SatIndex]);
|
|
1552
|
+
const sys = prn[0];
|
|
1552
1553
|
if (v2CurrentSatLine === 0) {
|
|
1553
1554
|
trackSat(sys, prn);
|
|
1554
1555
|
if (onSatObs) v2ObsAccum = [];
|
|
@@ -1639,6 +1640,7 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
|
|
|
1639
1640
|
} else {
|
|
1640
1641
|
epochLine = crxRepair(crxPrevEpochLine, line);
|
|
1641
1642
|
}
|
|
1643
|
+
epochLine = epochLine.replace(/ +$/, "");
|
|
1642
1644
|
if (isV3) {
|
|
1643
1645
|
if (epochLine[0] !== ">") return;
|
|
1644
1646
|
const info = parseEpochLine3(epochLine);
|
|
@@ -1656,10 +1658,11 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
|
|
|
1656
1658
|
return;
|
|
1657
1659
|
}
|
|
1658
1660
|
resetEpochAccum(info);
|
|
1659
|
-
const satPart = epochLine.substring(41).replace(/\s+$/, "");
|
|
1660
1661
|
crxSatList = [];
|
|
1661
|
-
for (let
|
|
1662
|
-
crxSatList.push(
|
|
1662
|
+
for (let k = 0; k < info.numSats; k++) {
|
|
1663
|
+
crxSatList.push(
|
|
1664
|
+
normalizePrn(epochLine.substring(41 + 3 * k, 44 + 3 * k))
|
|
1665
|
+
);
|
|
1663
1666
|
}
|
|
1664
1667
|
crxSatIndex = 0;
|
|
1665
1668
|
crxPrevEpochLine = epochLine;
|
|
@@ -1680,11 +1683,7 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
|
|
|
1680
1683
|
return;
|
|
1681
1684
|
}
|
|
1682
1685
|
resetEpochAccum(info);
|
|
1683
|
-
crxSatList = (info.satIds ?? []).map(
|
|
1684
|
-
let sys = id[0];
|
|
1685
|
-
if (sys === " " || /\d/.test(sys)) sys = "G";
|
|
1686
|
-
return sys + id.substring(1).padStart(2, "0");
|
|
1687
|
-
});
|
|
1686
|
+
crxSatList = (info.satIds ?? []).slice(0, info.numSats).map(normalizePrn);
|
|
1688
1687
|
crxSatIndex = 0;
|
|
1689
1688
|
crxPrevEpochLine = epochLine;
|
|
1690
1689
|
crxPhase = "clock";
|
package/dist/analysis.js
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
computeIonoRate,
|
|
9
9
|
detrendIonoArcs,
|
|
10
10
|
parseSinexBiasDcb
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-MFCPYUEI.js";
|
|
12
|
+
import "./chunk-FEABDV5P.js";
|
|
13
13
|
import "./chunk-FIEWO4J4.js";
|
|
14
14
|
export {
|
|
15
15
|
CompletenessAccumulator,
|
package/dist/android.cjs
ADDED
|
@@ -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 };
|