@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 +125 -35
- package/dist/surveillance/asterix/cat062.test.d.ts +2 -0
- package/dist/surveillance/asterix/cat062.test.d.ts.map +1 -0
- package/dist/surveillance/asterix/decoder.d.ts +8 -3
- package/dist/surveillance/asterix/decoder.d.ts.map +1 -1
- package/dist/surveillance/asterix/parser.d.ts +16 -1
- package/dist/surveillance/asterix/parser.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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)
|
|
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)
|
|
244
|
+
if (id === void 0) return { end: off, complete: false };
|
|
225
245
|
const itemDef = def.items.get(id);
|
|
226
|
-
if (!itemDef)
|
|
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
|
-
|
|
671
|
-
|
|
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
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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,
|
|
944
|
+
best = { edition, all, consumed, score };
|
|
887
945
|
}
|
|
888
946
|
}
|
|
889
|
-
|
|
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
|
|
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 @@
|
|
|
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
|
|
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):
|
|
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
|
|
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,
|
|
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"}
|