@aeroswarm/codec 0.1.10 → 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":
@@ -667,14 +701,33 @@ function cat062(id, d, out, raw) {
667
701
  out.targetType = "vehicle";
668
702
  break;
669
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
+ }
670
717
  case "380": {
671
718
  let fsLen = 0;
672
719
  while (fsLen < d.length) {
673
720
  fsLen++;
674
721
  if ((d[fsLen - 1] & 1) === 0) break;
675
722
  }
676
- if (fsLen < d.length && (d[0] & 128) !== 0 && d.length >= fsLen + 3) {
677
- 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;
678
731
  }
679
732
  break;
680
733
  }
@@ -863,6 +916,14 @@ var EDITIONS = {
863
916
  };
864
917
  function parseAsterix(buf, defaultEdition) {
865
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 = [];
866
927
  let offset = 0;
867
928
  while (offset + 3 <= buf.length) {
868
929
  const cat = buf[offset];
@@ -875,26 +936,49 @@ function parseAsterix(buf, defaultEdition) {
875
936
  for (const edition of toTry) {
876
937
  const def = loadDef(cat, edition);
877
938
  if (!def) continue;
878
- const fields = {};
879
- try {
880
- decodeBlock(block, def, (id, data, itemDef) => {
881
- const bits = itemDef.type === "fixed" || itemDef.type === "variable" ? itemDef.bits : void 0;
882
- const bitData = itemDef.type === "variable" ? data.subarray(0, itemDef.chunkBytes) : data;
883
- const raw = bits?.length ? extractBitFields(bitData, bits) : {};
884
- decodeKnownField(cat, id, data, fields, raw);
885
- });
886
- } catch {
887
- continue;
888
- }
889
- if (!isPlausible(fields)) continue;
890
- 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);
891
943
  if (!best || score > best.score) {
892
- best = { edition, fields, score };
944
+ best = { edition, all, consumed, score };
893
945
  }
894
946
  }
895
- 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
+ });
896
954
  }
897
- 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 };
898
982
  }
899
983
  function isPlausible(f) {
900
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.10",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",