@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.
@@ -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;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;IACtB;uEACmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACxC;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,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;IAiJlC,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":"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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 (isSurface) {
238
- const targetType = f.targetType ?? (f.icao24 ? "aircraft" : "unknown");
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 = parseAftn(raw, { now });
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 } = splitAftnStream(text);
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 } = splitAftnStream(this.tcpBuffer + chunk.toString("latin1"));
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.2",
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",