@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.
@@ -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;CACtI;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;CACvB;AAID;;;8DAG8D;AAC9D,qBAAa,iBAAkB,YAAW,YAAY;IAwBxC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAvBhC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAM9B;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,IAAI;IASZ,OAAO,CAAC,SAAS;IAoBjB;wDACoD;IACpD,OAAO,CAAC,cAAc;IAqCtB,uEAAuE;IACvE,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IA6IlC,OAAO,CAAC,UAAU;IAkBlB,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"}
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.cfg.radarSite ?? null);
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
- if (isSurface) {
229
- const targetType = f.targetType ?? (f.icao24 ? "aircraft" : "unknown");
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 = parseAftn(raw, { now });
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 } = splitAftnStream(text);
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 } = splitAftnStream(this.tcpBuffer + chunk.toString("latin1"));
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.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",