@aeroswarm/ingest 0.1.1 → 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 +10 -0
- package/dist/feed/asterix.d.ts.map +1 -1
- package/dist/index.js +53 -9
- 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). */
|
|
@@ -35,6 +37,9 @@ export interface AsterixFeedConfig {
|
|
|
35
37
|
multicastIface?: string;
|
|
36
38
|
/** Radar site position for CAT020/048 polar→WGS84 resolution */
|
|
37
39
|
radarSite?: RadarSite;
|
|
40
|
+
/** Optional per-sensor override keyed "sac:sic" — a single stream can carry
|
|
41
|
+
* plots from several radars, each referenced to its own origin */
|
|
42
|
+
radarSites?: Record<string, RadarSite>;
|
|
38
43
|
}
|
|
39
44
|
/** Direct ASTERIX feed source: UDP datagrams (unicast or multicast) or a TCP
|
|
40
45
|
* client stream ([CAT][LEN]… blocks), decoded to FusedTrackEvents. Emits
|
|
@@ -54,6 +59,7 @@ export declare class AsterixFeedSource implements IngestSource {
|
|
|
54
59
|
private emit;
|
|
55
60
|
constructor(cfg?: AsterixFeedConfig);
|
|
56
61
|
private sourceId;
|
|
62
|
+
private siteFor;
|
|
57
63
|
private meta;
|
|
58
64
|
private makeFused;
|
|
59
65
|
/** CAT23 report → source health event. NOGO means the ground station itself
|
|
@@ -61,6 +67,10 @@ export declare class AsterixFeedSource implements IngestSource {
|
|
|
61
67
|
private handleGsStatus;
|
|
62
68
|
/** Decode one ASTERIX datagram/block and emit the resulting tracks. */
|
|
63
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;
|
|
64
74
|
private resetState;
|
|
65
75
|
start(onEvent: IngestEventHandler): void;
|
|
66
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;
|
|
@@ -44,6 +45,11 @@ var AsterixFeedSource = class {
|
|
|
44
45
|
sourceId() {
|
|
45
46
|
return this.cfg.sourceId ?? "ingest";
|
|
46
47
|
}
|
|
48
|
+
siteFor(f) {
|
|
49
|
+
const { sac, sic } = f.dataSource ?? {};
|
|
50
|
+
const keyed = sac != null && sic != null ? this.cfg.radarSites?.[`${sac}:${sic}`] : void 0;
|
|
51
|
+
return keyed ?? this.cfg.radarSite ?? null;
|
|
52
|
+
}
|
|
47
53
|
meta() {
|
|
48
54
|
return {
|
|
49
55
|
sourceType: this.stats.transport === "tcp" ? "ASTERIX_TCP" : "ASTERIX_UDP",
|
|
@@ -116,6 +122,7 @@ var AsterixFeedSource = class {
|
|
|
116
122
|
this.cfg.forcedEditions && Object.keys(this.cfg.forcedEditions).length > 0 ? this.cfg.forcedEditions : void 0
|
|
117
123
|
);
|
|
118
124
|
if (records.length === 0) {
|
|
125
|
+
if (this.processAftnDatagram(buf)) return;
|
|
119
126
|
this.stats.packetsUndecoded++;
|
|
120
127
|
if (this.stats.packetsUndecoded === 1 || this.stats.packetsUndecoded % 1e3 === 0) {
|
|
121
128
|
log.warn(
|
|
@@ -136,6 +143,10 @@ var AsterixFeedSource = class {
|
|
|
136
143
|
continue;
|
|
137
144
|
}
|
|
138
145
|
const f = rec.fields;
|
|
146
|
+
if (f.testTarget || f.fixedTransponder) {
|
|
147
|
+
this.stats.packetsSkipped++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
139
150
|
const isSurface = rec.category === 10 || rec.category === 11;
|
|
140
151
|
let icao24 = f.icao24;
|
|
141
152
|
if (!icao24 && f.trackNumber !== void 0) {
|
|
@@ -146,7 +157,7 @@ var AsterixFeedSource = class {
|
|
|
146
157
|
this.stats.packetsSkipped++;
|
|
147
158
|
continue;
|
|
148
159
|
}
|
|
149
|
-
const pos = resolveAsterixPosition(f, this.
|
|
160
|
+
const pos = resolveAsterixPosition(f, this.siteFor(f));
|
|
150
161
|
if (!pos) {
|
|
151
162
|
this.stats.packetsSkipped++;
|
|
152
163
|
continue;
|
|
@@ -225,8 +236,8 @@ var AsterixFeedSource = class {
|
|
|
225
236
|
);
|
|
226
237
|
if (squawk) fused.identity = { ...fused.identity, squawk };
|
|
227
238
|
if (this.cfg.airportId) fused.airportId = this.cfg.airportId;
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
const targetType = f.targetType ?? (isSurface ? f.icao24 ? "aircraft" : "unknown" : void 0);
|
|
240
|
+
if (targetType !== void 0 || f.vehicleFleet !== void 0) {
|
|
230
241
|
fused.identity = {
|
|
231
242
|
...fused.identity,
|
|
232
243
|
targetType,
|
|
@@ -237,10 +248,42 @@ var AsterixFeedSource = class {
|
|
|
237
248
|
this.stats.packetsForwarded++;
|
|
238
249
|
}
|
|
239
250
|
} catch (err) {
|
|
251
|
+
if (this.processAftnDatagram(buf)) {
|
|
252
|
+
this.stats.lastError = null;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
240
255
|
this.stats.packetsRejected++;
|
|
241
256
|
this.stats.lastError = err instanceof Error ? err.message : String(err);
|
|
242
257
|
}
|
|
243
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
|
+
}
|
|
244
287
|
resetState(patch) {
|
|
245
288
|
Object.assign(this.stats, {
|
|
246
289
|
running: true,
|
|
@@ -255,6 +298,7 @@ var AsterixFeedSource = class {
|
|
|
255
298
|
lastDecoded: null,
|
|
256
299
|
gsStatus: null,
|
|
257
300
|
lastError: null,
|
|
301
|
+
aftnMessages: 0,
|
|
258
302
|
...patch
|
|
259
303
|
});
|
|
260
304
|
this.lastHealth = { at: 0, packets: 0 };
|
|
@@ -405,7 +449,7 @@ var AsterixFeedSource = class {
|
|
|
405
449
|
// src/feed/aftn.ts
|
|
406
450
|
import { createSocket as createSocket2 } from "dgram";
|
|
407
451
|
import { connect as tcpConnect2 } from "net";
|
|
408
|
-
import { parseAftn, splitAftnStream } from "@aeroswarm/codec";
|
|
452
|
+
import { parseAftn as parseAftn2, splitAftnStream as splitAftnStream2 } from "@aeroswarm/codec";
|
|
409
453
|
import { createLogger as createLogger2, createNanoid as createNanoid2 } from "@aeroswarm/core";
|
|
410
454
|
var log2 = createLogger2("ingest:aftn");
|
|
411
455
|
var TCP_RECONNECT_MS2 = 5e3;
|
|
@@ -458,7 +502,7 @@ var AftnFeedSource = class {
|
|
|
458
502
|
processMessage(raw) {
|
|
459
503
|
this.stats.packetsReceived++;
|
|
460
504
|
const now = Date.now();
|
|
461
|
-
const parsed =
|
|
505
|
+
const parsed = parseAftn2(raw, { now });
|
|
462
506
|
if (!parsed) {
|
|
463
507
|
this.stats.packetsUndecoded++;
|
|
464
508
|
if (this.stats.packetsUndecoded === 1 || this.stats.packetsUndecoded % 1e3 === 0) {
|
|
@@ -494,7 +538,7 @@ var AftnFeedSource = class {
|
|
|
494
538
|
/** A UDP datagram normally holds one message; tolerate several back to back. */
|
|
495
539
|
processDatagram(buf) {
|
|
496
540
|
const text = buf.toString("latin1");
|
|
497
|
-
const { messages, rest } =
|
|
541
|
+
const { messages, rest } = splitAftnStream2(text);
|
|
498
542
|
const all = messages.length > 0 ? [...messages, ...rest.includes("(") ? [rest] : []] : [text];
|
|
499
543
|
for (const m of all) this.processMessage(m);
|
|
500
544
|
}
|
|
@@ -544,7 +588,7 @@ var AftnFeedSource = class {
|
|
|
544
588
|
log2.info({ host, port, airport: this.cfg.airportId ?? "unscoped" }, "aftn TCP feed connected");
|
|
545
589
|
});
|
|
546
590
|
sock.on("data", (chunk) => {
|
|
547
|
-
const { messages, rest } =
|
|
591
|
+
const { messages, rest } = splitAftnStream2(this.tcpBuffer + chunk.toString("latin1"));
|
|
548
592
|
this.tcpBuffer = rest;
|
|
549
593
|
for (const m of messages) this.processMessage(m);
|
|
550
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",
|