@packet-net/pdn-aprs 0.0.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +661 -0
  3. package/README.md +192 -0
  4. package/THIRD-PARTY-NOTICES.md +5 -0
  5. package/dist/builder.d.ts +232 -0
  6. package/dist/builder.d.ts.map +1 -0
  7. package/dist/builder.js +508 -0
  8. package/dist/builder.js.map +1 -0
  9. package/dist/bytes.d.ts +19 -0
  10. package/dist/bytes.d.ts.map +1 -0
  11. package/dist/bytes.js +120 -0
  12. package/dist/bytes.js.map +1 -0
  13. package/dist/codes.d.ts +13 -0
  14. package/dist/codes.d.ts.map +1 -0
  15. package/dist/codes.js +112 -0
  16. package/dist/codes.js.map +1 -0
  17. package/dist/decode/context.d.ts +33 -0
  18. package/dist/decode/context.d.ts.map +1 -0
  19. package/dist/decode/context.js +65 -0
  20. package/dist/decode/context.js.map +1 -0
  21. package/dist/decode/index.d.ts +10 -0
  22. package/dist/decode/index.d.ts.map +1 -0
  23. package/dist/decode/index.js +121 -0
  24. package/dist/decode/index.js.map +1 -0
  25. package/dist/decode/message.d.ts +5 -0
  26. package/dist/decode/message.d.ts.map +1 -0
  27. package/dist/decode/message.js +190 -0
  28. package/dist/decode/message.js.map +1 -0
  29. package/dist/decode/other.d.ts +25 -0
  30. package/dist/decode/other.d.ts.map +1 -0
  31. package/dist/decode/other.js +326 -0
  32. package/dist/decode/other.js.map +1 -0
  33. package/dist/decode/positioned.d.ts +132 -0
  34. package/dist/decode/positioned.d.ts.map +1 -0
  35. package/dist/decode/positioned.js +1063 -0
  36. package/dist/decode/positioned.js.map +1 -0
  37. package/dist/decode/reports.d.ts +13 -0
  38. package/dist/decode/reports.d.ts.map +1 -0
  39. package/dist/decode/reports.js +375 -0
  40. package/dist/decode/reports.js.map +1 -0
  41. package/dist/decoder.d.ts +23 -0
  42. package/dist/decoder.d.ts.map +1 -0
  43. package/dist/decoder.js +75 -0
  44. package/dist/decoder.js.map +1 -0
  45. package/dist/deviceid-data.d.ts +33 -0
  46. package/dist/deviceid-data.d.ts.map +1 -0
  47. package/dist/deviceid-data.js +2941 -0
  48. package/dist/deviceid-data.js.map +1 -0
  49. package/dist/deviceid.d.ts +12 -0
  50. package/dist/deviceid.d.ts.map +1 -0
  51. package/dist/deviceid.js +86 -0
  52. package/dist/deviceid.js.map +1 -0
  53. package/dist/diagnostics.d.ts +48 -0
  54. package/dist/diagnostics.d.ts.map +1 -0
  55. package/dist/diagnostics.js +65 -0
  56. package/dist/diagnostics.js.map +1 -0
  57. package/dist/encode/common.d.ts +19 -0
  58. package/dist/encode/common.d.ts.map +1 -0
  59. package/dist/encode/common.js +84 -0
  60. package/dist/encode/common.js.map +1 -0
  61. package/dist/encode/index.d.ts +35 -0
  62. package/dist/encode/index.d.ts.map +1 -0
  63. package/dist/encode/index.js +248 -0
  64. package/dist/encode/index.js.map +1 -0
  65. package/dist/encode/other.d.ts +21 -0
  66. package/dist/encode/other.d.ts.map +1 -0
  67. package/dist/encode/other.js +253 -0
  68. package/dist/encode/other.js.map +1 -0
  69. package/dist/encode/position.d.ts +31 -0
  70. package/dist/encode/position.d.ts.map +1 -0
  71. package/dist/encode/position.js +729 -0
  72. package/dist/encode/position.js.map +1 -0
  73. package/dist/header.d.ts +42 -0
  74. package/dist/header.d.ts.map +1 -0
  75. package/dist/header.js +206 -0
  76. package/dist/header.js.map +1 -0
  77. package/dist/index.d.ts +13 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +14 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/neutral.d.ts +22 -0
  82. package/dist/neutral.d.ts.map +1 -0
  83. package/dist/neutral.js +593 -0
  84. package/dist/neutral.js.map +1 -0
  85. package/dist/symbols-data.d.ts +832 -0
  86. package/dist/symbols-data.d.ts.map +1 -0
  87. package/dist/symbols-data.js +173 -0
  88. package/dist/symbols-data.js.map +1 -0
  89. package/dist/symbols.d.ts +35 -0
  90. package/dist/symbols.d.ts.map +1 -0
  91. package/dist/symbols.js +71 -0
  92. package/dist/symbols.js.map +1 -0
  93. package/dist/types.d.ts +458 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +4 -0
  96. package/dist/types.js.map +1 -0
  97. package/package.json +68 -0
  98. package/src/builder.ts +617 -0
  99. package/src/bytes.ts +118 -0
  100. package/src/codes.ts +218 -0
  101. package/src/decode/context.ts +63 -0
  102. package/src/decode/index.ts +135 -0
  103. package/src/decode/message.ts +194 -0
  104. package/src/decode/other.ts +307 -0
  105. package/src/decode/positioned.ts +1161 -0
  106. package/src/decode/reports.ts +378 -0
  107. package/src/decoder.ts +76 -0
  108. package/src/deviceid-data.ts +2954 -0
  109. package/src/deviceid.ts +83 -0
  110. package/src/diagnostics.ts +86 -0
  111. package/src/encode/common.ts +87 -0
  112. package/src/encode/index.ts +280 -0
  113. package/src/encode/other.ts +247 -0
  114. package/src/encode/position.ts +656 -0
  115. package/src/header.ts +221 -0
  116. package/src/index.ts +57 -0
  117. package/src/neutral.ts +595 -0
  118. package/src/symbols-data.ts +181 -0
  119. package/src/symbols.ts +85 -0
  120. package/src/types.ts +571 -0
package/src/header.ts ADDED
@@ -0,0 +1,221 @@
1
+ // The packet header: TNC2 text (`SOURCE>DEST,PATH:`) and AX.25 frames in KISS form.
2
+
3
+ import { bytesToBinary } from './bytes.js';
4
+ import type { DiagnosticCode } from './codes.js';
5
+ import { diagnostic, type Diagnostic, type ParseOptions } from './diagnostics.js';
6
+ import type { PathEntry, QConstruct } from './types.js';
7
+
8
+ /** Thrown when a packet's header is unusable, so nothing can be decoded. */
9
+ export class AprsHeaderError extends Error {
10
+ /** Why: the diagnostics, the last of them the error. */
11
+ readonly diagnostics: readonly Diagnostic[];
12
+
13
+ constructor(diagnostics: readonly Diagnostic[]) {
14
+ const last = diagnostics[diagnostics.length - 1];
15
+ super(last ? last.message : 'The header is unusable.');
16
+ this.name = 'AprsHeaderError';
17
+ this.diagnostics = diagnostics;
18
+ }
19
+ }
20
+
21
+ /** A parsed header and the information field that follows it. */
22
+ export interface ParsedHeader {
23
+ source: string;
24
+ destination: string;
25
+ path: PathEntry[];
26
+ /** The information field bytes. */
27
+ info: Uint8Array;
28
+ diagnostics: Diagnostic[];
29
+ }
30
+
31
+ class HeaderRejected {
32
+ constructor(readonly code: DiagnosticCode) {}
33
+ }
34
+
35
+ function tolerate(options: ParseOptions, diags: Diagnostic[], code: DiagnosticCode): void {
36
+ if (options.tolerates(code)) diags.push(diagnostic('warning', code));
37
+ else {
38
+ diags.push(diagnostic('error', code));
39
+ throw new HeaderRejected(code);
40
+ }
41
+ }
42
+
43
+ function fail(diags: Diagnostic[], code: DiagnosticCode): never {
44
+ diags.push(diagnostic('error', code));
45
+ throw new HeaderRejected(code);
46
+ }
47
+
48
+ const APRS_IS_ADDRESS = /^[A-Za-z0-9-]{1,9}$/;
49
+
50
+ /** Whether `text` is a valid APRS-IS address: 1-9 letters, digits or `-`. */
51
+ export function isAprsIsAddress(text: string): boolean {
52
+ return APRS_IS_ADDRESS.test(text);
53
+ }
54
+
55
+ /**
56
+ * Parses a TNC2 header from the bytes of a line; `info` is everything after the first `:`.
57
+ * Throws `AprsHeaderError` when the header is unusable.
58
+ */
59
+ export function parseTnc2Header(line: Uint8Array, options: ParseOptions): ParsedHeader {
60
+ const diags: Diagnostic[] = [];
61
+ try {
62
+ const colon = line.indexOf(0x3a);
63
+ if (colon < 0) fail(diags, 'invalid-header');
64
+ const header = bytesToBinary(line, 0, colon);
65
+ const info = line.subarray(colon + 1);
66
+ const parsed = parseHeaderText(header, options, diags);
67
+ return { ...parsed, info, diagnostics: diags };
68
+ } catch (e) {
69
+ if (e instanceof HeaderRejected) throw new AprsHeaderError(diags);
70
+ throw e;
71
+ }
72
+ }
73
+
74
+ /** Parses `SOURCE>DEST,PATH` (without the colon). Used for TNC2 lines and third-party headers. */
75
+ export function parseHeaderText(
76
+ header: string,
77
+ options: ParseOptions,
78
+ diags: Diagnostic[],
79
+ ): { source: string; destination: string; path: PathEntry[] } {
80
+ const gt = header.indexOf('>');
81
+ if (gt < 0) fail(diags, 'invalid-header');
82
+ const source = header.slice(0, gt);
83
+ if (source.length === 0) fail(diags, 'invalid-header');
84
+ if (!APRS_IS_ADDRESS.test(source)) fail(diags, 'invalid-address');
85
+ const parts = header.slice(gt + 1).split(',');
86
+ const destination = parts[0]!;
87
+ if (destination.length === 0) tolerate(options, diags, 'empty-destination');
88
+ else if (!APRS_IS_ADDRESS.test(destination)) fail(diags, 'invalid-address');
89
+ const raw: { address: string; marked: boolean }[] = [];
90
+ for (let i = 1; i < parts.length; i++) {
91
+ let entry = parts[i]!;
92
+ let marked = false;
93
+ if (entry.endsWith('*')) {
94
+ marked = true;
95
+ entry = entry.slice(0, -1);
96
+ }
97
+ if (entry.length === 0 && !marked) {
98
+ tolerate(options, diags, 'empty-path-entry');
99
+ continue;
100
+ }
101
+ if (!APRS_IS_ADDRESS.test(entry)) fail(diags, 'invalid-address');
102
+ raw.push({ address: entry, marked });
103
+ }
104
+ const markedCount = raw.filter((e) => e.marked).length;
105
+ if (markedCount > 1) tolerate(options, diags, 'multiple-used-markers');
106
+ return { source, destination, path: markUsed(raw) };
107
+ }
108
+
109
+ function markUsed(raw: readonly { address: string; marked: boolean }[]): PathEntry[] {
110
+ let last = -1;
111
+ raw.forEach((e, i) => {
112
+ if (e.marked) last = i;
113
+ });
114
+ return raw.map((e, i) => ({ address: e.address, used: i <= last }));
115
+ }
116
+
117
+ /** Formats a path as TNC2 text: only the last used entry is marked with `*`. */
118
+ export function formatPath(path: readonly PathEntry[]): string {
119
+ let last = -1;
120
+ path.forEach((e, i) => {
121
+ if (e.used) last = i;
122
+ });
123
+ return path.map((e, i) => (i === last ? `${e.address}*` : e.address)).join(',');
124
+ }
125
+
126
+ /** Formats a header as TNC2 text, without the colon. */
127
+ export function formatTnc2Header(source: string, destination: string, path: readonly PathEntry[]): string {
128
+ return path.length === 0 ? `${source}>${destination}` : `${source}>${destination},${formatPath(path)}`;
129
+ }
130
+
131
+ const Q_CONSTRUCT = /^qA[A-Za-z]$/;
132
+
133
+ /** The APRS-IS q-construct in a path, and the station after it. */
134
+ export function findQConstruct(path: readonly PathEntry[]): QConstruct | undefined {
135
+ for (let i = 0; i < path.length; i++) {
136
+ const address = path[i]!.address;
137
+ if (Q_CONSTRUCT.test(address)) {
138
+ const station = path[i + 1]?.address;
139
+ return station === undefined ? { construct: address } : { construct: address, station };
140
+ }
141
+ }
142
+ return undefined;
143
+ }
144
+
145
+ /** A decoded AX.25 address. */
146
+ interface Ax25Address {
147
+ text: string;
148
+ flag: boolean;
149
+ last: boolean;
150
+ }
151
+
152
+ function decodeAx25Address(
153
+ frame: Uint8Array,
154
+ offset: number,
155
+ options: ParseOptions,
156
+ diags: Diagnostic[],
157
+ ): Ax25Address {
158
+ let call = '';
159
+ let padding = false;
160
+ let nulPadded = false;
161
+ let badChars = false;
162
+ for (let i = 0; i < 6; i++) {
163
+ const b = frame[offset + i]!;
164
+ const c = b >> 1;
165
+ if (c === 0x20 || c === 0x00) {
166
+ padding = true;
167
+ if (c === 0x00) nulPadded = true;
168
+ continue;
169
+ }
170
+ if (padding) badChars = true;
171
+ const ch = String.fromCharCode(c);
172
+ if (!/[A-Z0-9]/.test(ch)) badChars = true;
173
+ call += ch;
174
+ }
175
+ if (nulPadded) tolerate(options, diags, 'nul-padded-address');
176
+ if (badChars) tolerate(options, diags, 'invalid-ax25-address-characters');
177
+ const ssidByte = frame[offset + 6]!;
178
+ const ssid = (ssidByte >> 1) & 0x0f;
179
+ return {
180
+ text: ssid === 0 ? call : `${call}-${ssid}`,
181
+ flag: (ssidByte & 0x80) !== 0,
182
+ last: (ssidByte & 0x01) !== 0,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Parses an AX.25 UI frame in KISS form (no flags, no FCS). Throws `AprsHeaderError` when it is
188
+ * not an APRS frame.
189
+ */
190
+ export function parseAx25Frame(frame: Uint8Array, options: ParseOptions): ParsedHeader {
191
+ const diags: Diagnostic[] = [];
192
+ try {
193
+ const addresses: Ax25Address[] = [];
194
+ let offset = 0;
195
+ for (;;) {
196
+ if (offset + 7 > frame.length) fail(diags, 'not-aprs-frame');
197
+ const address = decodeAx25Address(frame, offset, options, diags);
198
+ addresses.push(address);
199
+ offset += 7;
200
+ if (address.last) break;
201
+ }
202
+ if (addresses.length < 2) fail(diags, 'not-aprs-frame');
203
+ if (addresses.length > 10) fail(diags, 'too-many-digipeaters');
204
+ if (offset + 2 > frame.length) fail(diags, 'not-aprs-frame');
205
+ const control = frame[offset]!;
206
+ const pid = frame[offset + 1]!;
207
+ if ((control & ~0x10) !== 0x03 || pid !== 0xf0) fail(diags, 'not-aprs-frame');
208
+ const [dest, src, ...digis] = addresses;
209
+ const path = markUsed(digis.map((d) => ({ address: d.text, marked: d.flag })));
210
+ return {
211
+ source: src!.text,
212
+ destination: dest!.text,
213
+ path,
214
+ info: frame.subarray(offset + 2),
215
+ diagnostics: diags,
216
+ };
217
+ } catch (e) {
218
+ if (e instanceof HeaderRejected) throw new AprsHeaderError(diags);
219
+ throw e;
220
+ }
221
+ }
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ // @packet-net/pdn-aprs: an APRS encoder and decoder.
2
+
3
+ export * from './types.js';
4
+ export * from './codes.js';
5
+ export { ParseOptions, diagnostic, formatDiagnostic, type Diagnostic, type Severity } from './diagnostics.js';
6
+ export { AprsHeaderError, findQConstruct, formatPath, formatTnc2Header, isAprsIsAddress } from './header.js';
7
+ export { decodeTnc2, decodeAx25, decodeKiss, unwrapKiss } from './decoder.js';
8
+ export { decodeInformation, type DecodedInformation } from './decode/index.js';
9
+ export { identifyTocall, identifyMicE, DEVICEID_SOURCE, DEVICE_CLASSES } from './deviceid.js';
10
+ export {
11
+ toNeutralData,
12
+ toNeutralHeader,
13
+ toNeutralPacket,
14
+ fromNeutralData,
15
+ formatTimestamp,
16
+ parseTimestamp,
17
+ type Neutral,
18
+ } from './neutral.js';
19
+ export { bytesToHex, hexToBytes, encodeUtf8, decodeUtf8 } from './bytes.js';
20
+ export {
21
+ AprsEncodeError,
22
+ encodeInformation,
23
+ encodeTnc2,
24
+ encodeTnc2Bytes,
25
+ encodeAx25,
26
+ wrapKiss,
27
+ type EncodedInformation,
28
+ type PacketToEncode,
29
+ } from './encode/index.js';
30
+ export {
31
+ Symbols,
32
+ symbolOf,
33
+ symbolText,
34
+ symbolName,
35
+ describeSymbol,
36
+ withOverlay,
37
+ withoutOverlay,
38
+ overlayOf,
39
+ acceptsOverlay,
40
+ sameSymbol,
41
+ type SymbolName,
42
+ } from './symbols.js';
43
+ export {
44
+ Aprs,
45
+ PacketBuilder,
46
+ PositionBuilder,
47
+ ObjectBuilder,
48
+ ItemBuilder,
49
+ MicEBuilder,
50
+ MessageBuilder,
51
+ StatusBuilder,
52
+ TelemetryBuilder,
53
+ DataBuilder,
54
+ DEFAULT_DESTINATION,
55
+ timestampOf,
56
+ type BuiltPacket,
57
+ } from './builder.js';