@aeroswarm/ingest 0.1.2 → 0.1.5
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/dist/feed/asterix.d.ts +8 -0
- package/dist/feed/asterix.d.ts.map +1 -1
- package/dist/index.js +46 -10
- package/package.json +1 -1
package/dist/feed/asterix.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface AsterixFeedStats extends IngestSourceStats {
|
|
|
17
17
|
operationalRangeNm?: number;
|
|
18
18
|
at: number;
|
|
19
19
|
} | null;
|
|
20
|
+
/** AFTN messages recovered from non-ASTERIX datagrams on a mixed stream */
|
|
21
|
+
aftnMessages: number;
|
|
20
22
|
}
|
|
21
23
|
/** Per-feed configuration — every field explicit (no env fallbacks inside the
|
|
22
24
|
* package; the host process supplies them). */
|
|
@@ -26,6 +28,8 @@ export interface AsterixFeedConfig {
|
|
|
26
28
|
transport?: "udp" | "tcp";
|
|
27
29
|
/** TCP only: remote host to connect out to */
|
|
28
30
|
host?: string;
|
|
31
|
+
/** UDP only: local address to bind (default "0.0.0.0") */
|
|
32
|
+
bindHost?: string;
|
|
29
33
|
airportId?: string;
|
|
30
34
|
sourceId?: string;
|
|
31
35
|
feedId?: string;
|
|
@@ -65,6 +69,10 @@ export declare class AsterixFeedSource implements IngestSource {
|
|
|
65
69
|
private handleGsStatus;
|
|
66
70
|
/** Decode one ASTERIX datagram/block and emit the resulting tracks. */
|
|
67
71
|
processDatagram(buf: Buffer): void;
|
|
72
|
+
/** A datagram that decodes to no ASTERIX record may be AFTN/ATS text —
|
|
73
|
+
* some sites send both over one forwarded UDP port. Emits a flightplan
|
|
74
|
+
* event per ATS message found; returns true when at least one emitted. */
|
|
75
|
+
private processAftnDatagram;
|
|
68
76
|
private resetState;
|
|
69
77
|
start(onEvent: IngestEventHandler): void;
|
|
70
78
|
private startUdp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asterix.d.ts","sourceRoot":"","sources":["../../src/feed/asterix.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGjE,OAAO,KAAK,EAAe,kBAAkB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAgBpG,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD,WAAW,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnH,gDAAgD;IAChD,QAAQ,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"asterix.d.ts","sourceRoot":"","sources":["../../src/feed/asterix.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/feed/asterix.ts
|
|
2
2
|
import { createSocket } from "dgram";
|
|
3
3
|
import { connect as tcpConnect } from "net";
|
|
4
|
-
import { parseAsterix, resolveAsterixPosition, resolveAsterixAltitudeFt } from "@aeroswarm/codec";
|
|
4
|
+
import { parseAsterix, parseAftn, splitAftnStream, resolveAsterixPosition, resolveAsterixAltitudeFt } from "@aeroswarm/codec";
|
|
5
5
|
import { createLogger, createNanoid } from "@aeroswarm/core";
|
|
6
6
|
var log = createLogger("ingest:asterix");
|
|
7
7
|
var RAW_CAT21_TOPIC = "aero.raw.asterix.cat021.v1";
|
|
@@ -29,7 +29,8 @@ var AsterixFeedSource = class {
|
|
|
29
29
|
packetsUndecoded: 0,
|
|
30
30
|
lastDecoded: null,
|
|
31
31
|
gsStatus: null,
|
|
32
|
-
lastError: null
|
|
32
|
+
lastError: null,
|
|
33
|
+
aftnMessages: 0
|
|
33
34
|
};
|
|
34
35
|
lastHealth = { at: 0, packets: 0 };
|
|
35
36
|
socket = null;
|
|
@@ -121,6 +122,7 @@ var AsterixFeedSource = class {
|
|
|
121
122
|
this.cfg.forcedEditions && Object.keys(this.cfg.forcedEditions).length > 0 ? this.cfg.forcedEditions : void 0
|
|
122
123
|
);
|
|
123
124
|
if (records.length === 0) {
|
|
125
|
+
if (this.processAftnDatagram(buf)) return;
|
|
124
126
|
this.stats.packetsUndecoded++;
|
|
125
127
|
if (this.stats.packetsUndecoded === 1 || this.stats.packetsUndecoded % 1e3 === 0) {
|
|
126
128
|
log.warn(
|
|
@@ -141,7 +143,7 @@ var AsterixFeedSource = class {
|
|
|
141
143
|
continue;
|
|
142
144
|
}
|
|
143
145
|
const f = rec.fields;
|
|
144
|
-
if (f.testTarget || f.fixedTransponder) {
|
|
146
|
+
if (f.testTarget || f.fixedTransponder || f.simulated) {
|
|
145
147
|
this.stats.packetsSkipped++;
|
|
146
148
|
continue;
|
|
147
149
|
}
|
|
@@ -234,8 +236,9 @@ var AsterixFeedSource = class {
|
|
|
234
236
|
);
|
|
235
237
|
if (squawk) fused.identity = { ...fused.identity, squawk };
|
|
236
238
|
if (this.cfg.airportId) fused.airportId = this.cfg.airportId;
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
+
if (fused.position) fused.position.onGround = isSurface;
|
|
240
|
+
const targetType = f.targetType ?? (isSurface ? f.icao24 ? "aircraft" : "unknown" : void 0);
|
|
241
|
+
if (targetType !== void 0 || f.vehicleFleet !== void 0) {
|
|
239
242
|
fused.identity = {
|
|
240
243
|
...fused.identity,
|
|
241
244
|
targetType,
|
|
@@ -246,10 +249,42 @@ var AsterixFeedSource = class {
|
|
|
246
249
|
this.stats.packetsForwarded++;
|
|
247
250
|
}
|
|
248
251
|
} catch (err) {
|
|
252
|
+
if (this.processAftnDatagram(buf)) {
|
|
253
|
+
this.stats.lastError = null;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
249
256
|
this.stats.packetsRejected++;
|
|
250
257
|
this.stats.lastError = err instanceof Error ? err.message : String(err);
|
|
251
258
|
}
|
|
252
259
|
}
|
|
260
|
+
/** A datagram that decodes to no ASTERIX record may be AFTN/ATS text —
|
|
261
|
+
* some sites send both over one forwarded UDP port. Emits a flightplan
|
|
262
|
+
* event per ATS message found; returns true when at least one emitted. */
|
|
263
|
+
processAftnDatagram(buf) {
|
|
264
|
+
const text = buf.toString("latin1");
|
|
265
|
+
const { messages, rest } = splitAftnStream(text);
|
|
266
|
+
const all = messages.length > 0 ? [...messages, ...rest.includes("(") ? [rest] : []] : [text];
|
|
267
|
+
let emitted = false;
|
|
268
|
+
for (const m of all) {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const parsed = parseAftn(m, { now });
|
|
271
|
+
if (!parsed?.callsign) continue;
|
|
272
|
+
this.emit({
|
|
273
|
+
kind: "flightplan",
|
|
274
|
+
event: {
|
|
275
|
+
messageId: createNanoid(12),
|
|
276
|
+
sourceId: this.sourceId(),
|
|
277
|
+
...this.cfg.feedId && { feedId: this.cfg.feedId },
|
|
278
|
+
...this.cfg.airportId && { airportId: this.cfg.airportId },
|
|
279
|
+
receivedAt: now,
|
|
280
|
+
message: parsed
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
this.stats.aftnMessages++;
|
|
284
|
+
emitted = true;
|
|
285
|
+
}
|
|
286
|
+
return emitted;
|
|
287
|
+
}
|
|
253
288
|
resetState(patch) {
|
|
254
289
|
Object.assign(this.stats, {
|
|
255
290
|
running: true,
|
|
@@ -264,6 +299,7 @@ var AsterixFeedSource = class {
|
|
|
264
299
|
lastDecoded: null,
|
|
265
300
|
gsStatus: null,
|
|
266
301
|
lastError: null,
|
|
302
|
+
aftnMessages: 0,
|
|
267
303
|
...patch
|
|
268
304
|
});
|
|
269
305
|
this.lastHealth = { at: 0, packets: 0 };
|
|
@@ -291,7 +327,7 @@ var AsterixFeedSource = class {
|
|
|
291
327
|
this.stats.lastError = err.message;
|
|
292
328
|
log.warn({ err: err.message }, "asterix UDP socket error");
|
|
293
329
|
});
|
|
294
|
-
this.socket.bind(port, "0.0.0.0", () => {
|
|
330
|
+
this.socket.bind(port, this.cfg.bindHost ?? "0.0.0.0", () => {
|
|
295
331
|
if (this.cfg.multicastGroup) {
|
|
296
332
|
try {
|
|
297
333
|
this.socket.addMembership(this.cfg.multicastGroup, this.cfg.multicastIface || void 0);
|
|
@@ -414,7 +450,7 @@ var AsterixFeedSource = class {
|
|
|
414
450
|
// src/feed/aftn.ts
|
|
415
451
|
import { createSocket as createSocket2 } from "dgram";
|
|
416
452
|
import { connect as tcpConnect2 } from "net";
|
|
417
|
-
import { parseAftn, splitAftnStream } from "@aeroswarm/codec";
|
|
453
|
+
import { parseAftn as parseAftn2, splitAftnStream as splitAftnStream2 } from "@aeroswarm/codec";
|
|
418
454
|
import { createLogger as createLogger2, createNanoid as createNanoid2 } from "@aeroswarm/core";
|
|
419
455
|
var log2 = createLogger2("ingest:aftn");
|
|
420
456
|
var TCP_RECONNECT_MS2 = 5e3;
|
|
@@ -467,7 +503,7 @@ var AftnFeedSource = class {
|
|
|
467
503
|
processMessage(raw) {
|
|
468
504
|
this.stats.packetsReceived++;
|
|
469
505
|
const now = Date.now();
|
|
470
|
-
const parsed =
|
|
506
|
+
const parsed = parseAftn2(raw, { now });
|
|
471
507
|
if (!parsed) {
|
|
472
508
|
this.stats.packetsUndecoded++;
|
|
473
509
|
if (this.stats.packetsUndecoded === 1 || this.stats.packetsUndecoded % 1e3 === 0) {
|
|
@@ -503,7 +539,7 @@ var AftnFeedSource = class {
|
|
|
503
539
|
/** A UDP datagram normally holds one message; tolerate several back to back. */
|
|
504
540
|
processDatagram(buf) {
|
|
505
541
|
const text = buf.toString("latin1");
|
|
506
|
-
const { messages, rest } =
|
|
542
|
+
const { messages, rest } = splitAftnStream2(text);
|
|
507
543
|
const all = messages.length > 0 ? [...messages, ...rest.includes("(") ? [rest] : []] : [text];
|
|
508
544
|
for (const m of all) this.processMessage(m);
|
|
509
545
|
}
|
|
@@ -553,7 +589,7 @@ var AftnFeedSource = class {
|
|
|
553
589
|
log2.info({ host, port, airport: this.cfg.airportId ?? "unscoped" }, "aftn TCP feed connected");
|
|
554
590
|
});
|
|
555
591
|
sock.on("data", (chunk) => {
|
|
556
|
-
const { messages, rest } =
|
|
592
|
+
const { messages, rest } = splitAftnStream2(this.tcpBuffer + chunk.toString("latin1"));
|
|
557
593
|
this.tcpBuffer = rest;
|
|
558
594
|
for (const m of messages) this.processMessage(m);
|
|
559
595
|
if (this.tcpBuffer.length > TCP_MAX_BUFFER2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aeroswarm/ingest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Switchable surveillance ingest sources — direct ASTERIX/AFTN feeds (UDP/TCP) or AeroSwarm relays (Kafka/WS), one event stream",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|