From 4c02ccbe581a83419270f37da4268c03291ac647 Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Fri, 23 Jan 2026 16:55:41 -0500 Subject: [PATCH 1/2] Label 16 (2 (Z parsing --- lib/MessageDecoder.ts | 1 + lib/plugins/Label_16_Honeywell.test.ts | 71 +++++++++++++++++++++ lib/plugins/Label_16_Honeywell.ts | 85 ++++++++++++++++++++++++++ lib/plugins/official.ts | 1 + 4 files changed, 158 insertions(+) create mode 100644 lib/plugins/Label_16_Honeywell.test.ts create mode 100644 lib/plugins/Label_16_Honeywell.ts diff --git a/lib/MessageDecoder.ts b/lib/MessageDecoder.ts index 8ff6dbb..c7bbce0 100644 --- a/lib/MessageDecoder.ts +++ b/lib/MessageDecoder.ts @@ -23,6 +23,7 @@ export class MessageDecoder { this.registerPlugin(new Plugins.Label_13Through18_Slash(this)); this.registerPlugin(new Plugins.Label_15(this)); this.registerPlugin(new Plugins.Label_15_FST(this)); + this.registerPlugin(new Plugins.Label_16_Honeywell(this)); this.registerPlugin(new Plugins.Label_16_N_Space(this)); this.registerPlugin(new Plugins.Label_16_POSA1(this)); this.registerPlugin(new Plugins.Label_16_TOD(this)); diff --git a/lib/plugins/Label_16_Honeywell.test.ts b/lib/plugins/Label_16_Honeywell.test.ts new file mode 100644 index 0000000..8248950 --- /dev/null +++ b/lib/plugins/Label_16_Honeywell.test.ts @@ -0,0 +1,71 @@ +import { MessageDecoder } from "../MessageDecoder"; +import { Label_16_Honeywell } from "./Label_16_Honeywell"; + +describe("Label_16_Honeywell", () => { + let plugin: Label_16_Honeywell; + + beforeEach(() => { + const decoder = new MessageDecoder(); + plugin = new Label_16_Honeywell(decoder); + }); + + test("matches qualifiers", () => { + expect(plugin.decode).toBeDefined(); + expect(plugin.name).toBe("label-16-honeywell"); + expect(plugin.qualifiers).toBeDefined(); + expect(plugin.qualifiers()).toEqual({ + labels: ["16"], + preambles: ["(2"], + }); + }); + + test("decodes variant 1", () => { + const text = "(2AAABN39211W 77144KTEBMMTO-/A(Z"; + const decodeResult = plugin.decode({ text: text }); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe("partial"); + expect(decodeResult.raw.position.latitude).toBeCloseTo(39.352, 3); + expect(decodeResult.raw.position.longitude).toBeCloseTo(-77.24, 3); + expect(decodeResult.raw.departure_icao).toBe("KTEB"); + expect(decodeResult.raw.arrival_icao).toBe("MMTO"); + expect(decodeResult.formatted.items.length).toBe(3); + expect(decodeResult.remaining.text).toBe("AAAB/A"); + }); + + test("decodes variant 2", () => { + const text = "(2AAAAN37265W 78334-SSI /O(Z"; + const decodeResult = plugin.decode({ text: text }); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe("partial"); + expect(decodeResult.raw.position.latitude).toBeCloseTo(37.442, 3); + expect(decodeResult.raw.position.longitude).toBeCloseTo(-78.557, 3); + expect(decodeResult.raw.route.waypoints[0].name).toBe("SSI"); + expect(decodeResult.formatted.items.length).toBe(2); + expect(decodeResult.remaining.text).toBe("AAAA/O"); + }); + + test("decodes variant 3", () => { + const text = "(2AAABN37197W 78404-SLOJOGRONK/O(Z"; + const decodeResult = plugin.decode({ text: text }); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe("partial"); + expect(decodeResult.raw.position.latitude).toBeCloseTo(37.328, 3); + expect(decodeResult.raw.position.longitude).toBeCloseTo(-78.673, 3); + expect(decodeResult.raw.route.waypoints[0].name).toBe("SLOJO"); + expect(decodeResult.raw.route.waypoints[1].name).toBe("GRONK"); + expect(decodeResult.formatted.items.length).toBe(2); + expect(decodeResult.remaining.text).toBe("AAAB/O"); + }); + + test("does not decode ", () => { + const text = "(2 Bogus message"; + const decodeResult = plugin.decode({ text: text }); + + expect(decodeResult.decoded).toBe(false); + expect(decodeResult.decoder.decodeLevel).toBe("none"); + expect(decodeResult.message.text).toBe(text); + }); +}); diff --git a/lib/plugins/Label_16_Honeywell.ts b/lib/plugins/Label_16_Honeywell.ts new file mode 100644 index 0000000..4a91dcc --- /dev/null +++ b/lib/plugins/Label_16_Honeywell.ts @@ -0,0 +1,85 @@ +import { DecoderPlugin } from "../DecoderPlugin"; +import { DecodeResult, Message, Options } from "../DecoderPluginInterface"; +import { CoordinateUtils } from "../utils/coordinate_utils"; +import { ResultFormatter } from "../utils/result_formatter"; + +// Honeywell Label 16 Position Report +export class Label_16_Honeywell extends DecoderPlugin { + name = "label-16-honeywell"; + + qualifiers() { + // eslint-disable-line class-methods-use-this + return { + labels: ["16"], + preambles: ["(2"], + }; + } + + decode(message: Message, options: Options = {}): DecodeResult { + const decodeResult = this.defaultResult(); + decodeResult.decoder.name = this.name; + decodeResult.formatted.description = "Position Report"; + decodeResult.message = message; + + if (message.text.startsWith("(2") && message.text.endsWith("(Z")) { + const between = message.text.substring(2, message.text.length - 2); + ResultFormatter.unknown(decodeResult, between.substring(0, 4), ""); // Session ID + ResultFormatter.position( + decodeResult, + CoordinateUtils.decodeStringCoordinatesDecimalMinutes( + between.substring(4, 17), + ), + ); + if (between.charAt(17) === "-") { + // Waypoint mode + const waypoint1 = between.substring(18, 23).trim(); + const waypoint2 = between.substring(23, 28).trim(); + if (waypoint2) { + ResultFormatter.route(decodeResult, { + waypoints: [{ name: waypoint1 }, { name: waypoint2 }], + }); + } else { + ResultFormatter.route(decodeResult, { + waypoints: [{ name: waypoint1 }], + }); + } + } else { + // Route mode + ResultFormatter.departureAirport( + decodeResult, + between.substring(17, 21), + ); + ResultFormatter.arrivalAirport(decodeResult, between.substring(21, 25)); + //ignore the rest (should just be '-') + } + /* + const phases: { [key: string]: string } = { + '/O': 'En Route', + '/A': 'Arrival', + '/D': 'Departure', + '/P': 'Preflight', + '/C': 'Climb', + }; + */ + ResultFormatter.unknown( + decodeResult, + between.substring(between.length - 2), + "", + ); + } else { + if (options.debug) { + console.log(`Decoder: Unknown 16 Honeywell message: ${message.text}`); + } + ResultFormatter.unknown(decodeResult, message.text); + decodeResult.decoded = false; + decodeResult.decoder.decodeLevel = "none"; + return decodeResult; + } + + decodeResult.decoded = true; + decodeResult.decoder.decodeLevel = "partial"; + return decodeResult; + } +} + +export default {}; diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index cbd5e7a..1f1d654 100644 --- a/lib/plugins/official.ts +++ b/lib/plugins/official.ts @@ -7,6 +7,7 @@ export * from './Label_12_POS'; export * from './Label_13Through18_Slash'; export * from './Label_15'; export * from './Label_15_FST'; +export * from './Label_16_Honeywell'; export * from './Label_16_N_Space'; export * from './Label_16_POSA1'; export * from './Label_16_TOD'; From f915768b11311d776f6035953e8d97d03fb2c6eb Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Fri, 23 Jan 2026 19:28:47 -0500 Subject: [PATCH 2/2] bounds checking --- lib/plugins/Label_16_Honeywell.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugins/Label_16_Honeywell.ts b/lib/plugins/Label_16_Honeywell.ts index 4a91dcc..bd5bfaa 100644 --- a/lib/plugins/Label_16_Honeywell.ts +++ b/lib/plugins/Label_16_Honeywell.ts @@ -21,7 +21,7 @@ export class Label_16_Honeywell extends DecoderPlugin { decodeResult.formatted.description = "Position Report"; decodeResult.message = message; - if (message.text.startsWith("(2") && message.text.endsWith("(Z")) { + if (message.text.startsWith("(2") && message.text.endsWith("(Z") && message.text.length >= 29) { const between = message.text.substring(2, message.text.length - 2); ResultFormatter.unknown(decodeResult, between.substring(0, 4), ""); // Session ID ResultFormatter.position(