@aeroswarm/ingest 0.1.2 → 0.1.4
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 +6 -0
- package/dist/feed/asterix.d.ts.map +1 -1
- package/dist/index.js +43 -8
- 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). */
|
|
@@ -65,6 +67,10 @@ export declare class AsterixFeedSource implements IngestSource {
|
|
|
65
67
|
private handleGsStatus;
|
|
66
68
|
/** Decode one ASTERIX datagram/block and emit the resulting tracks. */
|
|
67
69
|
processDatagram(buf: Buffer): void;
|
|
70
|
+
/** A datagram that decodes to no ASTERIX record may be AFTN/ATS text —
|
|
71
|
+
* some sites send both over one forwarded UDP port. Emits a flightplan
|
|
72
|
+
* event per ATS message found; returns true when at least one emitted. */
|
|
73
|
+
private processAftnDatagram;
|
|
68
74
|
private resetState;
|
|
69
75
|
start(onEvent: IngestEventHandler): void;
|
|
70
76
|
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":"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;IACrI,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;gDACgD;AAChD,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;uEACmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACxC;AAID;;;8DAG8D;AAC9D,qBAAa,iBAAkB,YAAW,YAAY;IAyBxC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAxBhC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAO9B;IAEF,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAKX;IACL,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,IAAI,CAAgC;gBAEf,GAAG,GAAE,iBAA+B;IAEjE,OAAO,CAAC,QAAQ;IAEhB,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,IAAI;IASZ,OAAO,CAAC,SAAS;IAoBjB;wDACoD;IACpD,OAAO,CAAC,cAAc;IAqCtB,uEAAuE;IACvE,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IA4JlC;;+EAE2E;IAC3E,OAAO,CAAC,mBAAmB;IA0B3B,OAAO,CAAC,UAAU;IAmBlB,KAAK,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAUxC,OAAO,CAAC,QAAQ;IAyBhB,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,UAAU;YAgDJ,YAAY;IAM1B,OAAO,CAAC,WAAW;IASnB;8CAC0C;IAC1C,OAAO,CAAC,aAAa;IASf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAU5B"}
|
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(
|
|
@@ -234,8 +236,8 @@ 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
|
-
|
|
238
|
-
|
|
239
|
+
const targetType = f.targetType ?? (isSurface ? f.icao24 ? "aircraft" : "unknown" : void 0);
|
|
240
|
+
if (targetType !== void 0 || f.vehicleFleet !== void 0) {
|
|
239
241
|
fused.identity = {
|
|
240
242
|
...fused.identity,
|
|
241
243
|
targetType,
|
|
@@ -246,10 +248,42 @@ var AsterixFeedSource = class {
|
|
|
246
248
|
this.stats.packetsForwarded++;
|
|
247
249
|
}
|
|
248
250
|
} catch (err) {
|
|
251
|
+
if (this.processAftnDatagram(buf)) {
|
|
252
|
+
this.stats.lastError = null;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
249
255
|
this.stats.packetsRejected++;
|
|
250
256
|
this.stats.lastError = err instanceof Error ? err.message : String(err);
|
|
251
257
|
}
|
|
252
258
|
}
|
|
259
|
+
/** A datagram that decodes to no ASTERIX record may be AFTN/ATS text —
|
|
260
|
+
* some sites send both over one forwarded UDP port. Emits a flightplan
|
|
261
|
+
* event per ATS message found; returns true when at least one emitted. */
|
|
262
|
+
processAftnDatagram(buf) {
|
|
263
|
+
const text = buf.toString("latin1");
|
|
264
|
+
const { messages, rest } = splitAftnStream(text);
|
|
265
|
+
const all = messages.length > 0 ? [...messages, ...rest.includes("(") ? [rest] : []] : [text];
|
|
266
|
+
let emitted = false;
|
|
267
|
+
for (const m of all) {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const parsed = parseAftn(m, { now });
|
|
270
|
+
if (!parsed?.callsign) continue;
|
|
271
|
+
this.emit({
|
|
272
|
+
kind: "flightplan",
|
|
273
|
+
event: {
|
|
274
|
+
messageId: createNanoid(12),
|
|
275
|
+
sourceId: this.sourceId(),
|
|
276
|
+
...this.cfg.feedId && { feedId: this.cfg.feedId },
|
|
277
|
+
...this.cfg.airportId && { airportId: this.cfg.airportId },
|
|
278
|
+
receivedAt: now,
|
|
279
|
+
message: parsed
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
this.stats.aftnMessages++;
|
|
283
|
+
emitted = true;
|
|
284
|
+
}
|
|
285
|
+
return emitted;
|
|
286
|
+
}
|
|
253
287
|
resetState(patch) {
|
|
254
288
|
Object.assign(this.stats, {
|
|
255
289
|
running: true,
|
|
@@ -264,6 +298,7 @@ var AsterixFeedSource = class {
|
|
|
264
298
|
lastDecoded: null,
|
|
265
299
|
gsStatus: null,
|
|
266
300
|
lastError: null,
|
|
301
|
+
aftnMessages: 0,
|
|
267
302
|
...patch
|
|
268
303
|
});
|
|
269
304
|
this.lastHealth = { at: 0, packets: 0 };
|
|
@@ -414,7 +449,7 @@ var AsterixFeedSource = class {
|
|
|
414
449
|
// src/feed/aftn.ts
|
|
415
450
|
import { createSocket as createSocket2 } from "dgram";
|
|
416
451
|
import { connect as tcpConnect2 } from "net";
|
|
417
|
-
import { parseAftn, splitAftnStream } from "@aeroswarm/codec";
|
|
452
|
+
import { parseAftn as parseAftn2, splitAftnStream as splitAftnStream2 } from "@aeroswarm/codec";
|
|
418
453
|
import { createLogger as createLogger2, createNanoid as createNanoid2 } from "@aeroswarm/core";
|
|
419
454
|
var log2 = createLogger2("ingest:aftn");
|
|
420
455
|
var TCP_RECONNECT_MS2 = 5e3;
|
|
@@ -467,7 +502,7 @@ var AftnFeedSource = class {
|
|
|
467
502
|
processMessage(raw) {
|
|
468
503
|
this.stats.packetsReceived++;
|
|
469
504
|
const now = Date.now();
|
|
470
|
-
const parsed =
|
|
505
|
+
const parsed = parseAftn2(raw, { now });
|
|
471
506
|
if (!parsed) {
|
|
472
507
|
this.stats.packetsUndecoded++;
|
|
473
508
|
if (this.stats.packetsUndecoded === 1 || this.stats.packetsUndecoded % 1e3 === 0) {
|
|
@@ -503,7 +538,7 @@ var AftnFeedSource = class {
|
|
|
503
538
|
/** A UDP datagram normally holds one message; tolerate several back to back. */
|
|
504
539
|
processDatagram(buf) {
|
|
505
540
|
const text = buf.toString("latin1");
|
|
506
|
-
const { messages, rest } =
|
|
541
|
+
const { messages, rest } = splitAftnStream2(text);
|
|
507
542
|
const all = messages.length > 0 ? [...messages, ...rest.includes("(") ? [rest] : []] : [text];
|
|
508
543
|
for (const m of all) this.processMessage(m);
|
|
509
544
|
}
|
|
@@ -553,7 +588,7 @@ var AftnFeedSource = class {
|
|
|
553
588
|
log2.info({ host, port, airport: this.cfg.airportId ?? "unscoped" }, "aftn TCP feed connected");
|
|
554
589
|
});
|
|
555
590
|
sock.on("data", (chunk) => {
|
|
556
|
-
const { messages, rest } =
|
|
591
|
+
const { messages, rest } = splitAftnStream2(this.tcpBuffer + chunk.toString("latin1"));
|
|
557
592
|
this.tcpBuffer = rest;
|
|
558
593
|
for (const m of messages) this.processMessage(m);
|
|
559
594
|
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.4",
|
|
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",
|