@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
@@ -0,0 +1,247 @@
1
+ // Encoding everything that is not a position: messages and what is sent in message form, status,
2
+ // telemetry, queries, capabilities, and the rest.
3
+
4
+ import { encodeUtf8Binary } from '../bytes.js';
5
+ import { nmeaChecksumOk } from '../decode/other.js';
6
+ import { formatPath } from '../header.js';
7
+ import type {
8
+ Ack,
9
+ AgreloDf,
10
+ Bulletin,
11
+ Capabilities,
12
+ DirectedQuery,
13
+ MaidenheadBeacon,
14
+ Message,
15
+ NmeaSentence,
16
+ NwsBulletin,
17
+ Query,
18
+ RawWeather,
19
+ Reject,
20
+ StatusReport,
21
+ TelemetryBits,
22
+ TelemetryCoefficients,
23
+ TelemetryNames,
24
+ TelemetryReport,
25
+ TelemetryUnits,
26
+ TestData,
27
+ ThirdParty,
28
+ UserDefined,
29
+ } from '../types.js';
30
+ import { checkSymbol, freeText, isValidTimestamp, numberText, pad, refuse, whole } from './common.js';
31
+
32
+ const MESSAGE_ID = /^[A-Za-z0-9]{1,5}$/;
33
+ const REPLY_ACK = /^[A-Za-z0-9]{0,5}$/;
34
+
35
+ function addressee(a: string): string {
36
+ if (a.length === 0 || a.length > 9) refuse(`the addressee ${JSON.stringify(a)} is not 1-9 characters`);
37
+ if (!/^[\x21-\x7e]+$/.test(a) || a.includes(':')) refuse('the addressee has characters that cannot appear in one');
38
+ return a.padEnd(9, ' ');
39
+ }
40
+
41
+ function messageId(id: string | undefined, replyAck: string | undefined): string {
42
+ if (id === undefined) {
43
+ if (replyAck !== undefined) refuse('a reply-ack goes with a message ID');
44
+ return '';
45
+ }
46
+ if (!MESSAGE_ID.test(id)) refuse('a message ID is 1-5 letters or digits');
47
+ if (replyAck === undefined) return `{${id}`;
48
+ if (!REPLY_ACK.test(replyAck)) refuse('a reply-ack is 0-5 letters or digits');
49
+ return `{${id}}${replyAck}`;
50
+ }
51
+
52
+ function messageText(text: string, what = 'message text'): string {
53
+ if (text.includes('{')) refuse(`${what} cannot contain {, which starts a message ID`);
54
+ if ([...text].length > 67) refuse(`${what} is over 67 characters`);
55
+ return freeText(text, what);
56
+ }
57
+
58
+ export function encodeMessage(d: Message): string {
59
+ return `:${addressee(d.addressee)}:${messageText(d.text)}${messageId(d.messageId, d.replyAck)}`;
60
+ }
61
+
62
+ export function encodeAck(d: Ack | Reject): string {
63
+ if ((d as { messageId?: string }).messageId !== undefined) refuse('an ack or rej has no message ID of its own');
64
+ const id = d.type === 'ack' ? d.ackedId : d.rejectedId;
65
+ if (!MESSAGE_ID.test(id)) refuse('the acked message ID is 1-5 letters or digits');
66
+ let out = `:${addressee(d.addressee)}:${d.type === 'ack' ? 'ack' : 'rej'}${id}`;
67
+ if (d.replyAck !== undefined) {
68
+ if (!REPLY_ACK.test(d.replyAck)) refuse('a reply-ack is 0-5 letters or digits');
69
+ out += `}${d.replyAck}`;
70
+ }
71
+ return out;
72
+ }
73
+
74
+ export function encodeBulletin(d: Bulletin | NwsBulletin): string {
75
+ if (d.type === 'bulletin') {
76
+ if (!/^BLN[0-9A-Z]/.test(d.addressee)) refuse('a bulletin addressee is BLN then a digit or letter');
77
+ if (/^BLN[A-Z]./.test(d.addressee)) refuse('a group bulletin has a digit after BLN');
78
+ if (d.text.includes('{') || [...d.text].length > 67) refuse('bulletin text is up to 67 characters, without {');
79
+ } else {
80
+ if (!/^NWS[-_]/.test(d.addressee)) refuse('an NWS bulletin addressee starts NWS-');
81
+ if (d.text.includes('{')) refuse('bulletin text cannot contain {');
82
+ }
83
+ return `:${addressee(d.addressee)}:${freeText(d.text, 'bulletin text')}${messageId(d.messageId, undefined)}`;
84
+ }
85
+
86
+ function listItem(item: string, what: string): string {
87
+ if (item.includes(',')) refuse(`a telemetry ${what} cannot contain a comma`);
88
+ return item;
89
+ }
90
+
91
+ export function encodeTelemetryNames(d: TelemetryNames | TelemetryUnits): string {
92
+ const list = d.type === 'telemetry-names' ? d.names : d.units;
93
+ const what = d.type === 'telemetry-names' ? 'name' : 'unit';
94
+ if (list.length === 0) refuse(`telemetry ${what}s need at least one`);
95
+ if (list.length > 13) refuse(`telemetry ${what}s are at most 13 (5 analog, 8 digital)`);
96
+ const body = (d.type === 'telemetry-names' ? 'PARM.' : 'UNIT.') + list.map((n) => listItem(n, what)).join(',');
97
+ if (body.includes('{')) refuse('telemetry metadata cannot contain {');
98
+ return `:${addressee(d.addressee)}:${freeText(body, 'telemetry metadata')}${messageId(d.messageId, undefined)}`;
99
+ }
100
+
101
+ export function encodeTelemetryCoefficients(d: TelemetryCoefficients): string {
102
+ const list = d.coefficients;
103
+ if (list.length === 0 || list.length > 15) refuse('telemetry coefficients are 1-15 numbers (a, b, c for five channels)');
104
+ const texts = list.map((v, i) => {
105
+ const t = d.coefficientsText?.[i];
106
+ return t !== undefined && Number(t) === v ? t : numberText(v);
107
+ });
108
+ return `:${addressee(d.addressee)}:EQNS.${texts.join(',')}${messageId(d.messageId, undefined)}`;
109
+ }
110
+
111
+ export function encodeTelemetryBits(d: TelemetryBits): string {
112
+ if (!/^[01]{8}$/.test(d.bits)) refuse('telemetry bit sense is eight 0/1 characters');
113
+ let body = `BITS.${d.bits}`;
114
+ if (d.project !== undefined && d.project.length > 0) {
115
+ if ([...d.project].length > 23) refuse('a telemetry project title is at most 23 characters');
116
+ if (d.project.includes('{')) refuse('a telemetry project title cannot contain {');
117
+ body += `,${d.project}`;
118
+ }
119
+ return `:${addressee(d.addressee)}:${freeText(body, 'the project title')}${messageId(d.messageId, undefined)}`;
120
+ }
121
+
122
+ export function encodeDirectedQuery(d: DirectedQuery): string {
123
+ if (!/^(?:[A-Z]+|PING\?)$/.test(d.queryType)) refuse('a query type is upper-case letters');
124
+ let out = `:${addressee(d.addressee)}:?${d.queryType}`;
125
+ if (d.target !== undefined) {
126
+ if (!/^[A-Za-z0-9-]{1,9}$/.test(d.target)) refuse('a query target is one callsign');
127
+ out += d.target;
128
+ }
129
+ return out;
130
+ }
131
+
132
+ export function encodeStatus(d: StatusReport): string {
133
+ let out = '>';
134
+ if (d.timestamp) {
135
+ if (d.timestamp.kind !== 'dhm-zulu') refuse('a status timestamp is day/hour/minute zulu');
136
+ if (!isValidTimestamp(d.timestamp)) refuse('the timestamp is out of range');
137
+ if (d.locator !== undefined) refuse('a status report with a locator has no timestamp');
138
+ const t = d.timestamp;
139
+ out += `${pad(t.day, 2)}${pad(t.hour, 2)}${pad(t.minute, 2)}z`;
140
+ }
141
+ const text = d.text ?? '';
142
+ if (d.locator !== undefined) {
143
+ if (!/^[A-R]{2}[0-9]{2}(?:[A-X]{2})?$/.test(d.locator)) refuse('the locator is 4 or 6 characters, upper case');
144
+ if (!d.symbol) refuse('a status locator is followed by a symbol');
145
+ checkSymbol(d.symbol);
146
+ out += d.locator + d.symbol.table + d.symbol.code;
147
+ if (text.length > 0 || d.beam) out += ' ';
148
+ } else if (d.symbol) {
149
+ refuse('a status symbol goes with a locator');
150
+ }
151
+ out += freeText(text, 'status text');
152
+ if (d.beam) {
153
+ if (!/^[0-9A-Z]$/.test(d.beam.headingCode) || !/^[0-9:;<=>?@A-K]$/.test(d.beam.powerCode))
154
+ refuse('beam heading is 0-9 or A-Z, and power 1-9 or :-K');
155
+ out += `^${d.beam.headingCode}${d.beam.powerCode}`;
156
+ }
157
+ return out;
158
+ }
159
+
160
+ export function encodeTelemetryReport(d: TelemetryReport): string {
161
+ if (d.sequence !== 'MIC' && !/^[A-Za-z0-9]+$/.test(d.sequence)) refuse('a telemetry sequence is letters and digits, or MIC');
162
+ if (d.analog.length !== 5) refuse('a telemetry report has five analog values');
163
+ if (d.bits === undefined || !/^[01]{8}$/.test(d.bits)) refuse('a telemetry report has eight 0/1 bits');
164
+ const values = d.analog.map((v, i) => {
165
+ if (v === null) return '';
166
+ const t = d.analogText?.[i];
167
+ if (t !== undefined && t !== null && Number(t) === v) return t;
168
+ // The classic form: three digits, 000-999 (APRS12c ch. 13).
169
+ return Number.isInteger(v) && v >= 0 && v <= 999 ? pad(v, 3) : numberText(v);
170
+ });
171
+ const seq = d.sequence === 'MIC' ? 'MIC' : `${d.sequence},`;
172
+ return `T#${seq}${values.join(',')},${d.bits}${freeText(d.comment ?? '', 'the comment')}`;
173
+ }
174
+
175
+ const RAW_PREFIX: Record<RawWeather['format'], string> = {
176
+ 'peet-bros-hash': '#',
177
+ 'peet-bros-star': '*',
178
+ 'ultimeter-packet': '$ULTW',
179
+ 'ultimeter-logging': '!!',
180
+ };
181
+
182
+ export function encodeRawWeather(d: RawWeather): string {
183
+ const data = d.data ?? '';
184
+ if (!/^[\x20-\x7e]*$/.test(data)) refuse('raw weather data is printable ASCII');
185
+ return RAW_PREFIX[d.format] + data;
186
+ }
187
+
188
+ export function encodeNmea(d: NmeaSentence): string {
189
+ if (!/^[A-Z]{5},[\x20-\x7e]*$/.test(d.sentence)) refuse('not an NMEA sentence');
190
+ const checksum = nmeaChecksumOk(d.sentence);
191
+ if (checksum === false) refuse("the NMEA sentence's checksum does not match");
192
+ return `$${d.sentence}`;
193
+ }
194
+
195
+ export function encodeMaidenhead(d: MaidenheadBeacon): string {
196
+ if (!/^[A-R]{2}[0-9]{2}(?:[A-X]{2})?$/.test(d.locator)) refuse('the locator is 4 or 6 characters, upper case');
197
+ return `[${d.locator}]${freeText(d.comment ?? '', 'the comment')}`;
198
+ }
199
+
200
+ export function encodeQuery(d: Query): string {
201
+ if (!/^[A-Z]+$/.test(d.queryType)) refuse('a query type is upper-case letters');
202
+ let out = `?${d.queryType}?`;
203
+ if (d.footprint) {
204
+ const f = d.footprint;
205
+ if (!(f.latitude >= -90 && f.latitude <= 90) || !(f.longitude >= -180 && f.longitude <= 180)) refuse('the footprint is out of range');
206
+ const coord = (v: number): string => (v < 0 ? numberText(v) : ` ${numberText(v)}`);
207
+ out += `${coord(f.latitude)},${numberText(f.longitude)},${pad(whole(f.radiusMiles, 0, 9999, 'radius'), 4)}`;
208
+ }
209
+ return out;
210
+ }
211
+
212
+ export function encodeCapabilities(d: Capabilities): string {
213
+ if (d.capabilities.length === 0) refuse('capabilities need at least one item');
214
+ const items = d.capabilities.map((c) => {
215
+ const token = c[0];
216
+ if (token.length === 0 || /[\s,=]/.test(token)) refuse('a capability token has no space, comma or =');
217
+ if (c.length === 1) return token;
218
+ const value = c[1]!;
219
+ if (value.includes(',') || /^\s|\s$/.test(value)) refuse('a capability value has no comma or edge spaces');
220
+ return `${token}=${value}`;
221
+ });
222
+ return '<' + encodeUtf8Binary(items.join(','));
223
+ }
224
+
225
+ export function encodeUserDefined(d: UserDefined): string {
226
+ if (d.userId.length !== 1 || d.packetType.length !== 1) refuse('a user ID and packet type are one character each');
227
+ const data = d.data ?? '';
228
+ if (/[^\x00-\xff]/.test(data)) refuse('user-defined data is bytes (U+0000-U+00FF)');
229
+ if (/[\r\n]$/.test(data)) refuse('user-defined data cannot end in a line break');
230
+ return `{${d.userId}${d.packetType}${data}`;
231
+ }
232
+
233
+ export function encodeTest(d: TestData): string {
234
+ return `,${freeText(d.data ?? '', 'test data')}`;
235
+ }
236
+
237
+ export function encodeAgrelo(d: AgreloDf): string {
238
+ return `%${pad(whole(d.bearingDegrees, 0, 360, 'bearing'), 3)}/${whole(d.quality, 0, 9, 'quality')}`;
239
+ }
240
+
241
+ /** The third-party header and the encapsulated packet's information field. */
242
+ export function encodeThirdParty(d: ThirdParty, innerInfo: string): string {
243
+ const p = d.packet;
244
+ 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
+ return `}${header}:${innerInfo}`;
247
+ }