beast-decoder
Version:
Parses messages coming from a BEAST TCP port. For example out.adsb.lol:1337 [adsb.lol](https://www.adsb.lol/docs/overview/introduction/)
336 lines (331 loc) • 11 kB
JavaScript
// src/util/event_manager.ts
var EventManager = class {
constructor() {
this.listeners = /* @__PURE__ */ new Map();
}
addListener(eventName, callback) {
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, []);
}
this.listeners.get(eventName).push(callback);
}
removeListener(eventName, callback) {
if (this.listeners.has(eventName)) {
const list = this.listeners.get(eventName);
const index = list.findIndex((c) => c === callback);
if (index !== -1) {
list.splice(index, 1);
}
}
}
dispatch(eventName, data) {
if (this.listeners.has(eventName)) {
const list = this.listeners.get(eventName);
for (const l of list) {
l(data);
}
}
}
};
// src/parse.ts
import * as modeSAdsbParser from "mode-s-adsb-parser";
// src/util/env.ts
function isDevEnvironment() {
return process.env.NODE_ENV !== "production";
}
// src/util/log.ts
function logsAreEnabled() {
return isDevEnvironment();
}
function logError(...args) {
if (logsAreEnabled()) {
console.error.apply(null, args);
}
}
function logWarning(...args) {
if (logsAreEnabled()) {
console.warn.apply(null, args);
}
}
// src/parse.ts
function parseBeastSocketData(data, ignoreInvalid) {
const frames = parseBeastLine(data, ignoreInvalid);
const parsedFrames = [];
for (const frame of frames) {
try {
parsedFrames.push(parseBeastFrame(frame));
} catch (err) {
if (ignoreInvalid) {
logWarning("ignoring invalid frame", frame.format, frame.bytes);
} else {
throw err;
}
}
}
return parsedFrames;
}
var ESCAPE_BYTE = 26;
var BEAST_BINARY_FRAME_FORMAT_MODE_AC = 49;
var BEAST_BINARY_FRAME_FORMAT_MODE_S_SHORT = 50;
var BEAST_BINARY_FRAME_FORMAT_MODE_S_LONG = 51;
var BEAST_BINARY_FRAME_FORMAT_DEBUG = 52;
var BEAST_BINARY_FRAME_CONTENT_LENGTHS = {
49: 6 + 1 + 2,
50: 6 + 1 + 7,
51: 6 + 1 + 14,
52: 6 + 1 + 1
//TODO is this correct? documentation is unclear https://wiki.jetvision.de/wiki/Mode-S_Beast:Data_Output_Formats
};
var BEAST_BINARY_FRAME_TYPES = {
"49": "MODE_AC",
"50": "MODE_S",
"51": "ADSB",
"52": "DEBUG"
};
var FormatError = class extends Error {
constructor(message) {
super(message);
}
};
var FormatErrorWithByteIndex = class extends Error {
constructor(message, byteIndex) {
super(message + " at byte " + byteIndex);
}
};
function parseBeastLine(bytes, ignoreInvalid) {
const frames = [];
if (bytes[0] !== ESCAPE_BYTE) {
if (ignoreInvalid) {
return [];
} else {
throw new FormatErrorWithByteIndex("first byte is not an escape byte", 0);
}
}
let currentFrameBytes = [];
let currentFrameFormat;
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
const byte = bytes[byteIndex];
if (currentFrameBytes.length === 0 && typeof currentFrameFormat !== "number" && byte !== ESCAPE_BYTE) {
if (ignoreInvalid) {
break;
} else {
throw new FormatErrorWithByteIndex("first byte of frame is not an escape byte", byteIndex);
}
}
if (byte === ESCAPE_BYTE) {
const nextByteIndex = byteIndex + 1;
if (bytes.length <= nextByteIndex) {
if (ignoreInvalid) {
break;
} else {
throw new FormatErrorWithByteIndex("unexpected escape byte at the very end", byteIndex);
}
}
const nextByte = bytes[nextByteIndex];
if (nextByte === ESCAPE_BYTE) {
currentFrameBytes.push(nextByte);
} else if (nextByte === BEAST_BINARY_FRAME_FORMAT_MODE_AC || nextByte === BEAST_BINARY_FRAME_FORMAT_MODE_S_SHORT || nextByte === BEAST_BINARY_FRAME_FORMAT_MODE_S_LONG || nextByte === BEAST_BINARY_FRAME_FORMAT_DEBUG) {
if (currentFrameBytes.length > 0) {
makeFrameAndCleanup();
}
currentFrameFormat = nextByte;
} else {
if (ignoreInvalid) {
currentFrameBytes.push(nextByte);
} else {
throw new FormatErrorWithByteIndex("unexpected escape byte (the following byte " + (byteIndex + 1) + " does not need escaping)", byteIndex);
}
}
byteIndex += 1;
} else {
currentFrameBytes.push(byte);
}
}
if (currentFrameBytes.length > 0) {
makeFrameAndCleanup();
}
function makeFrameAndCleanup() {
if (typeof currentFrameFormat !== "number") {
if (ignoreInvalid) {
cleanup();
return;
} else {
throw new FormatError("can not make message, currentFrameFormat is undefined");
}
}
const expectedFrameContentLength = BEAST_BINARY_FRAME_CONTENT_LENGTHS[currentFrameFormat];
if (currentFrameBytes.length !== expectedFrameContentLength) {
if (ignoreInvalid) {
cleanup();
return;
} else {
logError(currentFrameBytes, Buffer.from(currentFrameBytes).toString("ascii"));
throw new FormatError('invalid content byte length for frame type "' + String.fromCharCode(currentFrameFormat) + '": ' + currentFrameBytes.length + " (expected " + expectedFrameContentLength + ")");
}
}
let allBytesAreNull = true;
for (const byte of currentFrameBytes) {
if (byte !== 0) {
allBytesAreNull = false;
break;
}
}
if (!allBytesAreNull) {
frames.push({
format: currentFrameFormat,
bytes: currentFrameBytes
});
}
cleanup();
function cleanup() {
currentFrameBytes = [];
currentFrameFormat = void 0;
}
}
return frames;
}
function parseBeastFrame(frame) {
const dataBytes = frame.bytes.slice(7);
const mlatTimestamp = modeSAdsbParser.util.bits_and_bytes.bitStringToUnsignedIntegerSafeRangeFromBits(modeSAdsbParser.util.bits_and_bytes.bytesToFullBitString(frame.bytes.slice(0, 6)), 6 * 8, "mlatTimestamp");
const signalLevel = frame.bytes[6];
if (frame.format < 49 || frame.format > 52) {
throw new Error("unexpected frame.format: " + frame.format);
}
return {
type: BEAST_BINARY_FRAME_TYPES[frame.format],
mlatTimestamp,
signalLevel,
dataBytes
};
}
// src/beast_handler.ts
import { decodeBasics, parseADSB, parseModeS, calculateUnambigiousLocation } from "mode-s-adsb-parser";
var MAX_AIRCRAFT_CACHE_TIME = 1e3 * 60 * 30;
var BeastHandler = class extends EventManager {
/**
* don't forget to call stop() once you are done
*/
constructor() {
super();
this.aircraftCache = /* @__PURE__ */ new Map();
this.cleanUpCacheInterval = setInterval(() => {
this.cleanUpCache();
}, 1e3 * 30);
}
stop() {
clearInterval(this.cleanUpCacheInterval);
this.aircraftCache.clear();
}
//TODO rewrite to be non-blocking
cleanUpCache() {
const now = Date.now();
for (const aircraftEntry of this.aircraftCache) {
const aircraft = aircraftEntry[1];
if (now - aircraft.lastDataReceivedTime > MAX_AIRCRAFT_CACHE_TIME) {
const icaoAddress = aircraftEntry[0];
this.aircraftCache.delete(icaoAddress);
this.dispatch("removed_from_cache", icaoAddress);
}
}
}
handleBeastData(data) {
const frames = parseBeastSocketData(data, true);
for (const frame of frames) {
this.handleBeastFrame(frame);
}
}
handleBeastFrame(frame) {
if (frame.type === "ADSB") {
try {
const parsedModeS = parseModeS(frame.dataBytes);
const parsedADSB = parseADSB(parsedModeS);
const myIcaoAddressHex = icaoNumberToHex(parsedModeS.icaoAddress);
const existingAircraft = this.aircraftCache.get(myIcaoAddressHex);
const aircraft = existingAircraft || {
lastDataReceivedTime: Date.now()
};
this.aircraftCache.set(myIcaoAddressHex, aircraft);
const decodedBasics = decodeBasics(parsedADSB);
if (decodedBasics.locationInformation.locationAvailable) {
if (decodedBasics.locationInformation.location.format === "even") {
aircraft.lastEncodedLocationEven = {
receivedTime: Date.now(),
//TODO shouldnt we use mlatTimestamp for this?
cpr: decodedBasics.locationInformation.location
};
} else {
aircraft.lastEncodedLocationOdd = {
receivedTime: Date.now(),
//TODO shouldnt we use mlatTimestamp for this?
cpr: decodedBasics.locationInformation.location
};
}
}
if (aircraft.lastEncodedLocationEven && aircraft.lastEncodedLocationOdd) {
if (Math.abs(aircraft.lastEncodedLocationEven.receivedTime - aircraft.lastEncodedLocationOdd.receivedTime) < 1e3 * 60 * 2) {
const olderMessage = aircraft.lastEncodedLocationEven.receivedTime < aircraft.lastEncodedLocationOdd.receivedTime ? aircraft.lastEncodedLocationEven.cpr : aircraft.lastEncodedLocationOdd.cpr;
const moreRecentMessage = aircraft.lastEncodedLocationEven.receivedTime < aircraft.lastEncodedLocationOdd.receivedTime ? aircraft.lastEncodedLocationOdd.cpr : aircraft.lastEncodedLocationEven.cpr;
const unambigiousLocation = calculateUnambigiousLocation([olderMessage, moreRecentMessage]);
aircraft.lastUnambigiousLocation = unambigiousLocation;
this.dispatch("aircraft_update_with_location", {
icaoAddress: myIcaoAddressHex,
location: unambigiousLocation
});
}
}
if (!existingAircraft) {
this.dispatch("aircraft_new", {
icaoAddress: myIcaoAddressHex
});
} else {
if (!aircraft.lastUnambigiousLocation) {
this.dispatch("aircraft_update", {
icaoAddress: myIcaoAddressHex
});
}
}
} catch (err) {
this.dispatch("warning", {
message: "Failed to decode adsb frame",
context: err
});
}
} else if (frame.type === "MODE_S") {
try {
const parsedModeS = parseModeS(frame.dataBytes);
const myIcaoAddressHex = icaoNumberToHex(parsedModeS.icaoAddress);
const existingAircraft = this.aircraftCache.get(myIcaoAddressHex);
const aircraft = existingAircraft || {
lastDataReceivedTime: Date.now()
};
this.aircraftCache.set(myIcaoAddressHex, aircraft);
if (existingAircraft) {
this.dispatch("aircraft_update", {
icaoAddress: myIcaoAddressHex
});
} else {
this.dispatch("aircraft_new", {
icaoAddress: myIcaoAddressHex
});
}
} catch (err) {
this.dispatch("warning", {
message: "Failed to decode mode s frame",
context: err
});
}
} else {
this.dispatch("warning", {
message: "Unsupported frame type",
context: frame.type
});
}
}
};
function icaoNumberToHex(n) {
return n.toString(16);
}
export {
BeastHandler
};
//# sourceMappingURL=index.mjs.map