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/)
372 lines (365 loc) • 12.7 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
BeastHandler: () => BeastHandler
});
module.exports = __toCommonJS(index_exports);
// 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
var modeSAdsbParser = __toESM(require("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
var import_mode_s_adsb_parser = require("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 = (0, import_mode_s_adsb_parser.parseModeS)(frame.dataBytes);
const parsedADSB = (0, import_mode_s_adsb_parser.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 = (0, import_mode_s_adsb_parser.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 = (0, import_mode_s_adsb_parser.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 = (0, import_mode_s_adsb_parser.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);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BeastHandler
});
//# sourceMappingURL=index.js.map