@packet-net/pdn-aprs 0.1.1 → 0.3.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/CHANGELOG.md +44 -1
- package/README.md +9 -5
- package/dist/builder.d.ts +33 -8
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +64 -10
- package/dist/builder.js.map +1 -1
- package/dist/bytes.d.ts.map +1 -1
- package/dist/bytes.js +2 -1
- package/dist/bytes.js.map +1 -1
- package/dist/codes.js +8 -8
- package/dist/codes.js.map +1 -1
- package/dist/decode/index.d.ts.map +1 -1
- package/dist/decode/index.js +8 -5
- package/dist/decode/index.js.map +1 -1
- package/dist/decode/message.d.ts +2 -0
- package/dist/decode/message.d.ts.map +1 -1
- package/dist/decode/message.js +59 -33
- package/dist/decode/message.js.map +1 -1
- package/dist/decode/other.d.ts +20 -2
- package/dist/decode/other.d.ts.map +1 -1
- package/dist/decode/other.js +110 -48
- package/dist/decode/other.js.map +1 -1
- package/dist/decode/positioned.d.ts +26 -2
- package/dist/decode/positioned.d.ts.map +1 -1
- package/dist/decode/positioned.js +145 -79
- package/dist/decode/positioned.js.map +1 -1
- package/dist/decode/reports.d.ts.map +1 -1
- package/dist/decode/reports.js +47 -5
- package/dist/decode/reports.js.map +1 -1
- package/dist/decoder.d.ts.map +1 -1
- package/dist/decoder.js +2 -4
- package/dist/decoder.js.map +1 -1
- package/dist/deviceid.d.ts +8 -2
- package/dist/deviceid.d.ts.map +1 -1
- package/dist/deviceid.js +28 -6
- package/dist/deviceid.js.map +1 -1
- package/dist/encode/common.d.ts +13 -2
- package/dist/encode/common.d.ts.map +1 -1
- package/dist/encode/common.js +36 -4
- package/dist/encode/common.js.map +1 -1
- package/dist/encode/index.d.ts.map +1 -1
- package/dist/encode/index.js +59 -22
- package/dist/encode/index.js.map +1 -1
- package/dist/encode/other.d.ts.map +1 -1
- package/dist/encode/other.js +45 -17
- package/dist/encode/other.js.map +1 -1
- package/dist/encode/position.d.ts +9 -3
- package/dist/encode/position.d.ts.map +1 -1
- package/dist/encode/position.js +258 -124
- package/dist/encode/position.js.map +1 -1
- package/dist/header.d.ts +5 -2
- package/dist/header.d.ts.map +1 -1
- package/dist/header.js +8 -3
- package/dist/header.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/neutral.d.ts.map +1 -1
- package/dist/neutral.js +22 -3
- package/dist/neutral.js.map +1 -1
- package/dist/types.d.ts +19 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/builder.ts +77 -14
- package/src/bytes.ts +2 -1
- package/src/codes.ts +8 -8
- package/src/decode/index.ts +7 -4
- package/src/decode/message.ts +54 -37
- package/src/decode/other.ts +116 -44
- package/src/decode/positioned.ts +159 -90
- package/src/decode/reports.ts +47 -4
- package/src/decoder.ts +2 -5
- package/src/deviceid.ts +29 -5
- package/src/encode/common.ts +34 -4
- package/src/encode/index.ts +57 -20
- package/src/encode/other.ts +39 -13
- package/src/encode/position.ts +261 -119
- package/src/header.ts +9 -2
- package/src/index.ts +1 -0
- package/src/neutral.ts +24 -3
- package/src/types.ts +20 -5
package/dist/decode/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The information field decoder: picks the format by the data type identifier.
|
|
2
2
|
import { binaryToBytes, bytesToBinary } from '../bytes.js';
|
|
3
|
-
import {
|
|
3
|
+
import { identifyDevice } from '../deviceid.js';
|
|
4
|
+
import { parseHeaderText } from '../header.js';
|
|
4
5
|
import { DecodeContext, Rejected } from './context.js';
|
|
5
6
|
import { decodeMessage } from './message.js';
|
|
6
7
|
import { decodeAgrelo, decodeCapabilities, decodeDollar, decodeMaidenhead, decodeQuery, decodeRawWeather, decodeStatus, decodeTelemetry, decodeTest, decodeUserDefined, } from './other.js';
|
|
@@ -98,7 +99,7 @@ function decodeThirdParty(ctx, s, options) {
|
|
|
98
99
|
const headerDiags = [];
|
|
99
100
|
let header;
|
|
100
101
|
try {
|
|
101
|
-
header = parseHeaderText(body.slice(0, colon), options, headerDiags);
|
|
102
|
+
header = parseHeaderText(body.slice(0, colon), options, headerDiags, true);
|
|
102
103
|
}
|
|
103
104
|
catch {
|
|
104
105
|
return ctx.fail('invalid-third-party');
|
|
@@ -113,9 +114,11 @@ function decodeThirdParty(ctx, s, options) {
|
|
|
113
114
|
data: inner.data,
|
|
114
115
|
diagnostics: [...headerDiags, ...inner.diagnostics],
|
|
115
116
|
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
// The inner packet has its device too, from its own destination (vectors rulings, A2).
|
|
118
|
+
const device = identifyDevice(inner.data, header.destination);
|
|
119
|
+
if (device)
|
|
120
|
+
packet.device = device;
|
|
121
|
+
// A q-construct is read only in the outer header: the third-party path is kept as sent.
|
|
119
122
|
return { type: 'third-party', packet };
|
|
120
123
|
}
|
|
121
124
|
//# sourceMappingURL=index.js.map
|
package/dist/decode/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/decode/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/decode/index.ts"],"names":[],"mappings":"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"}
|
package/dist/decode/message.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AprsData } from '../types.js';
|
|
2
2
|
import type { DecodeContext } from './context.js';
|
|
3
|
+
/** Known directed query types (APRS12c ch. 15), longest first. */
|
|
4
|
+
export declare const DIRECTED_QUERY_TYPES: readonly string[];
|
|
3
5
|
/** `:`: a message, or one of the things sent in message form. */
|
|
4
6
|
export declare function decodeMessage(ctx: DecodeContext, s: string): AprsData;
|
|
5
7
|
//# sourceMappingURL=message.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/decode/message.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/decode/message.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,kEAAkE;AAClE,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAA6E,CAAC;AAsChI,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,QAAQ,CA0ErE"}
|
package/dist/decode/message.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
// Messages, acks and rejects, bulletins, telemetry metadata and directed queries (APRS12c ch.
|
|
2
2
|
// 13-15).
|
|
3
3
|
/** Known directed query types (APRS12c ch. 15), longest first. */
|
|
4
|
-
const DIRECTED_QUERY_TYPES = ['APRSD', 'APRSH', 'APRSM', 'APRSO', 'APRSP', 'APRSS', 'APRST', 'PING?'];
|
|
4
|
+
export const DIRECTED_QUERY_TYPES = ['APRSD', 'APRSH', 'APRSM', 'APRSO', 'APRSP', 'APRSS', 'APRST', 'PING?'];
|
|
5
5
|
const MESSAGE_ID_RE = /^([A-Za-z0-9]{1,5})(?:\}([A-Za-z0-9]{0,5}))?$/;
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const PLAIN_ID_RE = /^([A-Za-z0-9]{1,5})$/;
|
|
7
|
+
// ack or rej, the ID, an optional reply-ack, and an optional message ID of its own.
|
|
8
|
+
const ACK_RE = /^(ack|rej)([A-Za-z0-9]{1,5})(?:\}([A-Za-z0-9]{0,5}))?(\{[A-Za-z0-9]{1,5})?$/;
|
|
9
|
+
// A number: an optional -, digits with an optional point, and for a coefficient an exponent.
|
|
10
|
+
const COEFFICIENT_RE = /^-?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
|
|
11
|
+
const METADATA_PREFIXES = ['PARM.', 'UNIT.', 'EQNS.', 'BITS.'];
|
|
12
|
+
/**
|
|
13
|
+
* Splits `text{id}ack` into its parts. Bulletins and telemetry metadata are not acknowledged, so
|
|
14
|
+
* for them (`replyAcks` false) the reply-ack form is not an ID and stays in the text (vectors
|
|
15
|
+
* interpretations.md, "Reply-acks are for messages").
|
|
16
|
+
*/
|
|
17
|
+
function splitMessageId(text, replyAcks) {
|
|
10
18
|
const brace = text.lastIndexOf('{');
|
|
11
19
|
if (brace < 0)
|
|
12
|
-
return { text };
|
|
13
|
-
const m = MESSAGE_ID_RE.exec(text.slice(brace + 1));
|
|
14
|
-
if (!m)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
const out = { text: text.slice(0, brace), messageId: m[1] };
|
|
20
|
+
return { text, strayBrace: false };
|
|
21
|
+
const m = (replyAcks ? MESSAGE_ID_RE : PLAIN_ID_RE).exec(text.slice(brace + 1));
|
|
22
|
+
if (!m)
|
|
23
|
+
return { text, strayBrace: true };
|
|
24
|
+
const out = { text: text.slice(0, brace), messageId: m[1], strayBrace: false };
|
|
19
25
|
if (m[2] !== undefined)
|
|
20
26
|
out.replyAck = m[2];
|
|
21
27
|
if (out.text.includes('{'))
|
|
22
|
-
|
|
28
|
+
out.strayBrace = true;
|
|
23
29
|
return out;
|
|
24
30
|
}
|
|
25
31
|
function parseList(body) {
|
|
@@ -27,13 +33,14 @@ function parseList(body) {
|
|
|
27
33
|
}
|
|
28
34
|
/** `:`: a message, or one of the things sent in message form. */
|
|
29
35
|
export function decodeMessage(ctx, s) {
|
|
30
|
-
|
|
31
|
-
ctx.fail('invalid-message');
|
|
36
|
+
// When the tenth byte is :, the addressee is the nine bytes before it, whatever they hold.
|
|
32
37
|
let colon = -1;
|
|
33
38
|
if (s.length > 10 && s[10] === ':') {
|
|
34
39
|
colon = 10;
|
|
35
40
|
}
|
|
36
41
|
else {
|
|
42
|
+
if (s[1] === ':')
|
|
43
|
+
ctx.fail('invalid-message');
|
|
37
44
|
for (let k = 2; k <= 9 && k < s.length; k++) {
|
|
38
45
|
if (s[k] === ':') {
|
|
39
46
|
colon = k;
|
|
@@ -69,12 +76,16 @@ export function decodeMessage(ctx, s) {
|
|
|
69
76
|
if (/^BLN[0-9]/.test(addressee) || /^BLN[A-Z]$/.test(addressee) || /^BLN[A-Z]./.test(addressee)) {
|
|
70
77
|
if (/^BLN[A-Z]./.test(addressee))
|
|
71
78
|
ctx.tolerate('letter-group-bulletin');
|
|
72
|
-
const split = splitMessageId(
|
|
79
|
+
const split = splitMessageId(body, false);
|
|
80
|
+
if (split.strayBrace)
|
|
81
|
+
ctx.tolerate('brace-in-message-text');
|
|
73
82
|
const text = ctx.text(split.text);
|
|
74
83
|
return withId({ type: 'bulletin', addressee, text }, split.messageId);
|
|
75
84
|
}
|
|
76
85
|
if (/^NWS[-_]/.test(addressee)) {
|
|
77
|
-
const split = splitMessageId(
|
|
86
|
+
const split = splitMessageId(body, false);
|
|
87
|
+
if (split.strayBrace)
|
|
88
|
+
ctx.tolerate('brace-in-message-text');
|
|
78
89
|
const text = ctx.text(split.text);
|
|
79
90
|
return withId({ type: 'nws-bulletin', addressee, text }, split.messageId);
|
|
80
91
|
}
|
|
@@ -84,11 +95,15 @@ export function decodeMessage(ctx, s) {
|
|
|
84
95
|
if (query)
|
|
85
96
|
return query;
|
|
86
97
|
}
|
|
87
|
-
const split = splitMessageId(ctx, body);
|
|
88
98
|
// Telemetry metadata: its structure is checked before the text's encoding.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
if (METADATA_PREFIXES.includes(body.slice(0, 5))) {
|
|
100
|
+
const meta = telemetryMetadata(ctx, addressee, body);
|
|
101
|
+
if (meta)
|
|
102
|
+
return meta;
|
|
103
|
+
}
|
|
104
|
+
const split = splitMessageId(body, true);
|
|
105
|
+
if (split.strayBrace)
|
|
106
|
+
ctx.tolerate('brace-in-message-text');
|
|
92
107
|
const text = ctx.text(split.text);
|
|
93
108
|
const message = {
|
|
94
109
|
type: 'message',
|
|
@@ -129,11 +144,11 @@ function directedQuery(ctx, addressee, body) {
|
|
|
129
144
|
queryType = m[1];
|
|
130
145
|
rest = q.slice(queryType.length);
|
|
131
146
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const target = rest.
|
|
147
|
+
// One space between the type and the target is a separator, and spaces after the target are
|
|
148
|
+
// padding (APRSH pads its target to 9 characters).
|
|
149
|
+
if (rest.startsWith(' '))
|
|
150
|
+
rest = rest.slice(1);
|
|
151
|
+
const target = rest.replace(/ +$/, '');
|
|
137
152
|
if (target.length === 0)
|
|
138
153
|
return { type: 'directed-query', addressee, queryType };
|
|
139
154
|
if (/^[A-Za-z0-9-]{1,9}$/.test(target))
|
|
@@ -141,19 +156,26 @@ function directedQuery(ctx, addressee, body) {
|
|
|
141
156
|
ctx.info('invalid-query');
|
|
142
157
|
return undefined;
|
|
143
158
|
}
|
|
144
|
-
/** Telemetry metadata sent as a message; `
|
|
145
|
-
function telemetryMetadata(ctx, addressee,
|
|
159
|
+
/** Telemetry metadata sent as a message; `body` is the text's bytes, decoded once it is valid. */
|
|
160
|
+
function telemetryMetadata(ctx, addressee, body) {
|
|
161
|
+
// Metadata takes a message ID, but not the reply-ack form.
|
|
162
|
+
const split = splitMessageId(body, false);
|
|
163
|
+
const binary = split.text;
|
|
164
|
+
const messageId = split.messageId;
|
|
146
165
|
const kind = binary.slice(0, 5);
|
|
147
|
-
if (kind !== 'PARM.' && kind !== 'UNIT.' && kind !== 'EQNS.' && kind !== 'BITS.')
|
|
148
|
-
return undefined;
|
|
149
166
|
const rawBody = binary.slice(5);
|
|
150
167
|
const invalid = () => {
|
|
151
168
|
ctx.info('invalid-telemetry-metadata');
|
|
152
169
|
return undefined;
|
|
153
170
|
};
|
|
171
|
+
const braces = () => {
|
|
172
|
+
if (split.strayBrace)
|
|
173
|
+
ctx.tolerate('brace-in-message-text');
|
|
174
|
+
};
|
|
154
175
|
if (kind === 'PARM.' || kind === 'UNIT.') {
|
|
155
176
|
if (parseList(rawBody).length > 13)
|
|
156
177
|
return invalid();
|
|
178
|
+
braces();
|
|
157
179
|
const list = parseList(ctx.text(rawBody));
|
|
158
180
|
return kind === 'PARM.'
|
|
159
181
|
? withId({ type: 'telemetry-names', addressee, names: list }, messageId)
|
|
@@ -162,22 +184,26 @@ function telemetryMetadata(ctx, addressee, binary, messageId) {
|
|
|
162
184
|
if (kind === 'EQNS.') {
|
|
163
185
|
const list = parseList(rawBody);
|
|
164
186
|
// Trailing empty entries (commas and spaces) are the list stopping.
|
|
165
|
-
while (list.length > 0 && list[list.length - 1]
|
|
187
|
+
while (list.length > 0 && /^ *$/.test(list[list.length - 1]))
|
|
166
188
|
list.pop();
|
|
167
189
|
if (list.length === 0 || list.length > 15)
|
|
168
190
|
return invalid();
|
|
169
191
|
const texts = [];
|
|
170
192
|
for (const item of list) {
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
// Spaces (U+0020 only) around a coefficient are padding (vectors interpretations.md,
|
|
194
|
+
// "Numbers in telemetry").
|
|
195
|
+
const t = item.replace(/^ +| +$/g, '');
|
|
196
|
+
if (!COEFFICIENT_RE.test(t) || !Number.isFinite(Number(t)))
|
|
173
197
|
return invalid();
|
|
174
198
|
texts.push(t);
|
|
175
199
|
}
|
|
200
|
+
braces();
|
|
176
201
|
return withId({ type: 'telemetry-coefficients', addressee, coefficients: texts.map(Number), coefficientsText: texts }, messageId);
|
|
177
202
|
}
|
|
178
203
|
const m = /^([01]{8})(?:,(.*))?$/s.exec(rawBody);
|
|
179
204
|
if (!m)
|
|
180
205
|
return invalid();
|
|
206
|
+
braces();
|
|
181
207
|
const bits = {
|
|
182
208
|
type: 'telemetry-bits',
|
|
183
209
|
addressee,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/decode/message.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,UAAU;AAKV,kEAAkE;AAClE,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/decode/message.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,UAAU;AAKV,kEAAkE;AAClE,MAAM,CAAC,MAAM,oBAAoB,GAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAEhI,MAAM,aAAa,GAAG,+CAA+C,CAAC;AACtE,MAAM,WAAW,GAAG,sBAAsB,CAAC;AAC3C,oFAAoF;AACpF,MAAM,MAAM,GAAG,6EAA6E,CAAC;AAC7F,6FAA6F;AAC7F,MAAM,cAAc,GAAG,sDAAsD,CAAC;AAC9E,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAU/D;;;;GAIG;AACH,SAAS,cAAc,CAAC,IAAY,EAAE,SAAkB;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAClD,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IAChF,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IAC1C,MAAM,GAAG,GAAc,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAC3F,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS;QAAE,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC;IAClD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,aAAa,CAAC,GAAkB,EAAE,CAAS;IACzD,2FAA2F;IAC3F,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC;IACf,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,GAAG,EAAE,CAAC;QACnC,KAAK,GAAG,EAAE,CAAC;IACb,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC9C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACjB,KAAK,GAAG,CAAC,CAAC;gBACV,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,KAAK,GAAG,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC3C,GAAG,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAC7F,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,GAAG,CAAC,QAAQ,CAAC,8BAA8B,CAAC,CAAC;IACzE,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAEhC,oBAAoB;IACpB,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,GAAG,EAAE,CAAC;QACR,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;YACrB,OAAO,QAAQ,KAAK,SAAS;gBAC3B,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAE,EAAE;gBAC9C,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAE,EAAE,QAAQ,EAAE,CAAC;QAC7D,CAAC;QACD,OAAO,QAAQ,KAAK,SAAS;YAC3B,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC,CAAE,EAAE;YACpD,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC,CAAE,EAAE,QAAQ,EAAE,CAAC;IACnE,CAAC;IAED,+BAA+B;IAC/B,IAAI,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAChG,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC;YAAE,GAAG,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC;QACxE,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC1C,IAAI,KAAK,CAAC,UAAU;YAAE,GAAG,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,OAAO,MAAM,CAAC,EAAE,IAAI,EAAE,UAAmB,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC1C,IAAI,KAAK,CAAC,UAAU;YAAE,GAAG,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,OAAO,MAAM,CAAC,EAAE,IAAI,EAAE,cAAuB,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACrF,CAAC;IAED,oBAAoB;IACpB,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;QAClD,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IAED,2EAA2E;IAC3E,IAAI,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,GAAG,iBAAiB,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;QACrD,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;IACxB,CAAC;IAED,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACzC,IAAI,KAAK,CAAC,UAAU;QAAE,GAAG,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,OAAO,GAAgG;QAC3G,IAAI,EAAE,SAAS;QACf,SAAS;QACT,IAAI;KACL,CAAC;IACF,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;QAAE,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IACvE,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;IACpE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,MAAM,CAAmB,IAAO,EAAE,SAA6B;IACtE,OAAO,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,CAAC;AACjE,CAAC;AAED,SAAS,aAAa,CAAC,GAAkB,EAAE,SAAiB,EAAE,IAAY;IACxE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACpB,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC1B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,MAAM,UAAU,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,IAAI,SAA6B,CAAC;IAClC,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,KAAK,EAAE,CAAC;QACV,SAAS,GAAG,KAAK,CAAC;QAClB,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;SAAM,IAAI,UAAU,EAAE,CAAC;QACtB,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC1B,OAAO,SAAS,CAAC;IACnB,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACzB,SAAS,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QAClB,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IACD,4FAA4F;IAC5F,mDAAmD;IACnD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;IACjF,IAAI,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IACxG,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC1B,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,kGAAkG;AAClG,SAAS,iBAAiB,CAAC,GAAkB,EAAE,SAAiB,EAAE,IAAY;IAC5E,2DAA2D;IAC3D,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC;IAC1B,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,GAAc,EAAE;QAC9B,GAAG,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;QACvC,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,GAAS,EAAE;QACxB,IAAI,KAAK,CAAC,UAAU;YAAE,GAAG,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC;IAC9D,CAAC,CAAC;IACF,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACzC,IAAI,SAAS,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,EAAE;YAAE,OAAO,OAAO,EAAE,CAAC;QACrD,MAAM,EAAE,CAAC;QACT,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1C,OAAO,IAAI,KAAK,OAAO;YACrB,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,iBAA0B,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,SAAS,CAAC;YACjF,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,iBAA0B,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,SAAS,CAAC,CAAC;IACtF,CAAC;IACD,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QAChC,oEAAoE;QACpE,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;YAAE,IAAI,CAAC,GAAG,EAAE,CAAC;QAC1E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE;YAAE,OAAO,OAAO,EAAE,CAAC;QAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;YACxB,qFAAqF;YACrF,2BAA2B;YAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YACvC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO,OAAO,EAAE,CAAC;YAC7E,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChB,CAAC;QACD,MAAM,EAAE,CAAC;QACT,OAAO,MAAM,CACX,EAAE,IAAI,EAAE,wBAAiC,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,KAAK,EAAE,EAChH,SAAS,CACV,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAG,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,CAAC,CAAC;QAAE,OAAO,OAAO,EAAE,CAAC;IACzB,MAAM,EAAE,CAAC;IACT,MAAM,IAAI,GAAkF;QAC1F,IAAI,EAAE,gBAAgB;QACtB,SAAS;QACT,IAAI,EAAE,CAAC,CAAC,CAAC,CAAE;KACZ,CAAC;IACF,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACjC,CAAC"}
|
package/dist/decode/other.d.ts
CHANGED
|
@@ -8,8 +8,26 @@ export declare function decodeTelemetry(ctx: DecodeContext, s: string): Telemetr
|
|
|
8
8
|
export declare function decodeQuery(ctx: DecodeContext, s: string): Query;
|
|
9
9
|
/** `<`: station capabilities. */
|
|
10
10
|
export declare function decodeCapabilities(ctx: DecodeContext, s: string): Capabilities;
|
|
11
|
-
/**
|
|
12
|
-
export
|
|
11
|
+
/** An NMEA 0183 sentence taken apart. */
|
|
12
|
+
export interface NmeaParts {
|
|
13
|
+
/** The sentence, up to and including any `*hh` checksum. */
|
|
14
|
+
sentence: string;
|
|
15
|
+
/** The comma-separated fields before any checksum, the address field first. */
|
|
16
|
+
fields: string[];
|
|
17
|
+
/** Whether the sentence has a checksum. */
|
|
18
|
+
hasChecksum: boolean;
|
|
19
|
+
/** Whether the checksum matches; true when there is none. */
|
|
20
|
+
checksumOk: boolean;
|
|
21
|
+
/** Whatever follows the checksum. */
|
|
22
|
+
rest: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Takes the text after `$` apart as an NMEA 0183 sentence (vectors interpretations.md, "What $
|
|
26
|
+
* text is an NMEA sentence"): printable ASCII, an address field of five upper-case letters or
|
|
27
|
+
* digits (or `P` and three or more), at least one field, no `$`, and no `*` but the one that
|
|
28
|
+
* starts a checksum of two hex digits, which ends the sentence. `undefined` when it is not one.
|
|
29
|
+
*/
|
|
30
|
+
export declare function splitNmea(text: string): NmeaParts | undefined;
|
|
13
31
|
/** `$`: raw NMEA, or Ultimeter raw weather (`$ULTW`). */
|
|
14
32
|
export declare function decodeDollar(ctx: DecodeContext, s: string): NmeaSentence | RawWeather;
|
|
15
33
|
/** Raw weather station data, kept as text. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"other.d.ts","sourceRoot":"","sources":["../../src/decode/other.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,QAAQ,EAER,YAAY,EAEZ,gBAAgB,EAChB,YAAY,EACZ,KAAK,EACL,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAYlD,4BAA4B;AAC5B,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,
|
|
1
|
+
{"version":3,"file":"other.d.ts","sourceRoot":"","sources":["../../src/decode/other.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,QAAQ,EAER,YAAY,EAEZ,gBAAgB,EAChB,YAAY,EACZ,KAAK,EACL,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAYlD,4BAA4B;AAC5B,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAqCxE;AAID,+BAA+B;AAC/B,wBAAgB,eAAe,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,eAAe,CA0D9E;AAMD,4BAA4B;AAC5B,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,KAAK,CAiBhE;AAcD,iCAAiC;AACjC,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAmB9E;AAwCD,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,2CAA2C;IAC3C,WAAW,EAAE,OAAO,CAAC;IACrB,6DAA6D;IAC7D,UAAU,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAgB7D;AAED,yDAAyD;AACzD,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,CA8DrF;AAED,8CAA8C;AAC9C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAIvG;AAED,6CAA6C;AAC7C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,CAU5E;AAED,iCAAiC;AACjC,wBAAgB,UAAU,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,QAAQ,CAGlE;AAED,gDAAgD;AAChD,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,QAAQ,CAKpE;AAED,wCAAwC;AACxC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAWhF"}
|
package/dist/decode/other.js
CHANGED
|
@@ -42,7 +42,8 @@ export function decodeStatus(ctx, s) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
// The ERP codes run 1-9, : to @, then A-K; there is no 0 (APRS12c ch. 16).
|
|
46
|
+
const beam = /\^([0-9A-Z])([1-9:;<=>?@A-K])$/.exec(body);
|
|
46
47
|
if (beam) {
|
|
47
48
|
out.beam = { headingCode: beam[1], powerCode: beam[2] };
|
|
48
49
|
body = body.slice(0, beam.index);
|
|
@@ -123,6 +124,9 @@ export function decodeTelemetry(ctx, s) {
|
|
|
123
124
|
out.comment = ctx.text(comment);
|
|
124
125
|
return out;
|
|
125
126
|
}
|
|
127
|
+
const FOOTPRINT_NUMBER = '[0-9]+\\.?[0-9]*|\\.[0-9]+';
|
|
128
|
+
const FOOTPRINT_VALUE = `( ?(?:${FOOTPRINT_NUMBER})|-(?:${FOOTPRINT_NUMBER}))`;
|
|
129
|
+
const FOOTPRINT_RE = new RegExp(`^${FOOTPRINT_VALUE},${FOOTPRINT_VALUE},([0-9]{4})$`);
|
|
126
130
|
/** `?`: a general query. */
|
|
127
131
|
export function decodeQuery(ctx, s) {
|
|
128
132
|
const m = /^\?([A-Z]+)\?(.*)$/s.exec(s);
|
|
@@ -131,27 +135,51 @@ export function decodeQuery(ctx, s) {
|
|
|
131
135
|
const out = { type: 'query', queryType: m[1] };
|
|
132
136
|
const rest = m[2];
|
|
133
137
|
if (rest.length > 0) {
|
|
134
|
-
|
|
138
|
+
// Decimal degrees, each a number as a telemetry value is (`-.1715`), a positive one with or
|
|
139
|
+
// without a leading space, and a 4-digit radius.
|
|
140
|
+
const f = FOOTPRINT_RE.exec(rest);
|
|
135
141
|
if (!f)
|
|
136
142
|
ctx.fail('invalid-general-query');
|
|
137
|
-
|
|
143
|
+
const latitude = Number(f[1]);
|
|
144
|
+
const longitude = Number(f[2]);
|
|
145
|
+
// A footprint is a real place (vectors interpretations.md).
|
|
146
|
+
if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180)
|
|
147
|
+
ctx.fail('invalid-general-query');
|
|
148
|
+
out.footprint = { latitude, longitude, radiusMiles: Number(f[3]), latitudeText: f[1], longitudeText: f[2] };
|
|
138
149
|
}
|
|
139
150
|
return out;
|
|
140
151
|
}
|
|
152
|
+
/** Drops U+0020 spaces, and no other character, from both ends. */
|
|
153
|
+
function trimSpaces(text) {
|
|
154
|
+
let a = 0;
|
|
155
|
+
let b = text.length;
|
|
156
|
+
while (a < b && text[a] === ' ')
|
|
157
|
+
a++;
|
|
158
|
+
while (b > a && text[b - 1] === ' ')
|
|
159
|
+
b--;
|
|
160
|
+
return text.slice(a, b);
|
|
161
|
+
}
|
|
162
|
+
/** A control character: below U+0020, or U+007F. */
|
|
163
|
+
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
141
164
|
/** `<`: station capabilities. */
|
|
142
165
|
export function decodeCapabilities(ctx, s) {
|
|
143
166
|
const text = ctx.text(s.slice(1));
|
|
144
167
|
const capabilities = [];
|
|
145
168
|
let freeText = false;
|
|
146
169
|
for (const raw of text.split(',')) {
|
|
147
|
-
|
|
170
|
+
// Spaces around an item, a token or a value are padding; nothing else is (vectors
|
|
171
|
+
// interpretations.md, "Station capabilities: items, tokens and values").
|
|
172
|
+
const item = trimSpaces(raw);
|
|
148
173
|
if (item.length === 0)
|
|
149
174
|
continue; // empty items are skipped
|
|
150
175
|
const eq = item.indexOf('=');
|
|
151
|
-
const token = eq < 0 ? item : item.slice(0, eq);
|
|
152
|
-
|
|
176
|
+
const token = eq < 0 ? item : trimSpaces(item.slice(0, eq));
|
|
177
|
+
const value = eq < 0 ? undefined : trimSpaces(item.slice(eq + 1));
|
|
178
|
+
if (token.length === 0 || token.includes(' ') || CONTROL_RE.test(token))
|
|
179
|
+
freeText = true;
|
|
180
|
+
if (value !== undefined && CONTROL_RE.test(value))
|
|
153
181
|
freeText = true;
|
|
154
|
-
capabilities.push(
|
|
182
|
+
capabilities.push(value === undefined ? [token] : [token, value]);
|
|
155
183
|
}
|
|
156
184
|
if (capabilities.length === 0)
|
|
157
185
|
ctx.fail('invalid-capabilities');
|
|
@@ -159,79 +187,112 @@ export function decodeCapabilities(ctx, s) {
|
|
|
159
187
|
ctx.tolerate('free-text-capabilities');
|
|
160
188
|
return { type: 'capabilities', capabilities };
|
|
161
189
|
}
|
|
162
|
-
|
|
190
|
+
/** A coordinate: at least three digits before an optional `.` and fraction, the last two minutes. */
|
|
191
|
+
function nmeaDegrees(value, hemisphere, limit) {
|
|
163
192
|
if (value === undefined || hemisphere === undefined)
|
|
164
193
|
return undefined;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const dot = value.indexOf('.');
|
|
168
|
-
const intPart = dot < 0 ? value : value.slice(0, dot);
|
|
169
|
-
if (intPart.length < 3)
|
|
194
|
+
const m = /^([0-9]+)([0-9]{2}(?:\.[0-9]*)?)$/.exec(value);
|
|
195
|
+
if (!m)
|
|
170
196
|
return undefined;
|
|
171
|
-
const degrees = Number(
|
|
172
|
-
const minutes = Number(
|
|
197
|
+
const degrees = Number(m[1]);
|
|
198
|
+
const minutes = Number(m[2]);
|
|
173
199
|
if (minutes >= 60)
|
|
174
200
|
return undefined;
|
|
175
201
|
const v = degrees + minutes / 60;
|
|
176
|
-
if (v >
|
|
202
|
+
if (v > limit)
|
|
177
203
|
return undefined;
|
|
178
|
-
if (hemisphere === 'S'
|
|
204
|
+
if (hemisphere === (limit === 90 ? 'S' : 'W'))
|
|
179
205
|
return v === 0 ? 0 : -v;
|
|
180
|
-
if (hemisphere === 'N'
|
|
206
|
+
if (hemisphere === (limit === 90 ? 'N' : 'E'))
|
|
181
207
|
return v;
|
|
182
208
|
return undefined;
|
|
183
209
|
}
|
|
210
|
+
/** `hhmmss` with an optional fraction, as `HH:MM:SS` and the fraction as sent, less trailing zeros. */
|
|
184
211
|
function nmeaTime(value) {
|
|
185
212
|
if (value === undefined)
|
|
186
213
|
return undefined;
|
|
187
214
|
const m = /^([0-9]{2})([0-9]{2})([0-9]{2})(?:\.([0-9]*))?$/.exec(value);
|
|
188
215
|
if (!m)
|
|
189
216
|
return undefined;
|
|
217
|
+
if (Number(m[1]) > 23 || Number(m[2]) > 59 || Number(m[3]) > 59)
|
|
218
|
+
return undefined;
|
|
190
219
|
let t = `${m[1]}:${m[2]}:${m[3]}`;
|
|
191
220
|
const fraction = (m[4] ?? '').replace(/0+$/, '');
|
|
192
221
|
if (fraction.length > 0)
|
|
193
222
|
t += `.${fraction}`;
|
|
194
223
|
return t;
|
|
195
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* An optional `-`, then digits with an optional `.` and fraction. NMEA 0183 makes leading zeros
|
|
227
|
+
* optional, so a number may start with its decimal point (`.30`).
|
|
228
|
+
*/
|
|
196
229
|
function nmeaNumber(value) {
|
|
197
|
-
if (value === undefined || !/^-?(?:[0-9]
|
|
230
|
+
if (value === undefined || !/^-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$/.test(value))
|
|
198
231
|
return undefined;
|
|
199
232
|
return Number(value);
|
|
200
233
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
234
|
+
const NMEA_ADDRESS_RE = /^(?:[A-Z0-9]{5}|P[A-Z0-9]{3,})$/;
|
|
235
|
+
/**
|
|
236
|
+
* Takes the text after `$` apart as an NMEA 0183 sentence (vectors interpretations.md, "What $
|
|
237
|
+
* text is an NMEA sentence"): printable ASCII, an address field of five upper-case letters or
|
|
238
|
+
* digits (or `P` and three or more), at least one field, no `$`, and no `*` but the one that
|
|
239
|
+
* starts a checksum of two hex digits, which ends the sentence. `undefined` when it is not one.
|
|
240
|
+
*/
|
|
241
|
+
export function splitNmea(text) {
|
|
242
|
+
const star = text.indexOf('*');
|
|
243
|
+
const hasChecksum = star >= 0;
|
|
244
|
+
if (hasChecksum && !/^[0-9A-Fa-f]{2}$/.test(text.slice(star + 1, star + 3)))
|
|
245
|
+
return undefined;
|
|
246
|
+
const body = hasChecksum ? text.slice(0, star) : text;
|
|
247
|
+
if (!/^[\x20-\x7e]*$/.test(body) || body.includes('$'))
|
|
248
|
+
return undefined;
|
|
249
|
+
const fields = body.split(',');
|
|
250
|
+
if (fields.length < 2 || !NMEA_ADDRESS_RE.test(fields[0]))
|
|
205
251
|
return undefined;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
252
|
+
const end = hasChecksum ? star + 3 : text.length;
|
|
253
|
+
let checksumOk = true;
|
|
254
|
+
if (hasChecksum) {
|
|
255
|
+
let x = 0;
|
|
256
|
+
for (let k = 0; k < body.length; k++)
|
|
257
|
+
x ^= body.charCodeAt(k);
|
|
258
|
+
checksumOk = x === parseInt(text.slice(star + 1, star + 3), 16);
|
|
259
|
+
}
|
|
260
|
+
return { sentence: text.slice(0, end), fields, hasChecksum, checksumOk, rest: text.slice(end) };
|
|
210
261
|
}
|
|
211
262
|
/** `$`: raw NMEA, or Ultimeter raw weather (`$ULTW`). */
|
|
212
263
|
export function decodeDollar(ctx, s) {
|
|
213
264
|
if (s.startsWith('$ULTW'))
|
|
214
265
|
return decodeRawWeather(ctx, s.slice(5), 'ultimeter-packet');
|
|
215
266
|
ctx.info('obsolete-format');
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
267
|
+
// The structure is checked before the checksum.
|
|
268
|
+
const parts = splitNmea(s.slice(1));
|
|
269
|
+
if (!parts)
|
|
270
|
+
return ctx.fail('invalid-nmea');
|
|
271
|
+
if (!parts.checksumOk)
|
|
221
272
|
ctx.fail('nmea-checksum-mismatch');
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
if (checksum !== undefined)
|
|
273
|
+
const f = parts.fields;
|
|
274
|
+
const out = { type: 'nmea', sentence: parts.sentence };
|
|
275
|
+
if (parts.hasChecksum)
|
|
226
276
|
out.hasChecksum = true;
|
|
227
277
|
const set = (key, value) => {
|
|
228
278
|
if (value !== undefined)
|
|
229
279
|
out[key] = value;
|
|
230
280
|
};
|
|
231
|
-
|
|
281
|
+
// A position needs both coordinates.
|
|
282
|
+
const position = (lat, lon) => {
|
|
283
|
+
const latitude = nmeaDegrees(f[lat], f[lat + 1], 90);
|
|
284
|
+
const longitude = nmeaDegrees(f[lon], f[lon + 1], 180);
|
|
285
|
+
if (latitude === undefined || longitude === undefined)
|
|
286
|
+
return;
|
|
287
|
+
out.latitude = latitude;
|
|
288
|
+
out.longitude = longitude;
|
|
289
|
+
};
|
|
290
|
+
// Only an approved address (five characters, not P) has a sentence formatter.
|
|
291
|
+
const address = f[0];
|
|
292
|
+
const formatter = address.length === 5 && address[0] !== 'P' ? address.slice(2) : '';
|
|
293
|
+
switch (formatter) {
|
|
232
294
|
case 'GGA': {
|
|
233
|
-
|
|
234
|
-
set('longitude', nmeaDegrees(f[4], f[5], 180));
|
|
295
|
+
position(2, 4);
|
|
235
296
|
if (f[6] !== undefined && /^[0-9]$/.test(f[6]))
|
|
236
297
|
out.fix = f[6] === '0' ? 'invalid' : 'valid';
|
|
237
298
|
set('altitudeM', nmeaNumber(f[9]));
|
|
@@ -239,8 +300,7 @@ export function decodeDollar(ctx, s) {
|
|
|
239
300
|
break;
|
|
240
301
|
}
|
|
241
302
|
case 'GLL': {
|
|
242
|
-
|
|
243
|
-
set('longitude', nmeaDegrees(f[3], f[4], 180));
|
|
303
|
+
position(1, 3);
|
|
244
304
|
set('time', nmeaTime(f[5]));
|
|
245
305
|
if (f[6] === 'A')
|
|
246
306
|
out.fix = 'valid';
|
|
@@ -254,8 +314,7 @@ export function decodeDollar(ctx, s) {
|
|
|
254
314
|
out.fix = 'valid';
|
|
255
315
|
else if (f[2] === 'V')
|
|
256
316
|
out.fix = 'invalid';
|
|
257
|
-
|
|
258
|
-
set('longitude', nmeaDegrees(f[5], f[6], 180));
|
|
317
|
+
position(3, 5);
|
|
259
318
|
set('speedKnots', nmeaNumber(f[7]));
|
|
260
319
|
set('courseDegrees', nmeaNumber(f[8]));
|
|
261
320
|
break;
|
|
@@ -266,13 +325,15 @@ export function decodeDollar(ctx, s) {
|
|
|
266
325
|
break;
|
|
267
326
|
}
|
|
268
327
|
case 'WPL': {
|
|
269
|
-
|
|
270
|
-
set('longitude', nmeaDegrees(f[3], f[4], 180));
|
|
328
|
+
position(1, 3);
|
|
271
329
|
if (f[5] !== undefined && f[5].length > 0)
|
|
272
330
|
out.waypoint = f[5];
|
|
273
331
|
break;
|
|
274
332
|
}
|
|
275
333
|
}
|
|
334
|
+
// Text after the checksum is a comment, kept as sent.
|
|
335
|
+
if (parts.rest.length > 0)
|
|
336
|
+
out.comment = ctx.text(parts.rest);
|
|
276
337
|
return out;
|
|
277
338
|
}
|
|
278
339
|
/** Raw weather station data, kept as text. */
|
|
@@ -303,9 +364,10 @@ export function decodeTest(ctx, s) {
|
|
|
303
364
|
}
|
|
304
365
|
/** `%`: an Agrelo DFJr / MicroFinder report. */
|
|
305
366
|
export function decodeAgrelo(ctx, s) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
367
|
+
// Exactly %, a bearing of 000 to 360, / and a quality digit (vectors interpretations.md).
|
|
368
|
+
const m = /^%([0-9]{3})\/([0-9])$/.exec(s);
|
|
369
|
+
if (!m || Number(m[1]) > 360)
|
|
370
|
+
return ctx.fail('invalid-agrelo-df');
|
|
309
371
|
return { type: 'agrelo-df', bearingDegrees: Number(m[1]), quality: Number(m[2]) };
|
|
310
372
|
}
|
|
311
373
|
/** `[`: a Maidenhead locator beacon. */
|