@aeroswarm/codec 0.1.8 → 0.1.9

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/README.md CHANGED
@@ -52,11 +52,35 @@ In the services this is wired to the `ASTERIX_EDITIONS` env var (format `"21:0.2
52
52
  | CAT023 | CNS/ATM ground station & service status | 1.2 |
53
53
  | CAT034 | Radar service messages (north marker, sector crossing) — sensor id only | 1.27, 1.26 |
54
54
  | CAT048 | Monoradar (PSR/SSR) target reports | 1.21, 1.14 |
55
- | CAT062 | System/fused tracks from ATC (keyed by system track number; identity item 380 is compound and not yet unpacked) | 1.18, 1.16, 1.9, 1.7, 0.17 |
55
+ | CAT062 | System/fused tracks from ATC (keyed by system track number; item 380 is compound — only the target address `ADR` is unpacked into `icao24`) | 1.18, 1.16, 1.9, 1.7, 0.17 |
56
56
  | CAT063 | Sensor status — sensor id only | 1.3 |
57
57
 
58
+ CAT021 (edition 1.4+) and CAT048 also expose `trackNumber` (I021/161, I048/161), so reports without a Mode-S address can still be tracked per sensor.
59
+
58
60
  Definitions are standard `asterix` XML files in [`src/config/`](src/config/). More category XMLs are already present there (CAT001, 002, 004, …) but not yet registered — see "Adding a category" below.
59
61
 
62
+ ## AFTN / ATS messages
63
+
64
+ Flight-plan data, not positions: `parseAftn` turns one raw AFTN message (FPL, CHG, DLA, DEP, ARR, CNL) into plain JSON.
65
+
66
+ ```ts
67
+ import { parseAftn, splitAftnStream } from "@aeroswarm/codec";
68
+
69
+ // UDP: one message per datagram
70
+ const msg = parseAftn(datagram); // AftnMessage | null
71
+ // msg.callsign "BTK7630" · msg.adep "WARR" · msg.ades "WIII" · msg.aircraftType "A320"
72
+ // msg.cruiseSpeed "N0450" · msg.cruiseLevel "F350" · msg.route "DCT ABMAL L897 BIKMA DCT"
73
+ // msg.icao24 "8a0123" (item 18 CODE/) · msg.eobtAt / atdAt / ataAt (epoch ms, UTC)
74
+
75
+ // TCP: a message ends at NNNN or ETX
76
+ const { messages, rest } = splitAftnStream(buffered + chunk);
77
+ ```
78
+
79
+ - Tolerates gateway framing: SOH/STX/ETX, a `ZCZC` start line, `NNNN`, unknown header lines. Returns `null` when no `( … )` body is present — never throws.
80
+ - Month and year are resolved from `DOF/` or, failing that, the origin `DDHHMM` group nearest to `options.now`.
81
+ - `icao24` comes from `CODE/` and is the precise key for correlating a plan with a surveillance track; callsign is the fallback.
82
+ - Route stays a string (airways, fixes, `DCT`, coordinates) — turning fixes into coordinates needs an AIRAC database this package does not carry.
83
+
60
84
  ## Exposed API
61
85
 
62
86
  ### `parseAsterix(buf, defaultEdition?)`
@@ -90,6 +114,7 @@ interface AsterixFields {
90
114
  lat?: number; lon?: number; // WGS-84 degrees
91
115
  altitudeFt?: number;
92
116
  timeOfDay?: number; // seconds since UTC midnight
117
+ trackNumber?: number; // per-sensor track number (CAT010/011/020/021/048/062)
93
118
  velocity?: { groundSpeedKt?: number; headingDeg?: number };
94
119
  positionCartesian?: { xNm?: number; yNm?: number };
95
120
  measuredPosition?: { rangeNm?: number; azimuthDeg?: number };
package/dist/index.js CHANGED
@@ -268,6 +268,11 @@ function str(raw, ...names) {
268
268
  function validLatLon(lat, lon) {
269
269
  return lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180;
270
270
  }
271
+ function trdFlags(raw, out) {
272
+ if (num(raw, "TST") === 1) out.testTarget = true;
273
+ if (num(raw, "RAB") === 1) out.fixedTransponder = true;
274
+ if (num(raw, "SIM") === 1) out.simulated = true;
275
+ }
271
276
  function decodeKnownField(cat, id, data, out, raw = {}) {
272
277
  try {
273
278
  if (cat === 21) cat021(id, data, out, raw);
@@ -299,6 +304,10 @@ function catSystemStatus(id, d, out, raw) {
299
304
  }
300
305
  function cat021(id, d, out, raw) {
301
306
  switch (id) {
307
+ case "040":
308
+ trdFlags(raw, out);
309
+ break;
310
+ // I021/040 TRD
302
311
  case "010": {
303
312
  const sac = num(raw, "SAC"), sic = num(raw, "SIC");
304
313
  if (sac !== void 0 && sic !== void 0) out.dataSource = { sac, sic };
@@ -387,6 +396,12 @@ function cat021(id, d, out, raw) {
387
396
  else if (d.length >= 2) out.squawk = (d.readUIntBE(0, 2) & 4095).toString(8).padStart(4, "0");
388
397
  break;
389
398
  }
399
+ case "161": {
400
+ const tn = num(raw, "TrackN", "Tn");
401
+ if (tn !== void 0) out.trackNumber = tn;
402
+ else if (d.length >= 2) out.trackNumber = d.readUIntBE(0, 2) & 4095;
403
+ break;
404
+ }
390
405
  case "030": {
391
406
  const t = num(raw, "ToD");
392
407
  if (t !== void 0) out.timeOfDay = t;
@@ -397,6 +412,10 @@ function cat021(id, d, out, raw) {
397
412
  }
398
413
  function cat048(id, d, out, raw) {
399
414
  switch (id) {
415
+ case "020":
416
+ trdFlags(raw, out);
417
+ break;
418
+ // I048/020 TRD
400
419
  case "010": {
401
420
  const sac = num(raw, "SAC"), sic = num(raw, "SIC");
402
421
  if (sac !== void 0 && sic !== void 0) out.dataSource = { sac, sic };
@@ -416,6 +435,12 @@ function cat048(id, d, out, raw) {
416
435
  } else if (d.length >= 6) out.callsign = readCallsign(d);
417
436
  break;
418
437
  }
438
+ case "161": {
439
+ const tn = num(raw, "Tn");
440
+ if (tn !== void 0) out.trackNumber = tn;
441
+ else if (d.length >= 2) out.trackNumber = d.readUIntBE(0, 2) & 4095;
442
+ break;
443
+ }
419
444
  case "042": {
420
445
  const x = num(raw, "X"), y = num(raw, "Y");
421
446
  if (x !== void 0 && y !== void 0) out.positionCartesian = { xNm: x, yNm: y };
@@ -520,6 +545,10 @@ function cat023(id, d, out, raw) {
520
545
  }
521
546
  function cat010(id, d, out, raw) {
522
547
  switch (id) {
548
+ case "020":
549
+ trdFlags(raw, out);
550
+ break;
551
+ // I010/020 TRD
523
552
  case "010": {
524
553
  const sac = num(raw, "SAC"), sic = num(raw, "SIC");
525
554
  if (sac !== void 0 && sic !== void 0) out.dataSource = { sac, sic };
@@ -589,6 +618,10 @@ function cat010(id, d, out, raw) {
589
618
  var MS_TO_KT = 1 / 0.514444;
590
619
  function cat062(id, d, out, raw) {
591
620
  switch (id) {
621
+ case "080":
622
+ trdFlags(raw, out);
623
+ break;
624
+ // I062/080 TRD
592
625
  case "010": {
593
626
  const sac = num(raw, "SAC"), sic = num(raw, "SIC");
594
627
  if (sac !== void 0 && sic !== void 0) out.dataSource = { sac, sic };
@@ -628,6 +661,17 @@ function cat062(id, d, out, raw) {
628
661
  else if (d.length >= 2) out.trackNumber = d.readUIntBE(0, 2);
629
662
  break;
630
663
  }
664
+ case "380": {
665
+ let fsLen = 0;
666
+ while (fsLen < d.length) {
667
+ fsLen++;
668
+ if ((d[fsLen - 1] & 1) === 0) break;
669
+ }
670
+ if (fsLen < d.length && (d[0] & 128) !== 0 && d.length >= fsLen + 3) {
671
+ out.icao24 = d.readUIntBE(fsLen, 3).toString(16).padStart(6, "0");
672
+ }
673
+ break;
674
+ }
631
675
  case "070": {
632
676
  const t = num(raw, "ToD", "TOTI");
633
677
  if (t !== void 0) out.timeOfDay = t;
@@ -647,6 +691,10 @@ function catServiceMsg(id, d, out, raw) {
647
691
  }
648
692
  function cat020(id, d, out, raw) {
649
693
  switch (id) {
694
+ case "020":
695
+ trdFlags(raw, out);
696
+ break;
697
+ // I020/020 TRD
650
698
  case "010": {
651
699
  const sac = num(raw, "SAC"), sic = num(raw, "SIC");
652
700
  if (sac !== void 0 && sic !== void 0) out.dataSource = { sac, sic };
@@ -913,12 +961,289 @@ function decodeMlatFrame(_frame) {
913
961
  function decodeAsmgcsFrame(_frame) {
914
962
  return null;
915
963
  }
964
+
965
+ // src/surveillance/aftn/parser.ts
966
+ var PRIORITY_LINE = /^(SS|DD|FF|GG|KK)\s+(.+)$/;
967
+ var ORIGIN_LINE = /^(\d{6})\s+([A-Z]{8})\b/;
968
+ var ADDRESSEES_ONLY = /^(?:[A-Z]{8}\s*)+$/;
969
+ var AERODROME_TIME = /^([A-Z]{4})(\d{4})?/;
970
+ var ITEM18_ENTRY = /([A-Z]{2,5})\/(.*?)(?=\s+[A-Z]{2,5}\/|\s*$)/gs;
971
+ function normalise(input) {
972
+ const text = typeof input === "string" ? input : input.toString("latin1");
973
+ return text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").replace(/\r\n?/g, "\n").replace(/^\s*ZCZC[^\n]*\n/m, "").replace(/\s*NNNN\s*$/, "");
974
+ }
975
+ function parseHeader(headerText) {
976
+ const header = { addressees: [] };
977
+ const lines = headerText.split("\n").map((l) => l.trim()).filter(Boolean);
978
+ let inAddressees = false;
979
+ for (const line of lines) {
980
+ const origin = ORIGIN_LINE.exec(line);
981
+ if (origin) {
982
+ header.originTime = origin[1];
983
+ header.originator = origin[2];
984
+ inAddressees = false;
985
+ continue;
986
+ }
987
+ const prio = PRIORITY_LINE.exec(line);
988
+ if (prio && !header.priority) {
989
+ header.priority = prio[1];
990
+ header.addressees.push(...prio[2].match(/[A-Z]{8}/g) ?? []);
991
+ inAddressees = true;
992
+ continue;
993
+ }
994
+ if (inAddressees && ADDRESSEES_ONLY.test(line)) {
995
+ header.addressees.push(...line.match(/[A-Z]{8}/g) ?? []);
996
+ }
997
+ }
998
+ return header;
999
+ }
1000
+ function parseItem18(raw) {
1001
+ const out = {};
1002
+ const text = raw.trim();
1003
+ if (!text || text === "0") return out;
1004
+ for (const m of text.matchAll(ITEM18_ENTRY)) {
1005
+ const key = m[1];
1006
+ const value = m[2].trim();
1007
+ if (value && out[key] === void 0) out[key] = value;
1008
+ }
1009
+ return out;
1010
+ }
1011
+ function splitCallsign(token) {
1012
+ const cs = token?.split("/")[0]?.trim().toUpperCase();
1013
+ return cs || void 0;
1014
+ }
1015
+ function applyAerodromeTime(token, onAerodrome, onTime) {
1016
+ const m = token ? AERODROME_TIME.exec(token.trim().toUpperCase()) : null;
1017
+ if (!m) return;
1018
+ onAerodrome(m[1]);
1019
+ if (m[2] && onTime) onTime(m[2]);
1020
+ }
1021
+ function applyItem18(msg, entries) {
1022
+ Object.assign(msg.other, entries);
1023
+ if (entries["REG"]) msg.registration = entries["REG"].replace(/[^A-Z0-9-]/gi, "").toUpperCase() || void 0;
1024
+ if (entries["DOF"] && /^\d{6}$/.test(entries["DOF"])) msg.dof = entries["DOF"];
1025
+ const code = entries["CODE"]?.trim();
1026
+ if (code && /^[0-9A-Fa-f]{6}$/.test(code)) msg.icao24 = code.toLowerCase();
1027
+ }
1028
+ var SPEED_LEVEL_ROUTE = /^([NMK]\d{3,4})(F\d{3}|A\d{3}|S\d{4}|M\d{4}|VFR)?\s*(.*)$/;
1029
+ var TYPE_WAKE = /^\d{0,2}([A-Z0-9]{2,4})\/([LMHJ])$/;
1030
+ function applyRoute(msg, text) {
1031
+ const route = text?.trim().toUpperCase();
1032
+ if (!route) return;
1033
+ const m = SPEED_LEVEL_ROUTE.exec(route);
1034
+ if (!m) {
1035
+ msg.route = route;
1036
+ return;
1037
+ }
1038
+ msg.cruiseSpeed = m[1];
1039
+ if (m[2]) msg.cruiseLevel = m[2];
1040
+ if (m[3]) msg.route = m[3].trim();
1041
+ }
1042
+ function applyAircraft(msg, text) {
1043
+ const aircraft = text?.trim().toUpperCase();
1044
+ if (!aircraft) return;
1045
+ const m = TYPE_WAKE.exec(aircraft);
1046
+ if (m) {
1047
+ msg.aircraftType = m[1];
1048
+ msg.wake = m[2];
1049
+ } else msg.aircraftType = aircraft;
1050
+ }
1051
+ function parseFpl(parts, msg) {
1052
+ msg.callsign = splitCallsign(parts[1]);
1053
+ const rulesType = parts[2]?.trim().toUpperCase();
1054
+ if (rulesType) {
1055
+ msg.flightRules = rulesType[0];
1056
+ if (rulesType[1]) msg.flightType = rulesType[1];
1057
+ }
1058
+ applyAircraft(msg, parts[3]);
1059
+ if (parts[4]) msg.equipment = parts[4].trim();
1060
+ applyAerodromeTime(parts[5], (a) => {
1061
+ msg.adep = a;
1062
+ }, (t) => {
1063
+ msg.eobt = t;
1064
+ });
1065
+ applyRoute(msg, parts[6]);
1066
+ const dest = parts[7]?.trim().toUpperCase();
1067
+ if (dest) {
1068
+ const m = /^([A-Z]{4})(\d{4})?(?:\s+([A-Z]{4}))?(?:\s+([A-Z]{4}))?/.exec(dest);
1069
+ if (m) {
1070
+ msg.ades = m[1];
1071
+ if (m[2]) msg.eet = m[2];
1072
+ const alts = [m[3], m[4]].filter((x) => !!x);
1073
+ if (alts.length) msg.alternates = alts;
1074
+ }
1075
+ }
1076
+ applyItem18(msg, parseItem18(parts.slice(8).join("-")));
1077
+ }
1078
+ function parseChg(parts, msg) {
1079
+ msg.callsign = splitCallsign(parts[1]);
1080
+ applyAerodromeTime(parts[2], (a) => {
1081
+ msg.adep = a;
1082
+ }, (t) => {
1083
+ msg.eobt = t;
1084
+ });
1085
+ applyAerodromeTime(parts[3], (a) => {
1086
+ msg.ades = a;
1087
+ });
1088
+ const changes = {};
1089
+ const item18Chunks = [];
1090
+ for (const part of parts.slice(4)) {
1091
+ const numbered = /^(\d{1,2})\/(.*)$/.exec(part.trim());
1092
+ if (numbered) changes[numbered[1]] = numbered[2].trim();
1093
+ else item18Chunks.push(part);
1094
+ }
1095
+ if (Object.keys(changes).length) {
1096
+ msg.changes = changes;
1097
+ const f13 = changes["13"];
1098
+ if (f13) applyAerodromeTime(f13, (a) => {
1099
+ msg.adep = a;
1100
+ }, (t) => {
1101
+ msg.eobt = t;
1102
+ });
1103
+ applyRoute(msg, changes["15"]);
1104
+ const f16 = changes["16"]?.toUpperCase();
1105
+ if (f16) applyAerodromeTime(f16, (a) => {
1106
+ msg.ades = a;
1107
+ }, (t) => {
1108
+ msg.eet = t;
1109
+ });
1110
+ applyAircraft(msg, changes["9"]);
1111
+ const f18 = changes["18"];
1112
+ if (f18) applyItem18(msg, parseItem18(f18));
1113
+ }
1114
+ if (item18Chunks.length) applyItem18(msg, parseItem18(item18Chunks.join(" ")));
1115
+ }
1116
+ function parseMovement(parts, msg) {
1117
+ msg.callsign = splitCallsign(parts[1]);
1118
+ const type = msg.type;
1119
+ if (type === "DEP") {
1120
+ applyAerodromeTime(parts[2], (a) => {
1121
+ msg.adep = a;
1122
+ }, (t) => {
1123
+ msg.atd = t;
1124
+ });
1125
+ applyAerodromeTime(parts[3], (a) => {
1126
+ msg.ades = a;
1127
+ });
1128
+ } else if (type === "ARR") {
1129
+ applyAerodromeTime(parts[2], (a) => {
1130
+ msg.adep = a;
1131
+ });
1132
+ applyAerodromeTime(parts[3], (a) => {
1133
+ msg.ades = a;
1134
+ }, (t) => {
1135
+ msg.ata = t;
1136
+ });
1137
+ } else if (type === "DLA") {
1138
+ applyAerodromeTime(parts[2], (a) => {
1139
+ msg.adep = a;
1140
+ }, (t) => {
1141
+ msg.eobt = t;
1142
+ });
1143
+ applyAerodromeTime(parts[3], (a) => {
1144
+ msg.ades = a;
1145
+ });
1146
+ } else {
1147
+ applyAerodromeTime(parts[2], (a) => {
1148
+ msg.adep = a;
1149
+ });
1150
+ applyAerodromeTime(parts[3], (a) => {
1151
+ msg.ades = a;
1152
+ });
1153
+ }
1154
+ applyItem18(msg, parseItem18(parts.slice(4).join(" ")));
1155
+ }
1156
+ function resolveBaseDay(msg, now) {
1157
+ if (msg.dof) {
1158
+ const yy = Number(msg.dof.slice(0, 2));
1159
+ return { y: 2e3 + yy, m: Number(msg.dof.slice(2, 4)) - 1, d: Number(msg.dof.slice(4, 6)) };
1160
+ }
1161
+ const origin = msg.header.originTime;
1162
+ if (!origin) return null;
1163
+ const day = Number(origin.slice(0, 2));
1164
+ const ref = new Date(now);
1165
+ let best = null;
1166
+ let bestDiff = Infinity;
1167
+ for (const offset of [-1, 0, 1]) {
1168
+ const probe = new Date(Date.UTC(ref.getUTCFullYear(), ref.getUTCMonth() + offset, day));
1169
+ if (probe.getUTCDate() !== day) continue;
1170
+ const diff = Math.abs(probe.getTime() - now);
1171
+ if (diff < bestDiff) {
1172
+ bestDiff = diff;
1173
+ best = { y: probe.getUTCFullYear(), m: probe.getUTCMonth(), d: day };
1174
+ }
1175
+ }
1176
+ return best;
1177
+ }
1178
+ function toEpoch(base, hhmm) {
1179
+ if (!hhmm || !/^\d{4}$/.test(hhmm)) return void 0;
1180
+ const hh = Number(hhmm.slice(0, 2));
1181
+ const mm = Number(hhmm.slice(2, 4));
1182
+ if (hh > 23 || mm > 59) return void 0;
1183
+ return Date.UTC(base.y, base.m, base.d, hh, mm);
1184
+ }
1185
+ function parseAftn(input, options = {}) {
1186
+ const s = normalise(input);
1187
+ const open = s.indexOf("(");
1188
+ const close = s.lastIndexOf(")");
1189
+ if (open < 0 || close <= open) return null;
1190
+ const text = s.slice(open, close + 1).replace(/\s*\n\s*/g, " ").trim();
1191
+ const inner = text.slice(1, -1);
1192
+ const parts = inner.split("-").map((p) => p.trim());
1193
+ const type = parts[0];
1194
+ if (!type || !/^[A-Z]{3}$/.test(type)) return null;
1195
+ const msg = {
1196
+ type,
1197
+ header: parseHeader(s.slice(0, open)),
1198
+ text,
1199
+ other: {}
1200
+ };
1201
+ switch (type) {
1202
+ case "FPL":
1203
+ parseFpl(parts, msg);
1204
+ break;
1205
+ case "CHG":
1206
+ parseChg(parts, msg);
1207
+ break;
1208
+ case "DEP":
1209
+ case "ARR":
1210
+ case "DLA":
1211
+ case "CNL":
1212
+ parseMovement(parts, msg);
1213
+ break;
1214
+ default:
1215
+ msg.callsign = splitCallsign(parts[1]);
1216
+ }
1217
+ const base = resolveBaseDay(msg, options.now ?? Date.now());
1218
+ if (base) {
1219
+ msg.eobtAt = toEpoch(base, msg.eobt);
1220
+ msg.atdAt = toEpoch(base, msg.atd);
1221
+ msg.ataAt = toEpoch(base, msg.ata);
1222
+ }
1223
+ return msg;
1224
+ }
1225
+ function splitAftnStream(buffer) {
1226
+ const messages = [];
1227
+ let rest = buffer;
1228
+ const terminator = /NNNN|\x03/;
1229
+ for (; ; ) {
1230
+ const m = terminator.exec(rest);
1231
+ if (!m) break;
1232
+ const end = m.index + m[0].length;
1233
+ const chunk = rest.slice(0, end);
1234
+ if (chunk.includes("(")) messages.push(chunk);
1235
+ rest = rest.slice(end);
1236
+ }
1237
+ return { messages, rest };
1238
+ }
916
1239
  export {
917
1240
  decodeAdsbFrame,
918
1241
  decodeAis,
919
1242
  decodeAsmgcsFrame,
920
1243
  decodeMlatFrame,
1244
+ parseAftn,
921
1245
  parseAsterix,
922
1246
  resolveAsterixAltitudeFt,
923
- resolveAsterixPosition
1247
+ resolveAsterixPosition,
1248
+ splitAftnStream
924
1249
  };
@@ -0,0 +1,3 @@
1
+ export { parseAftn, splitAftnStream } from "./parser.js";
2
+ export type { AftnMessage, AftnHeader, ParseAftnOptions } from "./types.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/surveillance/aftn/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { AftnMessage, ParseAftnOptions } from "./types.js";
2
+ /**
3
+ * Parse one AFTN message (raw datagram / TCP-delimited text).
4
+ *
5
+ * Tolerant of gateway framing: control characters (SOH/STX/ETX), a
6
+ * classic `ZCZC` start line, `NNNN` terminator and unknown header lines are
7
+ * ignored. Returns `null` when no `( … )` ATS message body is present.
8
+ */
9
+ export declare function parseAftn(input: string | Buffer, options?: ParseAftnOptions): AftnMessage | null;
10
+ /** Split a TCP byte stream into complete messages: a message ends at `NNNN`
11
+ * or ETX (0x03). Returns the complete messages and the unconsumed tail. */
12
+ export declare function splitAftnStream(buffer: string): {
13
+ messages: string[];
14
+ rest: string;
15
+ };
16
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/surveillance/aftn/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAsN5E;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,WAAW,GAAG,IAAI,CAsCpG;AAED;4EAC4E;AAC5E,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAapF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parser.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.test.d.ts","sourceRoot":"","sources":["../../../src/surveillance/aftn/parser.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ /** Parsed AFTN message header (priority / addressees / origin group). */
2
+ export interface AftnHeader {
3
+ /** Priority indicator: SS, DD, FF, GG or KK */
4
+ priority?: string;
5
+ /** 8-letter addressee indicators */
6
+ addressees: string[];
7
+ /** Origin time group, DDHHMM (UTC) */
8
+ originTime?: string;
9
+ /** 8-letter originator indicator */
10
+ originator?: string;
11
+ }
12
+ /**
13
+ * One parsed ATS message (ICAO Doc 4444 App. 3 formats: FPL, CHG, DLA, DEP,
14
+ * ARR, CNL; any other 3-letter type is returned with identity fields only).
15
+ *
16
+ * Plain JSON — safe to publish on Kafka as-is.
17
+ */
18
+ export interface AftnMessage {
19
+ /** FPL | CHG | DLA | DEP | ARR | CNL | … */
20
+ type: string;
21
+ header: AftnHeader;
22
+ /** Message text from "(" to ")" with line breaks normalised to spaces */
23
+ text: string;
24
+ /** Aircraft identification (item 7), upper-case */
25
+ callsign?: string;
26
+ /** I (IFR) · V (VFR) · Y (IFR→VFR) · Z (VFR→IFR) */
27
+ flightRules?: string;
28
+ /** S scheduled · N non-scheduled · G general · M military · X other */
29
+ flightType?: string;
30
+ /** ICAO aircraft type designator, e.g. A320 */
31
+ aircraftType?: string;
32
+ /** L / M / H / J */
33
+ wake?: string;
34
+ /** Item 10 as sent (communication/navigation/surveillance equipment) */
35
+ equipment?: string;
36
+ /** Departure aerodrome (ICAO) */
37
+ adep?: string;
38
+ /** Destination aerodrome (ICAO) */
39
+ ades?: string;
40
+ /** Estimated off-block time HHMM (FPL), or the new EOBT (DLA) */
41
+ eobt?: string;
42
+ /** Actual time of departure HHMM (DEP) */
43
+ atd?: string;
44
+ /** Actual time of arrival HHMM (ARR) */
45
+ ata?: string;
46
+ /** Estimated elapsed time HHMM (FPL item 16) */
47
+ eet?: string;
48
+ /** Cruising speed group, e.g. N0450 / M078 / K0830 */
49
+ cruiseSpeed?: string;
50
+ /** Cruising level group, e.g. F350 / A045 / S1010 / VFR */
51
+ cruiseLevel?: string;
52
+ /** Route string (airways, fixes, DCT, coordinates) after speed/level */
53
+ route?: string;
54
+ /** Alternate aerodromes */
55
+ alternates?: string[];
56
+ /** Item 18 keyed entries: REG, DOF, EET, CODE, PBN, NAV, OPR, RMK, … */
57
+ other: Record<string, string>;
58
+ /** Aircraft registration (REG/) */
59
+ registration?: string;
60
+ /** 24-bit Mode-S address, lower-case hex (CODE/) — correlates with a track's icao24 */
61
+ icao24?: string;
62
+ /** Date of flight YYMMDD (DOF/) */
63
+ dof?: string;
64
+ /** CHG only: ICAO item number → new data */
65
+ changes?: Record<string, string>;
66
+ /** Epoch ms (UTC) resolved from DOF/origin day + the HHMM groups above */
67
+ eobtAt?: number;
68
+ atdAt?: number;
69
+ ataAt?: number;
70
+ }
71
+ export interface ParseAftnOptions {
72
+ /** Reference time used to resolve month/year when DOF/ is absent. Default: now. */
73
+ now?: number;
74
+ }
75
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/surveillance/aftn/types.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IAEb,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,mCAAmC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd"}
@@ -1 +1 @@
1
- {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AA0D7C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,GAAE,SAAc,GAAG,IAAI,CAWrH"}
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAoE7C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,GAAE,SAAc,GAAG,IAAI,CAWrH"}
@@ -41,6 +41,12 @@ export interface AsterixFields {
41
41
  vehicleFleet?: number;
42
42
  /** Sensor-assigned local track number */
43
43
  trackNumber?: number;
44
+ /** TST bit — test target */
45
+ testTarget?: boolean;
46
+ /** RAB bit — report from a fixed field-monitor transponder (MLAT RPU etc.) */
47
+ fixedTransponder?: boolean;
48
+ /** SIM bit — simulated target */
49
+ simulated?: boolean;
44
50
  }
45
51
  export interface AsterixRecord {
46
52
  category: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/types.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,iBAAiB,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,gBAAgB,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,UAAU,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1C,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAA;KAAE,CAAC;IACnG,4CAA4C;IAC5C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAChD,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;CACvB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/types.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,iBAAiB,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,gBAAgB,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,UAAU,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1C,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAA;KAAE,CAAC;IACnG,4CAA4C;IAC5C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAChD,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,4BAA4B;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iCAAiC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;CACvB"}
@@ -3,4 +3,5 @@ export * from "./adsb/index.js";
3
3
  export * from "./ais/index.js";
4
4
  export * from "./mlat/index.js";
5
5
  export * from "./asmgcs/index.js";
6
+ export * from "./aftn/index.js";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/surveillance/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/surveillance/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC"}
package/package.json CHANGED
@@ -1,40 +1,40 @@
1
- {
2
- "name": "@aeroswarm/codec",
3
- "version": "0.1.8",
4
- "type": "module",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "import": "./dist/index.js",
10
- "types": "./dist/index.d.ts",
11
- "main": "./dist/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist",
16
- "src/config"
17
- ],
18
- "publishConfig": {
19
- "access": "public",
20
- "registry": "https://registry.npmjs.org/"
21
- },
22
- "scripts": {
23
- "build": "tsup src/index.ts --format esm --clean && tsc --emitDeclarationOnly",
24
- "dev": "tsup src/index.ts --format esm --dts --watch",
25
- "test": "vitest run --passWithNoTests",
26
- "typecheck": "tsc --noEmit",
27
- "clean": "rm -rf dist",
28
- "prepublishOnly": "pnpm run typecheck && pnpm run test && pnpm run build",
29
- "release": "release-it",
30
- "release:dry": "release-it --dry-run",
31
- "release:patch": "release-it patch --no-git.requireCleanWorkingDir",
32
- "release:minor": "release-it minor --no-git.requireCleanWorkingDir",
33
- "release:major": "release-it major --no-git.requireCleanWorkingDir"
34
- },
35
- "devDependencies": {
36
- "release-it": "^18.1.2",
37
- "tsup": "^8.5.1",
38
- "vitest": "^4.1.6"
39
- }
40
- }
1
+ {
2
+ "name": "@aeroswarm/codec",
3
+ "version": "0.1.9",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "main": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src/config"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup src/index.ts --format esm --clean && tsc --emitDeclarationOnly",
24
+ "dev": "tsup src/index.ts --format esm --dts --watch",
25
+ "test": "vitest run --passWithNoTests",
26
+ "typecheck": "tsc --noEmit",
27
+ "clean": "rm -rf dist",
28
+ "prepublishOnly": "pnpm run typecheck && pnpm run test && pnpm run build",
29
+ "release": "release-it",
30
+ "release:dry": "release-it --dry-run",
31
+ "release:patch": "release-it patch --no-git.requireCleanWorkingDir",
32
+ "release:minor": "release-it minor --no-git.requireCleanWorkingDir",
33
+ "release:major": "release-it major --no-git.requireCleanWorkingDir"
34
+ },
35
+ "devDependencies": {
36
+ "release-it": "^18.1.2",
37
+ "tsup": "^8.5.1",
38
+ "vitest": "^4.1.6"
39
+ }
40
+ }