UNPKG

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
// 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