@packet-net/pdn-aprs 0.1.1 → 0.2.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 +15 -0
- package/README.md +2 -2
- 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.js +3 -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 +10 -2
- package/dist/decode/positioned.d.ts.map +1 -1
- package/dist/decode/positioned.js +129 -77
- package/dist/decode/positioned.js.map +1 -1
- package/dist/decode/reports.d.ts.map +1 -1
- package/dist/decode/reports.js +24 -4
- package/dist/decode/reports.js.map +1 -1
- package/dist/deviceid.d.ts.map +1 -1
- package/dist/deviceid.js +18 -4
- package/dist/deviceid.js.map +1 -1
- package/dist/encode/index.d.ts.map +1 -1
- package/dist/encode/index.js +29 -12
- package/dist/encode/index.js.map +1 -1
- package/dist/encode/other.d.ts.map +1 -1
- package/dist/encode/other.js +35 -15
- package/dist/encode/other.js.map +1 -1
- package/dist/encode/position.d.ts +4 -2
- package/dist/encode/position.d.ts.map +1 -1
- package/dist/encode/position.js +58 -22
- 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/neutral.d.ts.map +1 -1
- package/dist/neutral.js +2 -0
- package/dist/neutral.js.map +1 -1
- package/dist/types.d.ts +6 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bytes.ts +2 -1
- package/src/codes.ts +8 -8
- package/src/decode/index.ts +3 -4
- package/src/decode/message.ts +54 -37
- package/src/decode/other.ts +116 -44
- package/src/decode/positioned.ts +133 -88
- package/src/decode/reports.ts +22 -3
- package/src/deviceid.ts +17 -2
- package/src/encode/index.ts +30 -12
- package/src/encode/other.ts +27 -11
- package/src/encode/position.ts +54 -20
- package/src/header.ts +9 -2
- package/src/neutral.ts +2 -0
- package/src/types.ts +6 -4
package/src/encode/other.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// telemetry, queries, capabilities, and the rest.
|
|
3
3
|
|
|
4
4
|
import { encodeUtf8Binary } from '../bytes.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { DIRECTED_QUERY_TYPES } from '../decode/message.js';
|
|
6
|
+
import { splitNmea } from '../decode/other.js';
|
|
7
|
+
import { formatPath, isAprsIsAddress } from '../header.js';
|
|
7
8
|
import type {
|
|
8
9
|
Ack,
|
|
9
10
|
AgreloDf,
|
|
@@ -124,7 +125,10 @@ export function encodeDirectedQuery(d: DirectedQuery): string {
|
|
|
124
125
|
let out = `:${addressee(d.addressee)}:?${d.queryType}`;
|
|
125
126
|
if (d.target !== undefined) {
|
|
126
127
|
if (!/^[A-Za-z0-9-]{1,9}$/.test(d.target)) refuse('a query target is one callsign');
|
|
127
|
-
|
|
128
|
+
// A type the spec defines is followed straight by the target, an APRSH target padded to 9
|
|
129
|
+
// characters; any other type has no fixed length, so its target follows one space.
|
|
130
|
+
if (!DIRECTED_QUERY_TYPES.includes(d.queryType)) out += ` ${d.target}`;
|
|
131
|
+
else out += d.queryType === 'APRSH' ? d.target.padEnd(9, ' ') : d.target;
|
|
128
132
|
}
|
|
129
133
|
return out;
|
|
130
134
|
}
|
|
@@ -150,7 +154,7 @@ export function encodeStatus(d: StatusReport): string {
|
|
|
150
154
|
}
|
|
151
155
|
out += freeText(text, 'status text');
|
|
152
156
|
if (d.beam) {
|
|
153
|
-
if (!/^[0-9A-Z]$/.test(d.beam.headingCode) || !/^[
|
|
157
|
+
if (!/^[0-9A-Z]$/.test(d.beam.headingCode) || !/^[1-9:;<=>?@A-K]$/.test(d.beam.powerCode))
|
|
154
158
|
refuse('beam heading is 0-9 or A-Z, and power 1-9 or :-K');
|
|
155
159
|
out += `^${d.beam.headingCode}${d.beam.powerCode}`;
|
|
156
160
|
}
|
|
@@ -186,10 +190,12 @@ export function encodeRawWeather(d: RawWeather): string {
|
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
export function encodeNmea(d: NmeaSentence): string {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
+
const parts = splitNmea(d.sentence);
|
|
194
|
+
if (!parts || parts.rest.length > 0) refuse('not an NMEA 0183 sentence');
|
|
195
|
+
if (!parts.checksumOk) refuse("the NMEA sentence's checksum does not match");
|
|
196
|
+
const comment = d.comment ?? '';
|
|
197
|
+
if (comment.length > 0 && !parts.hasChecksum) refuse('a comment after an NMEA sentence needs the checksum that ends the sentence');
|
|
198
|
+
return `$${d.sentence}${freeText(comment, 'the comment')}`;
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
export function encodeMaidenhead(d: MaidenheadBeacon): string {
|
|
@@ -209,14 +215,20 @@ export function encodeQuery(d: Query): string {
|
|
|
209
215
|
return out;
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
/** A control character: below U+0020, or U+007F. */
|
|
219
|
+
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
220
|
+
|
|
212
221
|
export function encodeCapabilities(d: Capabilities): string {
|
|
213
222
|
if (d.capabilities.length === 0) refuse('capabilities need at least one item');
|
|
223
|
+
// Anything that would not read back the same (vectors interpretations.md, "Station
|
|
224
|
+
// capabilities: items, tokens and values") is refused.
|
|
214
225
|
const items = d.capabilities.map((c) => {
|
|
215
226
|
const token = c[0];
|
|
216
|
-
if (token.length === 0 || /[
|
|
227
|
+
if (token.length === 0 || /[ ,=]/.test(token) || CONTROL_RE.test(token)) refuse('a capability token is not empty and has no space, control character, comma or =');
|
|
217
228
|
if (c.length === 1) return token;
|
|
218
229
|
const value = c[1]!;
|
|
219
|
-
if (value.includes(',') ||
|
|
230
|
+
if (value.includes(',') || CONTROL_RE.test(value) || value.startsWith(' ') || value.endsWith(' '))
|
|
231
|
+
refuse('a capability value has no comma or control character, and does not start or end with a space');
|
|
220
232
|
return `${token}=${value}`;
|
|
221
233
|
});
|
|
222
234
|
return '<' + encodeUtf8Binary(items.join(','));
|
|
@@ -241,7 +253,11 @@ export function encodeAgrelo(d: AgreloDf): string {
|
|
|
241
253
|
/** The third-party header and the encapsulated packet's information field. */
|
|
242
254
|
export function encodeThirdParty(d: ThirdParty, innerInfo: string): string {
|
|
243
255
|
const p = d.packet;
|
|
256
|
+
// The source is 1-9 printable ASCII characters other than > and : (APRS12c ch. 17); the
|
|
257
|
+
// destination and path are APRS-IS addresses.
|
|
258
|
+
if (!/^[\x20-\x39\x3b-\x3d\x3f-\x7e]{1,9}$/.test(p.source)) refuse('a third-party source is 1-9 printable ASCII characters other than > and :');
|
|
259
|
+
if (!isAprsIsAddress(p.destination)) refuse('the third-party destination is not a valid address');
|
|
260
|
+
for (const e of p.path) if (!isAprsIsAddress(e.address)) refuse(`the third-party path entry ${JSON.stringify(e.address)} is not a valid address`);
|
|
244
261
|
const header = p.path.length > 0 ? `${p.source}>${p.destination},${formatPath(p.path)}` : `${p.source}>${p.destination}`;
|
|
245
|
-
if (!/^[\x21-\x7e]+$/.test(header) || header.includes(':')) refuse('the third-party header is not printable');
|
|
246
262
|
return `}${header}:${innerInfo}`;
|
|
247
263
|
}
|
package/src/encode/position.ts
CHANGED
|
@@ -109,6 +109,18 @@ function compressedPosition(f: PositionedFields, cs: string | undefined, compres
|
|
|
109
109
|
return table + base91(y, 4) + base91(x, 4) + f.symbol.code + tail;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* A GGA altitude in the cs bytes: 1.002^cs feet, so the nearest value they hold (1 foot for 0 feet
|
|
114
|
+
* or below), and whether that is exact. When it is not, a `/A=` altitude carries it and wins.
|
|
115
|
+
*/
|
|
116
|
+
function ggaAltitude(feet: number): { cs: string; exact: boolean } {
|
|
117
|
+
if (!Number.isFinite(feet)) refuse('altitude is not a number');
|
|
118
|
+
const v = feet <= 1 ? 0 : Math.round(Math.log(feet) / Math.log(1.002));
|
|
119
|
+
if (v > 91 * 91 - 1) refuse('altitude is too high for the compressed format');
|
|
120
|
+
const cs = String.fromCharCode(Math.floor(v / 91) + 33) + String.fromCharCode((v % 91) + 33);
|
|
121
|
+
return { cs, exact: Math.abs(Math.pow(1.002, v) - feet) <= 1e-9 * Math.abs(feet) };
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
// ---- data extensions and comment elements
|
|
113
125
|
|
|
114
126
|
function courseSpeed(course: number | undefined, speed: number | undefined, what = 'course'): string {
|
|
@@ -191,14 +203,20 @@ export function telemetryText(t: CommentTelemetry): string {
|
|
|
191
203
|
const v = (n: number, what: string): string => base91(whole(n, 0, 8280, what), 2);
|
|
192
204
|
let out = '|' + v(t.sequence, 'telemetry sequence');
|
|
193
205
|
for (const a of t.analog) out += v(a, 'telemetry value');
|
|
194
|
-
|
|
206
|
+
// Eight binary channels; bits 9-13 are reserved.
|
|
207
|
+
if (t.digital !== undefined) out += base91(whole(t.digital, 0, 255, 'telemetry bits'), 2);
|
|
195
208
|
return out + '|';
|
|
196
209
|
}
|
|
197
210
|
|
|
198
211
|
function daoText(f: PositionedFields, latRemainder: number, lonRemainder: number, applied: boolean): string {
|
|
199
212
|
const dao = f.dao!;
|
|
200
213
|
const datum = dao.datum;
|
|
201
|
-
if (
|
|
214
|
+
if (/^[0-9]$/.test(datum)) {
|
|
215
|
+
// A local datum digit has no case to give a precision, so it only goes with spaces.
|
|
216
|
+
if (dao.precision !== 'none') refuse('a !DAO! datum digit carries no added precision');
|
|
217
|
+
return `!${datum} !`;
|
|
218
|
+
}
|
|
219
|
+
if (!/^[A-Za-z]$/.test(datum)) refuse('the !DAO! datum is a letter or a digit');
|
|
202
220
|
switch (dao.precision) {
|
|
203
221
|
case 'none':
|
|
204
222
|
return `!${datum.toUpperCase()} !`;
|
|
@@ -226,6 +244,22 @@ function weatherValue(v: number | undefined, width: number, min: number, max: nu
|
|
|
226
244
|
return pad(n, width);
|
|
227
245
|
}
|
|
228
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Snowfall written exactly in its three characters, with a decimal point where it needs one
|
|
249
|
+
* (0.32 as `.32`), and refused when they cannot hold it.
|
|
250
|
+
*/
|
|
251
|
+
function snowfallText(inches: number): string {
|
|
252
|
+
if (!Number.isFinite(inches) || inches < 0) refuse('snowfall is not a number of inches');
|
|
253
|
+
let text: string;
|
|
254
|
+
if (Number.isInteger(inches)) text = pad(whole(inches, 0, 999, 'snowfall'), 3);
|
|
255
|
+
else if (String(inches).length === 3) text = String(inches);
|
|
256
|
+
else text = inches < 1 ? '.' + pad(Math.round(inches * 100), 2) : '';
|
|
257
|
+
if (!/^[0-9.]{3}$/.test(text) || /\..*\./.test(text) || Math.abs(Number(text) - inches) > 1e-9) {
|
|
258
|
+
refuse(`snowfall ${inches} does not fit in 3 characters`);
|
|
259
|
+
}
|
|
260
|
+
return text;
|
|
261
|
+
}
|
|
262
|
+
|
|
229
263
|
/** Weather fields after the wind: gust, temperature, rain, humidity, pressure, and on. */
|
|
230
264
|
function weatherFields(w: Weather, mandatory: boolean): string {
|
|
231
265
|
let out = '';
|
|
@@ -243,12 +277,7 @@ function weatherFields(w: Weather, mandatory: boolean): string {
|
|
|
243
277
|
const l = whole(w.luminosityWM2, 0, 1999, 'luminosity');
|
|
244
278
|
out += l < 1000 ? 'L' + pad(l, 3) : 'l' + pad(l - 1000, 3);
|
|
245
279
|
}
|
|
246
|
-
if (w.snow24hIn !== undefined)
|
|
247
|
-
const s = w.snow24hIn;
|
|
248
|
-
let text = Number.isInteger(s) ? pad(whole(s, 0, 999, 'snowfall'), 3) : String(s);
|
|
249
|
-
if (text.length !== 3 || !/^[0-9.]{3}$/.test(text)) text = refuse('snowfall is 3 characters');
|
|
250
|
-
out += 's' + text;
|
|
251
|
-
}
|
|
280
|
+
if (w.snow24hIn !== undefined) out += 's' + snowfallText(w.snow24hIn);
|
|
252
281
|
if (w.rainRaw !== undefined) out += '#' + pad(whole(w.rainRaw, 0, 999, 'raw rain counter'), 3);
|
|
253
282
|
for (const e of w.extra ?? []) {
|
|
254
283
|
if (!/^[A-Za-z]$/.test(e.letter) || 'csgtrpPhbLl'.includes(e.letter)) refuse(`${e.letter} is not an extra weather field letter`);
|
|
@@ -296,9 +325,11 @@ function hasExtension(f: PositionedFields): string[] {
|
|
|
296
325
|
/**
|
|
297
326
|
* How the comment is joined on: `plain` (after a space when it follows a frequency), `joined`
|
|
298
327
|
* (straight after a frequency, with no space) or `delimited` (after a `/`), for when it would
|
|
299
|
-
* otherwise read as something else
|
|
328
|
+
* otherwise read as something else; or `braces-first`, as `plain` but with the signpost or
|
|
329
|
+
* corridor braces written before the comment, for a comment holding braces that would be taken
|
|
330
|
+
* for them (the first well-formed braces are the signpost).
|
|
300
331
|
*/
|
|
301
|
-
export type CommentMode = 'plain' | 'joined' | 'delimited';
|
|
332
|
+
export type CommentMode = 'plain' | 'joined' | 'delimited' | 'braces-first';
|
|
302
333
|
|
|
303
334
|
/** Encodes a position and what follows it. */
|
|
304
335
|
function positionedBody(f: PositionedFields, mode: CommentMode): Body {
|
|
@@ -342,11 +373,9 @@ function positionedBody(f: PositionedFields, mode: CommentMode): Body {
|
|
|
342
373
|
cs = '{' + String.fromCharCode(s + 33);
|
|
343
374
|
compression = compression ?? DEFAULT_COMPRESSION;
|
|
344
375
|
} else if (f.altitudeFeet !== undefined && compression?.source === 'gga') {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
cs = String.fromCharCode(Math.floor(v / 91) + 33) + String.fromCharCode((v % 91) + 33);
|
|
349
|
-
altitudeInCs = Math.abs(Math.pow(1.002, v) - f.altitudeFeet) <= 1e-9 * f.altitudeFeet;
|
|
376
|
+
const alt = ggaAltitude(f.altitudeFeet);
|
|
377
|
+
cs = alt.cs;
|
|
378
|
+
altitudeInCs = alt.exact;
|
|
350
379
|
} else if (compression !== undefined) {
|
|
351
380
|
refuse('a compression type needs cs data (course/speed, range or altitude)');
|
|
352
381
|
}
|
|
@@ -393,7 +422,8 @@ function positionedBody(f: PositionedFields, mode: CommentMode): Body {
|
|
|
393
422
|
if (f.frequency) text += (extensionEnded && !out.endsWith('/') ? '/' : '') + frequencyText(f.frequency);
|
|
394
423
|
let braces = '';
|
|
395
424
|
if (f.signpost !== undefined) {
|
|
396
|
-
|
|
425
|
+
// Printable ASCII, a space included, other than the braces around it.
|
|
426
|
+
if (!/^[\x20-\x7a\x7c\x7e]{1,3}$/.test(f.signpost)) refuse('a signpost is 1-3 printable ASCII characters other than { and }');
|
|
397
427
|
braces = `{${f.signpost}}`;
|
|
398
428
|
}
|
|
399
429
|
if (f.area?.corridorWidthMiles !== undefined) {
|
|
@@ -405,7 +435,7 @@ function positionedBody(f: PositionedFields, mode: CommentMode): Body {
|
|
|
405
435
|
if (text.length > 0 && mode !== 'joined') text += ' ';
|
|
406
436
|
text += (mode === 'delimited' ? '/' : '') + freeText(comment, 'the comment');
|
|
407
437
|
}
|
|
408
|
-
text
|
|
438
|
+
text = mode === 'braces-first' ? braces + text : text + braces;
|
|
409
439
|
if (f.telemetry) text += telemetryText(f.telemetry);
|
|
410
440
|
if (f.dao) text += daoText(f, latRem, lonRem, daoApplied);
|
|
411
441
|
const body: Body = { text: out + text };
|
|
@@ -448,9 +478,9 @@ function weatherBody(f: PositionedFields): Body {
|
|
|
448
478
|
compression = compression ?? DEFAULT_COMPRESSION;
|
|
449
479
|
if (compression.source === 'gga') refuse('a GGA compression type means the cs bytes hold altitude');
|
|
450
480
|
} else if (f.altitudeFeet !== undefined && compression?.source === 'gga') {
|
|
451
|
-
const
|
|
452
|
-
cs =
|
|
453
|
-
altitudeInCs =
|
|
481
|
+
const alt = ggaAltitude(f.altitudeFeet);
|
|
482
|
+
cs = alt.cs;
|
|
483
|
+
altitudeInCs = alt.exact;
|
|
454
484
|
} else if (f.rangeMiles !== undefined) {
|
|
455
485
|
if (compression?.source === 'gga') refuse('a GGA compression type means the cs bytes hold altitude');
|
|
456
486
|
const s = Math.round(Math.log(f.rangeMiles / 2) / Math.log(1.08));
|
|
@@ -656,6 +686,10 @@ export function encodeMicE(d: MicEReport, mode: CommentMode): { text: string; de
|
|
|
656
686
|
if (d.typeCode === undefined) refuse('a Mic-E device suffix needs a type code');
|
|
657
687
|
text += d.deviceSuffix;
|
|
658
688
|
}
|
|
689
|
+
// Mic-E status text must not start with 0x1D, which would be taken for Rev 0 telemetry
|
|
690
|
+
// (APRS12c ch. 10), so a comment that would start it is written after a /.
|
|
691
|
+
const legacy = d.legacyTelemetry !== undefined && d.legacyTelemetry.length > 0;
|
|
692
|
+
if (!legacy && text.startsWith('\x1d') && mode !== 'delimited') return encodeMicE(d, 'delimited');
|
|
659
693
|
info += text;
|
|
660
694
|
return { text: info, destination };
|
|
661
695
|
}
|
package/src/header.ts
CHANGED
|
@@ -71,17 +71,24 @@ export function parseTnc2Header(line: Uint8Array, options: ParseOptions): Parsed
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
74
|
+
/** A third-party source: 1-9 printable ASCII characters (APRS12c ch. 17); `>` and `:` end it. */
|
|
75
|
+
const THIRD_PARTY_SOURCE = /^[\x20-\x7e]{1,9}$/;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parses `SOURCE>DEST,PATH` (without the colon). Used for TNC2 lines and third-party headers,
|
|
79
|
+
* whose source need not be an APRS-IS address (`thirdParty`).
|
|
80
|
+
*/
|
|
75
81
|
export function parseHeaderText(
|
|
76
82
|
header: string,
|
|
77
83
|
options: ParseOptions,
|
|
78
84
|
diags: Diagnostic[],
|
|
85
|
+
thirdParty = false,
|
|
79
86
|
): { source: string; destination: string; path: PathEntry[] } {
|
|
80
87
|
const gt = header.indexOf('>');
|
|
81
88
|
if (gt < 0) fail(diags, 'invalid-header');
|
|
82
89
|
const source = header.slice(0, gt);
|
|
83
90
|
if (source.length === 0) fail(diags, 'invalid-header');
|
|
84
|
-
if (!APRS_IS_ADDRESS.test(source)) fail(diags, 'invalid-address');
|
|
91
|
+
if (!(thirdParty ? THIRD_PARTY_SOURCE : APRS_IS_ADDRESS).test(source)) fail(diags, 'invalid-address');
|
|
85
92
|
const parts = header.slice(gt + 1).split(',');
|
|
86
93
|
const destination = parts[0]!;
|
|
87
94
|
if (destination.length === 0) tolerate(options, diags, 'empty-destination');
|
package/src/neutral.ts
CHANGED
|
@@ -281,6 +281,7 @@ export function toNeutralData(d: AprsData): Neutral {
|
|
|
281
281
|
put(o, 'altitude_m', d.altitudeM);
|
|
282
282
|
put(o, 'time', d.time);
|
|
283
283
|
put(o, 'waypoint', d.waypoint);
|
|
284
|
+
put(o, 'comment', d.comment);
|
|
284
285
|
break;
|
|
285
286
|
case 'maidenhead-beacon':
|
|
286
287
|
put(o, 'locator', d.locator);
|
|
@@ -560,6 +561,7 @@ export function fromNeutralData(d: J): AprsData {
|
|
|
560
561
|
opt(o, 'altitudeM', d.altitude_m);
|
|
561
562
|
opt(o, 'time', d.time);
|
|
562
563
|
opt(o, 'waypoint', d.waypoint);
|
|
564
|
+
opt(o, 'comment', d.comment);
|
|
563
565
|
break;
|
|
564
566
|
case 'maidenhead-beacon':
|
|
565
567
|
o.locator = d.locator ?? '';
|
package/src/types.ts
CHANGED
|
@@ -136,7 +136,7 @@ export type DaoPrecision = 'none' | 'thousandths' | 'base91';
|
|
|
136
136
|
|
|
137
137
|
/** The `!DAO!` datum and precision (APRS12c ch. 5). */
|
|
138
138
|
export interface Dao {
|
|
139
|
-
/** The datum letter, upper case (`W` is WGS84). */
|
|
139
|
+
/** The datum: a letter, upper case (`W` is WGS84), or a digit for a local datum. */
|
|
140
140
|
readonly datum: string;
|
|
141
141
|
readonly precision: DaoPrecision;
|
|
142
142
|
}
|
|
@@ -146,7 +146,7 @@ export interface CommentTelemetry {
|
|
|
146
146
|
readonly sequence: number;
|
|
147
147
|
/** One to five analog values, 0-8280. */
|
|
148
148
|
readonly analog: readonly number[];
|
|
149
|
-
/** The eight
|
|
149
|
+
/** The eight binary channels as a number, 0-255, B1 the least significant bit. */
|
|
150
150
|
readonly digital?: number;
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -419,7 +419,7 @@ export interface RawWeather {
|
|
|
419
419
|
/** A raw NMEA sentence (`$`). */
|
|
420
420
|
export interface NmeaSentence {
|
|
421
421
|
readonly type: 'nmea';
|
|
422
|
-
/** The sentence without the
|
|
422
|
+
/** The sentence without the `$`, up to and including any `*hh` checksum. */
|
|
423
423
|
readonly sentence: string;
|
|
424
424
|
readonly hasChecksum?: boolean;
|
|
425
425
|
readonly latitude?: number;
|
|
@@ -428,9 +428,11 @@ export interface NmeaSentence {
|
|
|
428
428
|
readonly courseDegrees?: number;
|
|
429
429
|
readonly speedKnots?: number;
|
|
430
430
|
readonly altitudeM?: number;
|
|
431
|
-
/** `HH:MM:SS`, with any fraction of a second. */
|
|
431
|
+
/** `HH:MM:SS`, with any fraction of a second as sent. */
|
|
432
432
|
readonly time?: string;
|
|
433
433
|
readonly waypoint?: string;
|
|
434
|
+
/** Text after the checksum (TinyTrack and FreeTrak send one), as sent. */
|
|
435
|
+
readonly comment?: string;
|
|
434
436
|
}
|
|
435
437
|
|
|
436
438
|
/** A Maidenhead locator beacon (`[`), obsolete. */
|