@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 +119 -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":
|
|
@@ -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
|
-
|
|
677
|
-
|
|
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
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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,
|
|
944
|
+
best = { edition, all, consumed, score };
|
|
893
945
|
}
|
|
894
946
|
}
|
|
895
|
-
|
|
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
|
|
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 @@
|
|
|
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"}
|