@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +661 -0
- package/README.md +192 -0
- package/THIRD-PARTY-NOTICES.md +5 -0
- package/dist/builder.d.ts +232 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +508 -0
- package/dist/builder.js.map +1 -0
- package/dist/bytes.d.ts +19 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +120 -0
- package/dist/bytes.js.map +1 -0
- package/dist/codes.d.ts +13 -0
- package/dist/codes.d.ts.map +1 -0
- package/dist/codes.js +112 -0
- package/dist/codes.js.map +1 -0
- package/dist/decode/context.d.ts +33 -0
- package/dist/decode/context.d.ts.map +1 -0
- package/dist/decode/context.js +65 -0
- package/dist/decode/context.js.map +1 -0
- package/dist/decode/index.d.ts +10 -0
- package/dist/decode/index.d.ts.map +1 -0
- package/dist/decode/index.js +121 -0
- package/dist/decode/index.js.map +1 -0
- package/dist/decode/message.d.ts +5 -0
- package/dist/decode/message.d.ts.map +1 -0
- package/dist/decode/message.js +190 -0
- package/dist/decode/message.js.map +1 -0
- package/dist/decode/other.d.ts +25 -0
- package/dist/decode/other.d.ts.map +1 -0
- package/dist/decode/other.js +326 -0
- package/dist/decode/other.js.map +1 -0
- package/dist/decode/positioned.d.ts +132 -0
- package/dist/decode/positioned.d.ts.map +1 -0
- package/dist/decode/positioned.js +1063 -0
- package/dist/decode/positioned.js.map +1 -0
- package/dist/decode/reports.d.ts +13 -0
- package/dist/decode/reports.d.ts.map +1 -0
- package/dist/decode/reports.js +375 -0
- package/dist/decode/reports.js.map +1 -0
- package/dist/decoder.d.ts +23 -0
- package/dist/decoder.d.ts.map +1 -0
- package/dist/decoder.js +75 -0
- package/dist/decoder.js.map +1 -0
- package/dist/deviceid-data.d.ts +33 -0
- package/dist/deviceid-data.d.ts.map +1 -0
- package/dist/deviceid-data.js +2941 -0
- package/dist/deviceid-data.js.map +1 -0
- package/dist/deviceid.d.ts +12 -0
- package/dist/deviceid.d.ts.map +1 -0
- package/dist/deviceid.js +86 -0
- package/dist/deviceid.js.map +1 -0
- package/dist/diagnostics.d.ts +48 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +65 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/encode/common.d.ts +19 -0
- package/dist/encode/common.d.ts.map +1 -0
- package/dist/encode/common.js +84 -0
- package/dist/encode/common.js.map +1 -0
- package/dist/encode/index.d.ts +35 -0
- package/dist/encode/index.d.ts.map +1 -0
- package/dist/encode/index.js +248 -0
- package/dist/encode/index.js.map +1 -0
- package/dist/encode/other.d.ts +21 -0
- package/dist/encode/other.d.ts.map +1 -0
- package/dist/encode/other.js +253 -0
- package/dist/encode/other.js.map +1 -0
- package/dist/encode/position.d.ts +31 -0
- package/dist/encode/position.d.ts.map +1 -0
- package/dist/encode/position.js +729 -0
- package/dist/encode/position.js.map +1 -0
- package/dist/header.d.ts +42 -0
- package/dist/header.d.ts.map +1 -0
- package/dist/header.js +206 -0
- package/dist/header.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/neutral.d.ts +22 -0
- package/dist/neutral.d.ts.map +1 -0
- package/dist/neutral.js +593 -0
- package/dist/neutral.js.map +1 -0
- package/dist/symbols-data.d.ts +832 -0
- package/dist/symbols-data.d.ts.map +1 -0
- package/dist/symbols-data.js +173 -0
- package/dist/symbols-data.js.map +1 -0
- package/dist/symbols.d.ts +35 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +71 -0
- package/dist/symbols.js.map +1 -0
- package/dist/types.d.ts +458 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +68 -0
- package/src/builder.ts +617 -0
- package/src/bytes.ts +118 -0
- package/src/codes.ts +218 -0
- package/src/decode/context.ts +63 -0
- package/src/decode/index.ts +135 -0
- package/src/decode/message.ts +194 -0
- package/src/decode/other.ts +307 -0
- package/src/decode/positioned.ts +1161 -0
- package/src/decode/reports.ts +378 -0
- package/src/decoder.ts +76 -0
- package/src/deviceid-data.ts +2954 -0
- package/src/deviceid.ts +83 -0
- package/src/diagnostics.ts +86 -0
- package/src/encode/common.ts +87 -0
- package/src/encode/index.ts +280 -0
- package/src/encode/other.ts +247 -0
- package/src/encode/position.ts +656 -0
- package/src/header.ts +221 -0
- package/src/index.ts +57 -0
- package/src/neutral.ts +595 -0
- package/src/symbols-data.ts +181 -0
- package/src/symbols.ts +85 -0
- package/src/types.ts +571 -0
package/src/builder.ts
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
// A fluent builder for the packets an application typically sends.
|
|
2
|
+
//
|
|
3
|
+
// const packet = Aprs.from('M0LTE-9')
|
|
4
|
+
// .via('WIDE1-1', 'WIDE2-1')
|
|
5
|
+
// .position(51.45, -0.98)
|
|
6
|
+
// .symbol(Symbols.car)
|
|
7
|
+
// .course(88)
|
|
8
|
+
// .speed(36)
|
|
9
|
+
// .comment('Mobile')
|
|
10
|
+
// .build();
|
|
11
|
+
// packet.toTnc2(); // 'M0LTE-9>APZ001,WIDE1-1,WIDE2-1:!5127.00N/00058.80W>088/036Mobile'
|
|
12
|
+
//
|
|
13
|
+
// `build()` encodes the packet, so anything the spec does not allow is refused there, with an
|
|
14
|
+
// `AprsEncodeError` that says why.
|
|
15
|
+
|
|
16
|
+
import { encodeInformation, encodeAx25, encodeTnc2, encodeTnc2Bytes, wrapKiss, type PacketToEncode } from './encode/index.js';
|
|
17
|
+
import { symbolOf } from './symbols.js';
|
|
18
|
+
import type {
|
|
19
|
+
AprsData,
|
|
20
|
+
AprsSymbol,
|
|
21
|
+
AprsTimestamp,
|
|
22
|
+
CommentTelemetry,
|
|
23
|
+
CompressionType,
|
|
24
|
+
Dao,
|
|
25
|
+
MicEMessage,
|
|
26
|
+
Phg,
|
|
27
|
+
PositionedFields,
|
|
28
|
+
VoiceFrequency,
|
|
29
|
+
Weather,
|
|
30
|
+
} from './types.js';
|
|
31
|
+
|
|
32
|
+
/** The tocall used when none is given: the experimental `APZ` range. */
|
|
33
|
+
export const DEFAULT_DESTINATION = 'APZ001';
|
|
34
|
+
|
|
35
|
+
/** A packet the builder made: its parts, and the ways to send it. */
|
|
36
|
+
export interface BuiltPacket {
|
|
37
|
+
readonly source: string;
|
|
38
|
+
/** The destination (for Mic-E, the one the encoder computed). */
|
|
39
|
+
readonly destination: string;
|
|
40
|
+
readonly path: readonly string[];
|
|
41
|
+
readonly data: AprsData;
|
|
42
|
+
/** The information field bytes. */
|
|
43
|
+
readonly information: Uint8Array;
|
|
44
|
+
/** The TNC2 / APRS-IS line. */
|
|
45
|
+
toTnc2(): string;
|
|
46
|
+
/** The TNC2 / APRS-IS line as bytes. */
|
|
47
|
+
toTnc2Bytes(): Uint8Array;
|
|
48
|
+
/** The AX.25 UI frame (no flags, no FCS). */
|
|
49
|
+
toAx25(): Uint8Array;
|
|
50
|
+
/** The AX.25 frame wrapped for a KISS TNC. */
|
|
51
|
+
toKiss(port?: number): Uint8Array;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Header {
|
|
55
|
+
source: string;
|
|
56
|
+
destination?: string;
|
|
57
|
+
path: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function built(header: Header, data: AprsData): BuiltPacket {
|
|
61
|
+
const encoded = encodeInformation(data);
|
|
62
|
+
const destination = encoded.destination ?? header.destination ?? DEFAULT_DESTINATION;
|
|
63
|
+
const toEncode: PacketToEncode = { source: header.source, destination, path: header.path, data };
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
source: header.source,
|
|
66
|
+
destination,
|
|
67
|
+
path: Object.freeze([...header.path]),
|
|
68
|
+
data,
|
|
69
|
+
information: encoded.info,
|
|
70
|
+
toTnc2: () => encodeTnc2(toEncode),
|
|
71
|
+
toTnc2Bytes: () => encodeTnc2Bytes(toEncode),
|
|
72
|
+
toAx25: () => encodeAx25(toEncode),
|
|
73
|
+
toKiss: (port = 0) => wrapKiss(encodeAx25(toEncode), port),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function asSymbol(symbol: AprsSymbol | string): AprsSymbol {
|
|
78
|
+
return typeof symbol === 'string' ? symbolOf(symbol) : symbol;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A timestamp from a `Date`, in UTC: day, hour and minute (`dhm-zulu`, the recommended form),
|
|
83
|
+
* or hours, minutes and seconds (`hms`).
|
|
84
|
+
*/
|
|
85
|
+
export function timestampOf(date: Date, kind: 'dhm-zulu' | 'hms' = 'dhm-zulu'): AprsTimestamp {
|
|
86
|
+
return kind === 'hms'
|
|
87
|
+
? { kind: 'hms', hour: date.getUTCHours(), minute: date.getUTCMinutes(), second: date.getUTCSeconds() }
|
|
88
|
+
: { kind: 'dhm-zulu', day: date.getUTCDate(), hour: date.getUTCHours(), minute: date.getUTCMinutes() };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function asTimestamp(t: Date | AprsTimestamp, kind?: 'dhm-zulu' | 'hms'): AprsTimestamp {
|
|
92
|
+
return t instanceof Date ? timestampOf(t, kind) : t;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
|
96
|
+
|
|
97
|
+
/** Starts a packet: `Aprs.from('M0LTE-9')`. */
|
|
98
|
+
export const Aprs = {
|
|
99
|
+
/** A packet from `source`. */
|
|
100
|
+
from(source: string): PacketBuilder {
|
|
101
|
+
return new PacketBuilder({ source, path: [] });
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** The header, then the kind of packet. */
|
|
106
|
+
export class PacketBuilder {
|
|
107
|
+
constructor(private readonly header: Header) {}
|
|
108
|
+
|
|
109
|
+
/** The destination (tocall) identifying the sending software; ignored for Mic-E. */
|
|
110
|
+
to(destination: string): PacketBuilder {
|
|
111
|
+
return new PacketBuilder({ ...this.header, destination });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The digipeater path, e.g. `via('WIDE1-1', 'WIDE2-1')`. */
|
|
115
|
+
via(...path: string[]): PacketBuilder {
|
|
116
|
+
return new PacketBuilder({ ...this.header, path: [...this.header.path, ...path] });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A position report. */
|
|
120
|
+
position(latitude: number, longitude: number): PositionBuilder {
|
|
121
|
+
return new PositionBuilder(this.header, latitude, longitude);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A weather report at a position (the weather station symbol, unless you pick another `_`). */
|
|
125
|
+
weather(latitude: number, longitude: number, weather: Weather): PositionBuilder {
|
|
126
|
+
return new PositionBuilder(this.header, latitude, longitude).symbol('/_').weather(weather);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** An object: `timestamp` defaults to now (objects always carry one). */
|
|
130
|
+
object(name: string, latitude: number, longitude: number): ObjectBuilder {
|
|
131
|
+
return new ObjectBuilder(this.header, name, latitude, longitude);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** An item. */
|
|
135
|
+
item(name: string, latitude: number, longitude: number): ItemBuilder {
|
|
136
|
+
return new ItemBuilder(this.header, name, latitude, longitude);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A Mic-E report: its position and message type go in the destination address. */
|
|
140
|
+
micE(latitude: number, longitude: number): MicEBuilder {
|
|
141
|
+
return new MicEBuilder(this.header, latitude, longitude);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A message to `addressee`. */
|
|
145
|
+
message(addressee: string, text: string): MessageBuilder {
|
|
146
|
+
return new MessageBuilder(this.header, addressee, text);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** An acknowledgement of message `id` from `addressee`. */
|
|
150
|
+
ack(addressee: string, id: string): DataBuilder {
|
|
151
|
+
return new DataBuilder(this.header, { type: 'ack', addressee, ackedId: id });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A rejection of message `id` from `addressee`. */
|
|
155
|
+
reject(addressee: string, id: string): DataBuilder {
|
|
156
|
+
return new DataBuilder(this.header, { type: 'reject', addressee, rejectedId: id });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A bulletin (`id` a digit) or announcement (`id` a letter), optionally to a group. */
|
|
160
|
+
bulletin(id: string, text: string, group = ''): DataBuilder {
|
|
161
|
+
return new DataBuilder(this.header, { type: 'bulletin', addressee: `BLN${id}${group}`, text });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A status report. */
|
|
165
|
+
status(text?: string): StatusBuilder {
|
|
166
|
+
return new StatusBuilder(this.header, text);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A telemetry report: sequence, five analog values and eight bits (`'01101001'`, B1 first). */
|
|
170
|
+
telemetry(sequence: number | string, analog: readonly (number | null)[], bits: string): TelemetryBuilder {
|
|
171
|
+
const seq = typeof sequence === 'number' ? String(sequence).padStart(3, '0') : sequence;
|
|
172
|
+
return new TelemetryBuilder(this.header, seq, analog, bits);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Telemetry channel names (`PARM.`), sent to the station whose telemetry they describe. */
|
|
176
|
+
telemetryNames(addressee: string, names: readonly string[]): DataBuilder {
|
|
177
|
+
return new DataBuilder(this.header, { type: 'telemetry-names', addressee, names: [...names] });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Telemetry units and labels (`UNIT.`). */
|
|
181
|
+
telemetryUnits(addressee: string, units: readonly string[]): DataBuilder {
|
|
182
|
+
return new DataBuilder(this.header, { type: 'telemetry-units', addressee, units: [...units] });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Telemetry equation coefficients (`EQNS.`): a, b, c for each analog channel. */
|
|
186
|
+
telemetryCoefficients(addressee: string, coefficients: readonly number[]): DataBuilder {
|
|
187
|
+
return new DataBuilder(this.header, { type: 'telemetry-coefficients', addressee, coefficients: [...coefficients] });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Telemetry bit sense and project title (`BITS.`). */
|
|
191
|
+
telemetryBits(addressee: string, bits: string, project?: string): DataBuilder {
|
|
192
|
+
const data: Mutable<Extract<AprsData, { type: 'telemetry-bits' }>> = { type: 'telemetry-bits', addressee, bits };
|
|
193
|
+
if (project !== undefined) data.project = project;
|
|
194
|
+
return new DataBuilder(this.header, data);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A positionless weather report (not recommended: a weather report with a position is better). */
|
|
198
|
+
weatherReport(weather: Weather, at: Date = new Date()): DataBuilder {
|
|
199
|
+
const timestamp: AprsTimestamp = {
|
|
200
|
+
kind: 'mdhm',
|
|
201
|
+
month: at.getUTCMonth() + 1,
|
|
202
|
+
day: at.getUTCDate(),
|
|
203
|
+
hour: at.getUTCHours(),
|
|
204
|
+
minute: at.getUTCMinutes(),
|
|
205
|
+
};
|
|
206
|
+
return new DataBuilder(this.header, { type: 'weather', timestamp, weather });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Any data, built by hand. */
|
|
210
|
+
data(data: AprsData): DataBuilder {
|
|
211
|
+
return new DataBuilder(this.header, data);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Builds data that needs nothing more. */
|
|
216
|
+
export class DataBuilder {
|
|
217
|
+
constructor(
|
|
218
|
+
protected readonly header: Header,
|
|
219
|
+
protected readonly value: AprsData,
|
|
220
|
+
) {}
|
|
221
|
+
|
|
222
|
+
/** Encodes the packet; throws `AprsEncodeError` if the spec does not allow it. */
|
|
223
|
+
build(): BuiltPacket {
|
|
224
|
+
return built(this.header, this.value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Shorthand for `build().toTnc2()`. */
|
|
228
|
+
toTnc2(): string {
|
|
229
|
+
return this.build().toTnc2();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** What positions, objects, items and Mic-E reports share. */
|
|
234
|
+
abstract class PositionedBuilder<Self> {
|
|
235
|
+
protected fields: Mutable<PositionedFields>;
|
|
236
|
+
|
|
237
|
+
constructor(
|
|
238
|
+
protected readonly header: Header,
|
|
239
|
+
latitude: number,
|
|
240
|
+
longitude: number,
|
|
241
|
+
) {
|
|
242
|
+
this.fields = { latitude, longitude, symbol: { table: '/', code: '/' } };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
protected abstract self(): Self;
|
|
246
|
+
|
|
247
|
+
/** The display symbol: `Symbols.car`, or its two characters (`'/>'`). */
|
|
248
|
+
symbol(symbol: AprsSymbol | string): Self {
|
|
249
|
+
this.fields.symbol = asSymbol(symbol);
|
|
250
|
+
return this.self();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Course over the ground, degrees 1-360 (360 is north). */
|
|
254
|
+
course(degrees: number): Self {
|
|
255
|
+
this.fields.courseDegrees = degrees;
|
|
256
|
+
return this.self();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Speed, knots. */
|
|
260
|
+
speed(knots: number): Self {
|
|
261
|
+
this.fields.speedKnots = knots;
|
|
262
|
+
return this.self();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Altitude, feet. */
|
|
266
|
+
altitude(feet: number): Self {
|
|
267
|
+
this.fields.altitudeFeet = feet;
|
|
268
|
+
return this.self();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Altitude, metres (sent in feet, rounded). */
|
|
272
|
+
altitudeMetres(metres: number): Self {
|
|
273
|
+
this.fields.altitudeFeet = Math.round(metres / 0.3048);
|
|
274
|
+
return this.self();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Free text. */
|
|
278
|
+
comment(text: string): Self {
|
|
279
|
+
this.fields.comment = text;
|
|
280
|
+
return this.self();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** An APRS 1.2 voice frequency, e.g. `frequency(145.5, { tone: 'tone', toneValue: 77 })`. */
|
|
284
|
+
frequency(mhz: number, details: Omit<VoiceFrequency, 'mhz'> = {}): Self {
|
|
285
|
+
this.fields.frequency = { mhz, ...details };
|
|
286
|
+
return this.self();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Base-91 comment telemetry: a sequence and one to five analog values (0-8280). */
|
|
290
|
+
commentTelemetry(sequence: number, analog: readonly number[], digital?: number): Self {
|
|
291
|
+
const t: Mutable<CommentTelemetry> = { sequence, analog: [...analog] };
|
|
292
|
+
if (digital !== undefined) t.digital = digital;
|
|
293
|
+
this.fields.telemetry = t;
|
|
294
|
+
return this.self();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A `!DAO!` for about a foot of precision (base-91), with its datum (`W` is WGS84). */
|
|
298
|
+
dao(datum = 'W', precision: Dao['precision'] = 'base91'): Self {
|
|
299
|
+
this.fields.dao = { datum, precision };
|
|
300
|
+
return this.self();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Blanks 1-4 digits of the position (APRS12c ch. 6). */
|
|
304
|
+
ambiguity(digits: number): Self {
|
|
305
|
+
this.fields.ambiguity = digits;
|
|
306
|
+
return this.self();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** A position (`!`, `=`, `/`, `@`), or an object or item's position. */
|
|
311
|
+
abstract class ReportBuilder<Self> extends PositionedBuilder<Self> {
|
|
312
|
+
/** Power, antenna height, gain and directivity codes (APRS12c ch. 7). */
|
|
313
|
+
phg(power: number, height: number, gain: number, directivity = 0, beaconsPerHour?: number): Self {
|
|
314
|
+
const phg: Mutable<Phg> = { power, height, gain, directivity };
|
|
315
|
+
if (beaconsPerHour !== undefined) phg.beaconsPerHour = beaconsPerHour;
|
|
316
|
+
this.fields.phg = phg;
|
|
317
|
+
return this.self();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A pre-calculated radio range, miles. */
|
|
321
|
+
range(miles: number): Self {
|
|
322
|
+
this.fields.rangeMiles = miles;
|
|
323
|
+
return this.self();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Weather data (needs a weather station symbol, `_`). */
|
|
327
|
+
weather(weather: Weather): Self {
|
|
328
|
+
this.fields.weather = weather;
|
|
329
|
+
return this.self();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** The compressed format (APRS12c ch. 9), with its type byte if you want to set it. */
|
|
333
|
+
compressed(compression?: CompressionType): Self {
|
|
334
|
+
this.fields.compressed = true;
|
|
335
|
+
if (compression) this.fields.compression = compression;
|
|
336
|
+
return this.self();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** A position report. */
|
|
341
|
+
export class PositionBuilder extends ReportBuilder<PositionBuilder> {
|
|
342
|
+
private timestampValue?: AprsTimestamp;
|
|
343
|
+
private messagingValue = false;
|
|
344
|
+
|
|
345
|
+
protected self(): PositionBuilder {
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** When the position was fixed: a `Date` (UTC) or a timestamp. */
|
|
350
|
+
timestamp(at: Date | AprsTimestamp, kind?: 'dhm-zulu' | 'hms'): PositionBuilder {
|
|
351
|
+
this.timestampValue = asTimestamp(at, kind);
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** The sender can receive APRS messages. */
|
|
356
|
+
messaging(capable = true): PositionBuilder {
|
|
357
|
+
this.messagingValue = capable;
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
build(): BuiltPacket {
|
|
362
|
+
const data: Mutable<Extract<AprsData, { type: 'position' }>> = { type: 'position', ...this.fields };
|
|
363
|
+
if (this.timestampValue) data.timestamp = this.timestampValue;
|
|
364
|
+
if (this.messagingValue) data.messaging = true;
|
|
365
|
+
return built(this.header, data);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
toTnc2(): string {
|
|
369
|
+
return this.build().toTnc2();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** An object report. */
|
|
374
|
+
export class ObjectBuilder extends ReportBuilder<ObjectBuilder> {
|
|
375
|
+
private timestampValue: AprsTimestamp = timestampOf(new Date());
|
|
376
|
+
private killedValue = false;
|
|
377
|
+
|
|
378
|
+
constructor(
|
|
379
|
+
header: Header,
|
|
380
|
+
private readonly name: string,
|
|
381
|
+
latitude: number,
|
|
382
|
+
longitude: number,
|
|
383
|
+
) {
|
|
384
|
+
super(header, latitude, longitude);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
protected self(): ObjectBuilder {
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** When the object was at this position (defaults to now). */
|
|
392
|
+
timestamp(at: Date | AprsTimestamp, kind?: 'dhm-zulu' | 'hms'): ObjectBuilder {
|
|
393
|
+
this.timestampValue = asTimestamp(at, kind);
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Kills the object: receivers remove it. */
|
|
398
|
+
killed(killed = true): ObjectBuilder {
|
|
399
|
+
this.killedValue = killed;
|
|
400
|
+
return this;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
build(): BuiltPacket {
|
|
404
|
+
const data: Mutable<Extract<AprsData, { type: 'object' }>> = {
|
|
405
|
+
type: 'object',
|
|
406
|
+
name: this.name,
|
|
407
|
+
timestamp: this.timestampValue,
|
|
408
|
+
...this.fields,
|
|
409
|
+
};
|
|
410
|
+
if (this.killedValue) data.killed = true;
|
|
411
|
+
return built(this.header, data);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
toTnc2(): string {
|
|
415
|
+
return this.build().toTnc2();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** An item report. */
|
|
420
|
+
export class ItemBuilder extends ReportBuilder<ItemBuilder> {
|
|
421
|
+
private killedValue = false;
|
|
422
|
+
|
|
423
|
+
constructor(
|
|
424
|
+
header: Header,
|
|
425
|
+
private readonly name: string,
|
|
426
|
+
latitude: number,
|
|
427
|
+
longitude: number,
|
|
428
|
+
) {
|
|
429
|
+
super(header, latitude, longitude);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
protected self(): ItemBuilder {
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Kills the item: receivers remove it. */
|
|
437
|
+
killed(killed = true): ItemBuilder {
|
|
438
|
+
this.killedValue = killed;
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
build(): BuiltPacket {
|
|
443
|
+
const data: Mutable<Extract<AprsData, { type: 'item' }>> = { type: 'item', name: this.name, ...this.fields };
|
|
444
|
+
if (this.killedValue) data.killed = true;
|
|
445
|
+
return built(this.header, data);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
toTnc2(): string {
|
|
449
|
+
return this.build().toTnc2();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** A Mic-E report. */
|
|
454
|
+
export class MicEBuilder extends PositionedBuilder<MicEBuilder> {
|
|
455
|
+
private messageValue: MicEMessage = 'off-duty';
|
|
456
|
+
private typeCodeValue: string | undefined = '`';
|
|
457
|
+
private locatorValue?: string;
|
|
458
|
+
private oldValue = false;
|
|
459
|
+
|
|
460
|
+
protected self(): MicEBuilder {
|
|
461
|
+
return this;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** The Mic-E message: `off-duty` (the default), `en-route`, `in-service`, `returning`, ... */
|
|
465
|
+
micEMessage(message: MicEMessage): MicEBuilder {
|
|
466
|
+
this.messageValue = message;
|
|
467
|
+
return this;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Whether the sender can receive messages: the `` ` `` (yes, the default) or `'` type code. */
|
|
471
|
+
messaging(capable = true): MicEBuilder {
|
|
472
|
+
this.typeCodeValue = capable ? '`' : "'";
|
|
473
|
+
return this;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** A Maidenhead locator in the status text. */
|
|
477
|
+
locator(locator: string): MicEBuilder {
|
|
478
|
+
this.locatorValue = locator.toUpperCase();
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Marks the position as old (not current) GPS data. */
|
|
483
|
+
old(old = true): MicEBuilder {
|
|
484
|
+
this.oldValue = old;
|
|
485
|
+
return this;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
build(): BuiltPacket {
|
|
489
|
+
const data: Mutable<Extract<AprsData, { type: 'mic-e' }>> = { type: 'mic-e', micEMessage: this.messageValue, ...this.fields };
|
|
490
|
+
if (this.typeCodeValue !== undefined) data.typeCode = this.typeCodeValue;
|
|
491
|
+
if (this.locatorValue !== undefined) data.locator = this.locatorValue;
|
|
492
|
+
if (this.oldValue) data.oldData = true;
|
|
493
|
+
return built(this.header, data);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
toTnc2(): string {
|
|
497
|
+
return this.build().toTnc2();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** A message. */
|
|
502
|
+
export class MessageBuilder {
|
|
503
|
+
private idValue?: string;
|
|
504
|
+
private replyAckValue?: string;
|
|
505
|
+
|
|
506
|
+
constructor(
|
|
507
|
+
private readonly header: Header,
|
|
508
|
+
private readonly addressee: string,
|
|
509
|
+
private readonly text: string,
|
|
510
|
+
) {}
|
|
511
|
+
|
|
512
|
+
/** A message ID (1-5 letters or digits), so the addressee acknowledges it. */
|
|
513
|
+
id(id: string | number): MessageBuilder {
|
|
514
|
+
this.idValue = String(id);
|
|
515
|
+
return this;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** A reply-ack: the ID of the other station's message this also acknowledges; `''` for none. */
|
|
519
|
+
replyAck(id = ''): MessageBuilder {
|
|
520
|
+
this.replyAckValue = id;
|
|
521
|
+
return this;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
build(): BuiltPacket {
|
|
525
|
+
const data: Mutable<Extract<AprsData, { type: 'message' }>> = {
|
|
526
|
+
type: 'message',
|
|
527
|
+
addressee: this.addressee,
|
|
528
|
+
text: this.text,
|
|
529
|
+
};
|
|
530
|
+
if (this.idValue !== undefined) data.messageId = this.idValue;
|
|
531
|
+
if (this.replyAckValue !== undefined) data.replyAck = this.replyAckValue;
|
|
532
|
+
return built(this.header, data);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
toTnc2(): string {
|
|
536
|
+
return this.build().toTnc2();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** A status report. */
|
|
541
|
+
export class StatusBuilder {
|
|
542
|
+
private timestampValue?: AprsTimestamp;
|
|
543
|
+
private locatorValue?: { locator: string; symbol: AprsSymbol };
|
|
544
|
+
private beamValue?: { headingCode: string; powerCode: string };
|
|
545
|
+
|
|
546
|
+
constructor(
|
|
547
|
+
private readonly header: Header,
|
|
548
|
+
private readonly text?: string,
|
|
549
|
+
) {}
|
|
550
|
+
|
|
551
|
+
/** When (UTC day, hour and minute). A status with a locator has no timestamp. */
|
|
552
|
+
timestamp(at: Date | AprsTimestamp): StatusBuilder {
|
|
553
|
+
this.timestampValue = asTimestamp(at, 'dhm-zulu');
|
|
554
|
+
return this;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** A Maidenhead locator and a symbol, in place of a timestamp. */
|
|
558
|
+
locator(locator: string, symbol: AprsSymbol | string): StatusBuilder {
|
|
559
|
+
this.locatorValue = { locator: locator.toUpperCase(), symbol: asSymbol(symbol) };
|
|
560
|
+
return this;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Meteor scatter beam heading and power codes (`^B7`). */
|
|
564
|
+
beam(headingCode: string, powerCode: string): StatusBuilder {
|
|
565
|
+
this.beamValue = { headingCode, powerCode };
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
build(): BuiltPacket {
|
|
570
|
+
const data: Mutable<Extract<AprsData, { type: 'status' }>> = { type: 'status' };
|
|
571
|
+
if (this.timestampValue) data.timestamp = this.timestampValue;
|
|
572
|
+
if (this.locatorValue) {
|
|
573
|
+
data.locator = this.locatorValue.locator;
|
|
574
|
+
data.symbol = this.locatorValue.symbol;
|
|
575
|
+
}
|
|
576
|
+
if (this.beamValue) data.beam = this.beamValue;
|
|
577
|
+
if (this.text !== undefined && this.text.length > 0) data.text = this.text;
|
|
578
|
+
return built(this.header, data);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
toTnc2(): string {
|
|
582
|
+
return this.build().toTnc2();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** A telemetry report. */
|
|
587
|
+
export class TelemetryBuilder {
|
|
588
|
+
private commentValue?: string;
|
|
589
|
+
|
|
590
|
+
constructor(
|
|
591
|
+
private readonly header: Header,
|
|
592
|
+
private readonly sequence: string,
|
|
593
|
+
private readonly analog: readonly (number | null)[],
|
|
594
|
+
private readonly bits: string,
|
|
595
|
+
) {}
|
|
596
|
+
|
|
597
|
+
/** Free text after the bits. */
|
|
598
|
+
comment(text: string): TelemetryBuilder {
|
|
599
|
+
this.commentValue = text;
|
|
600
|
+
return this;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
build(): BuiltPacket {
|
|
604
|
+
const data: Mutable<Extract<AprsData, { type: 'telemetry' }>> = {
|
|
605
|
+
type: 'telemetry',
|
|
606
|
+
sequence: this.sequence,
|
|
607
|
+
analog: [...this.analog],
|
|
608
|
+
bits: this.bits,
|
|
609
|
+
};
|
|
610
|
+
if (this.commentValue !== undefined && this.commentValue.length > 0) data.comment = this.commentValue;
|
|
611
|
+
return built(this.header, data);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
toTnc2(): string {
|
|
615
|
+
return this.build().toTnc2();
|
|
616
|
+
}
|
|
617
|
+
}
|