@aeroswarm/codec 0.1.9 → 0.1.11

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/index.js CHANGED
@@ -130,27 +130,47 @@ function parseCompound(s) {
130
130
  const varEnd = inner.indexOf("</Variable>");
131
131
  const afterVar = varEnd >= 0 ? inner.slice(varEnd + "</Variable>".length) : "";
132
132
  const subs = [];
133
- const subRe = /<(Fixed|Variable|Repetitive|Explicit)\s/g;
134
- let sm;
135
- while ((sm = subRe.exec(afterVar)) !== null) {
136
- const tag = sm[1];
137
- const rest = afterVar.slice(sm.index);
138
- if (tag === "Fixed") {
139
- const lm = rest.match(/<Fixed\s+length="(\d+)"/);
133
+ for (const child of topLevelChildren(afterVar)) {
134
+ const first = child.body.match(/<Fixed\s+length="(\d+)"/);
135
+ if (child.tag === "Fixed") {
136
+ const lm = child.open.match(/length="(\d+)"/);
140
137
  if (lm) subs.push({ type: "fixed", bytes: parseInt(lm[1]) });
141
- } else if (tag === "Variable") {
142
- const lm = rest.match(/<Fixed\s+length="(\d+)"/);
143
- subs.push({ type: "variable", chunkBytes: lm ? parseInt(lm[1]) : 1 });
144
- } else if (tag === "Repetitive") {
145
- const repAttr = rest.match(/<Repetitive\s+rep="(\d+)"/);
146
- const lm = rest.match(/<Fixed\s+length="(\d+)"/);
147
- subs.push({ type: "repetitive", repBytes: repAttr ? parseInt(repAttr[1]) : 1, itemBytes: lm ? parseInt(lm[1]) : 1 });
138
+ } else if (child.tag === "Variable") {
139
+ subs.push({ type: "variable", chunkBytes: first ? parseInt(first[1]) : 1 });
140
+ } else if (child.tag === "Repetitive") {
141
+ const repAttr = child.open.match(/rep="(\d+)"/);
142
+ subs.push({ type: "repetitive", repBytes: repAttr ? parseInt(repAttr[1]) : 1, itemBytes: first ? parseInt(first[1]) : 1 });
148
143
  } else {
149
144
  subs.push({ type: "explicit" });
150
145
  }
151
146
  }
152
147
  return { type: "compound", indicatorChunk, subs };
153
148
  }
149
+ function topLevelChildren(s) {
150
+ const out = [];
151
+ const re = /<(\/?)(Fixed|Variable|Repetitive|Explicit|Compound)\b([^>]*?)(\/?)>/g;
152
+ let depth = 0;
153
+ let current = null;
154
+ let m;
155
+ while ((m = re.exec(s)) !== null) {
156
+ const closing = m[1] === "/";
157
+ const selfClosing = m[4] === "/";
158
+ if (!closing) {
159
+ if (depth === 0) {
160
+ if (selfClosing) out.push({ tag: m[2], open: m[0], body: "" });
161
+ else current = { tag: m[2], open: m[0], start: m.index + m[0].length };
162
+ }
163
+ if (!selfClosing) depth++;
164
+ } else {
165
+ depth--;
166
+ if (depth === 0 && current) {
167
+ out.push({ tag: current.tag, open: current.open, body: s.slice(current.start, m.index) });
168
+ current = null;
169
+ }
170
+ }
171
+ }
172
+ return out;
173
+ }
154
174
  function firstContent(s, tag) {
155
175
  return s.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`))?.[1] ?? "";
156
176
  }
@@ -218,16 +238,17 @@ function decodeBlock(block, def, onField) {
218
238
  const uapMap = new Map(def.uap.map((e) => [e.bit, e.id]));
219
239
  for (let i = 0; i < bits.length; i++) {
220
240
  if (!bits[i]) continue;
221
- if (off >= block.length) break;
241
+ if (off >= block.length) return { end: block.length, complete: true };
222
242
  const uapPos = i + Math.floor(i / 7);
223
243
  const id = uapMap.get(uapPos);
224
- if (id === void 0) break;
244
+ if (id === void 0) return { end: off, complete: false };
225
245
  const itemDef = def.items.get(id);
226
- if (!itemDef) break;
246
+ if (!itemDef) return { end: off, complete: false };
227
247
  const start = off;
228
248
  off = skipItem(block, off, itemDef);
229
249
  onField(id, block.slice(start, off), itemDef);
230
250
  }
251
+ return { end: off, complete: true };
231
252
  }
232
253
 
233
254
  // src/surveillance/asterix/fields.ts
@@ -616,6 +637,19 @@ function cat010(id, d, out, raw) {
616
637
  }
617
638
  }
618
639
  var MS_TO_KT = 1 / 0.514444;
640
+ function flightPlanCallsign(d) {
641
+ let indicators = 0;
642
+ while (indicators < d.length) {
643
+ indicators++;
644
+ if ((d[indicators - 1] & 1) === 0) break;
645
+ }
646
+ const first = d[0] ?? 0;
647
+ if ((first & 64) === 0) return void 0;
648
+ const at = indicators + ((first & 128) !== 0 ? 2 : 0);
649
+ if (d.length < at + 7) return void 0;
650
+ const text = d.subarray(at, at + 7).toString("latin1").trim();
651
+ return text.length > 0 && /^[A-Z0-9 ]+$/.test(text) ? text : void 0;
652
+ }
619
653
  function cat062(id, d, out, raw) {
620
654
  switch (id) {
621
655
  case "080":
@@ -661,14 +695,39 @@ function cat062(id, d, out, raw) {
661
695
  else if (d.length >= 2) out.trackNumber = d.readUIntBE(0, 2);
662
696
  break;
663
697
  }
698
+ case "300": {
699
+ const vfi = num(raw, "VFI");
700
+ out.vehicleFleet = vfi ?? (d.length >= 1 ? d[0] : void 0);
701
+ out.targetType = "vehicle";
702
+ break;
703
+ }
704
+ case "245": {
705
+ const cs = str(raw, "TId");
706
+ const text = cs !== void 0 ? cs : d.length >= 7 ? readCallsign(d.subarray(1)) : "";
707
+ if (text) out.callsign = text;
708
+ break;
709
+ }
710
+ case "390": {
711
+ if (out.callsign === void 0) {
712
+ const csn = flightPlanCallsign(d);
713
+ if (csn) out.callsign = csn;
714
+ }
715
+ break;
716
+ }
664
717
  case "380": {
665
718
  let fsLen = 0;
666
719
  while (fsLen < d.length) {
667
720
  fsLen++;
668
721
  if ((d[fsLen - 1] & 1) === 0) break;
669
722
  }
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");
723
+ let at = fsLen;
724
+ if (fsLen < d.length && (d[0] & 128) !== 0 && d.length >= at + 3) {
725
+ out.icao24 = d.readUIntBE(at, 3).toString(16).padStart(6, "0");
726
+ at += 3;
727
+ }
728
+ if (fsLen < d.length && (d[0] & 64) !== 0 && d.length >= at + 6 && out.callsign === void 0) {
729
+ const id2 = readCallsign(d.subarray(at, at + 6));
730
+ if (id2) out.callsign = id2;
672
731
  }
673
732
  break;
674
733
  }
@@ -857,6 +916,14 @@ var EDITIONS = {
857
916
  };
858
917
  function parseAsterix(buf, defaultEdition) {
859
918
  const records = [];
919
+ for (const block of parseAsterixBlocks(buf, defaultEdition)) {
920
+ if (block.edition === null) continue;
921
+ for (const fields of block.records) records.push({ category: block.category, edition: block.edition, fields });
922
+ }
923
+ return records;
924
+ }
925
+ function parseAsterixBlocks(buf, defaultEdition) {
926
+ const blocks = [];
860
927
  let offset = 0;
861
928
  while (offset + 3 <= buf.length) {
862
929
  const cat = buf[offset];
@@ -869,26 +936,49 @@ function parseAsterix(buf, defaultEdition) {
869
936
  for (const edition of toTry) {
870
937
  const def = loadDef(cat, edition);
871
938
  if (!def) continue;
872
- const fields = {};
873
- try {
874
- decodeBlock(block, def, (id, data, itemDef) => {
875
- const bits = itemDef.type === "fixed" || itemDef.type === "variable" ? itemDef.bits : void 0;
876
- const bitData = itemDef.type === "variable" ? data.subarray(0, itemDef.chunkBytes) : data;
877
- const raw = bits?.length ? extractBitFields(bitData, bits) : {};
878
- decodeKnownField(cat, id, data, fields, raw);
879
- });
880
- } catch {
881
- continue;
882
- }
883
- if (!isPlausible(fields)) continue;
884
- const score = plausibilityScore(fields);
939
+ const { all, consumed } = decodeRecords(block, def, cat);
940
+ const first = all[0];
941
+ if (!first) continue;
942
+ const score = plausibilityScore(first);
885
943
  if (!best || score > best.score) {
886
- best = { edition, fields, score };
944
+ best = { edition, all, consumed, score };
887
945
  }
888
946
  }
889
- if (best) records.push({ category: cat, edition: best.edition, fields: best.fields });
947
+ blocks.push({
948
+ category: cat,
949
+ edition: best?.edition ?? null,
950
+ length: block.length,
951
+ consumed: best?.consumed ?? 0,
952
+ records: best?.all ?? []
953
+ });
890
954
  }
891
- return records;
955
+ return blocks;
956
+ }
957
+ function decodeRecords(block, def, cat) {
958
+ const out = [];
959
+ let pos = 0;
960
+ let consumed = 0;
961
+ while (pos < block.length) {
962
+ const fields = {};
963
+ let result;
964
+ try {
965
+ result = decodeBlock(block.subarray(pos), def, (id, data, itemDef) => {
966
+ const bits = itemDef.type === "fixed" || itemDef.type === "variable" ? itemDef.bits : void 0;
967
+ const bitData = itemDef.type === "variable" ? data.subarray(0, itemDef.chunkBytes) : data;
968
+ const raw = bits?.length ? extractBitFields(bitData, bits) : {};
969
+ decodeKnownField(cat, id, data, fields, raw);
970
+ });
971
+ } catch {
972
+ break;
973
+ }
974
+ if (!isPlausible(fields)) break;
975
+ if (out.length > 0 && Object.keys(fields).length === 0) break;
976
+ out.push(fields);
977
+ if (!result.complete || result.end <= 0) break;
978
+ pos += result.end;
979
+ consumed = pos;
980
+ }
981
+ return { all: out, consumed };
892
982
  }
893
983
  function isPlausible(f) {
894
984
  if (f.lat !== void 0 && (f.lat < -90 || f.lat > 90)) return false;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cat062.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cat062.test.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/cat062.test.ts"],"names":[],"mappings":""}
@@ -7,8 +7,13 @@ export declare function parseFspec(buf: Buffer, start?: number): {
7
7
  /** Advance offset past one DataItem, returning new offset. */
8
8
  export declare function skipItem(buf: Buffer, off: number, def: ItemDef): number;
9
9
  /**
10
- * Decode one ASTERIX data block (everything after the 3-byte header).
11
- * Calls `onField(id, bytes, itemDef)` for each present DataItem.
10
+ * Decode ONE record at the start of `block` (a data block can carry many records back to back, each with its own
11
+ * FSPEC). Calls `onField(id, bytes, itemDef)` for each present DataItem. Returns where the record ended, so the caller
12
+ * can go on with the next one, and whether that position can be trusted: `complete` is false when an item the
13
+ * definition does not know stopped the decode (the length of what follows is then unknown).
12
14
  */
13
- export declare function decodeBlock(block: Buffer, def: AsterixDef, onField: (id: string, data: Buffer, itemDef: ItemDef) => void): void;
15
+ export declare function decodeBlock(block: Buffer, def: AsterixDef, onField: (id: string, data: Buffer, itemDef: ItemDef) => void): {
16
+ end: number;
17
+ complete: boolean;
18
+ };
14
19
  //# sourceMappingURL=decoder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/decoder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI3D,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG;IAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAUnF;AAID,8DAA8D;AAC9D,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAiDvE;AAID;;;GAGG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GAC5D,IAAI,CAyBN"}
1
+ {"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/decoder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI3D,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG;IAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAUnF;AAID,8DAA8D;AAC9D,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAiDvE;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GAC5D;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CA0BpC"}
@@ -1,4 +1,4 @@
1
- import type { AsterixRecord } from './types.js';
1
+ import type { AsterixRecord, AsterixFields } from './types.js';
2
2
  /**
3
3
  * Parse raw ASTERIX UDP datagram into structured records.
4
4
  *
@@ -13,4 +13,19 @@ import type { AsterixRecord } from './types.js';
13
13
  * Use when you know the sender's edition for certainty.
14
14
  */
15
15
  export declare function parseAsterix(buf: Buffer, defaultEdition?: Record<number, string>): AsterixRecord[];
16
+ /** What became of one data block: the edition that fit (null = none, the block is dropped), its records, and how much of the block they used. */
17
+ export interface AsterixBlockInfo {
18
+ category: number;
19
+ edition: string | null;
20
+ /** Octets of the block after its 3-octet header. */
21
+ length: number;
22
+ /** Octets taken up by the decoded records. Less than `length` means an item was sized wrongly or the tail was not understood. */
23
+ consumed: number;
24
+ records: AsterixFields[];
25
+ }
26
+ /**
27
+ * Same decoding as `parseAsterix`, block by block, with the facts needed to judge it (see `AsterixBlockInfo`). For tools
28
+ * that check a decoder against a capture; `parseAsterix` is built on it.
29
+ */
30
+ export declare function parseAsterixBlocks(buf: Buffer, defaultEdition?: Record<number, string>): AsterixBlockInfo[];
16
31
  //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAC;AAsB/D;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,aAAa,EAAE,CA6CjB"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAsB/D;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,aAAa,EAAE,CAOjB;AAED,iJAAiJ;AACjJ,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,iIAAiI;IACjI,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,gBAAgB,EAAE,CAwCpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeroswarm/codec",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",