From 944594632aa3ee6dfc1c920243aceb551f1b08c4 Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Fri, 23 Oct 2026 08:34:41 -0400 Subject: [PATCH 1/3] add label 76401 parsing --- lib/plugins/Label_H1_76401.test.ts | 85 ++++++++++++++++++++++++++++++ lib/plugins/Label_H1_76401.ts | 73 +++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 lib/plugins/Label_H1_76401.test.ts create mode 100644 lib/plugins/Label_H1_76401.ts diff --git a/lib/plugins/Label_H1_76401.test.ts b/lib/plugins/Label_H1_76401.test.ts new file mode 100644 index 0000000..eda27c4 --- /dev/null +++ b/lib/plugins/Label_H1_76401.test.ts @@ -0,0 +1,85 @@ +import { MessageDecoder } from '../MessageDecoder'; +import { Label_H1_76401 } from './Label_H1_76401'; + +describe('Label H1 Preamble 76401', () => { + let plugin: Label_H1_76401; + const message = { label: 'H1', text: '' }; + + beforeEach(() => { + const decoder = new MessageDecoder(); + plugin = new Label_H1_76401(decoder); + }); + test('matches qualifiers', () => { + expect(plugin.decode).toBeDefined(); + expect(plugin.name).toBe('label-h1-76401'); + expect(plugin.qualifiers).toBeDefined(); + expect(plugin.qualifiers()).toEqual({ + labels: ['H1'], + preambles: ['76401\r\n'], + }); + }); + + test('decodes variant 1', () => { + // https://app.airframes.io/messages/7903362642 + message.text = + '76401\r\n02E23KDJTKBWI\r\nN35551W07767100084099M050276030G1801230030YY,\r\n'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.departure_icao).toBe('KDJT'); + expect(decodeResult.raw.arrival_icao).toBe('KBWI'); + expect(decodeResult.raw.position?.latitude).toBe(35.551); + expect(decodeResult.raw.position?.longitude).toBe(-77.671); + expect(decodeResult.raw.message_timestamp).toBe(520); // 00:08:40 + expect(decodeResult.remaining.text).toBe( + '76401\r\n02E23\r\n99M050276030G1801230030YY,\r\n', + ); + }); + + test('decodes variant 2', () => { + // https://app.airframes.io/messages/7903363689 + message.text = + '76401\r\n02E23KBWIKBDL\r\nN39831W07515900082500M032334018B000025003005Z\r\n'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.departure_icao).toBe('KBWI'); + expect(decodeResult.raw.arrival_icao).toBe('KBDL'); + expect(decodeResult.raw.route?.waypoints?.length).toBe(1); + expect(decodeResult.raw.position?.latitude).toBe(39.831); + expect(decodeResult.raw.position?.longitude).toBe(-75.159); + expect(decodeResult.raw.message_timestamp).toBe(505); // 00:08:25 + expect(decodeResult.remaining.text).toBe( + '76401\r\n02E23\r\n00M032334018B000025003005Z\r\n', + ); + }); + + test('decodes multiple positions', () => { + // https://app.airframes.io/messages/7903359093 + message.text = + '76401\r\n02E23KPHXKBUR\r\nN34550W11842100030787M001223021G000025::Q030;\r\nN34499W11847100040641P003227022G000025::2HGMY\r\nN34443W11851500050596P004225020G000025::B0YY,\r\nN34386W11855900060596P004227017G000025::B0G'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.departure_icao).toBe('KPHX'); + expect(decodeResult.raw.arrival_icao).toBe('KBUR'); + expect(decodeResult.raw.route?.waypoints?.length).toBe(4); + expect(decodeResult.raw.position?.latitude).toBe(34.386); + expect(decodeResult.raw.position?.longitude).toBe(-118.559); + expect(decodeResult.raw.message_timestamp).toBe(365); // 00:06:05 + expect(decodeResult.remaining.text).toBe( + '76401\r\n02E23\r\n87M001223021G000025::Q030;\r\n41P003227022G000025::2HGMY\r\n96P004225020G000025::B0YY,\r\n96P004227017G000025::B0G', + ); + }); + test('does not decode invalid', () => { + message.text = '76401\r\n'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(false); + expect(decodeResult.decoder.decodeLevel).toBe('none'); + expect(decodeResult.formatted.description).toBe('Position Report'); + }); +}); diff --git a/lib/plugins/Label_H1_76401.ts b/lib/plugins/Label_H1_76401.ts new file mode 100644 index 0000000..6577c69 --- /dev/null +++ b/lib/plugins/Label_H1_76401.ts @@ -0,0 +1,73 @@ +import { DateTimeUtils } from '../DateTimeUtils'; +import { DecoderPlugin } from '../DecoderPlugin'; +import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; +import { Waypoint } from '../types/waypoint'; +import { CoordinateUtils } from '../utils/coordinate_utils'; +import { ResultFormatter } from '../utils/result_formatter'; + +export class Label_H1_76401 extends DecoderPlugin { + name = 'label-h1-76401'; + + qualifiers() { + return { + labels: ['H1'], + preambles: ['76401\r\n'], + }; + } + + decode(message: Message, options: Options = {}): DecodeResult { + const decodeResult = this.initResult(message, 'Position Report'); + + const lines = message.text.split('\r\n'); + + if (lines.length < 3) { + return decodeResult; + } + + ResultFormatter.unknown(decodeResult, lines[0], '\r\n'); + + ResultFormatter.unknown(decodeResult, lines[1].substring(0, 5), '\r\n'); + ResultFormatter.departureAirport(decodeResult, lines[1].substring(5, 9)); + ResultFormatter.arrivalAirport(decodeResult, lines[1].substring(9, 13)); + ResultFormatter.unknown(decodeResult, lines[1].substring(13), ''); // should have nothing left on line 1 + + const waypoints: Waypoint[] = []; + for (const line of lines.slice(2)) { + const position = CoordinateUtils.decodeStringCoordinates( + line.substring(0, 13), + ); + if (position) { + waypoints.push({ + name: CoordinateUtils.coordinateString(position), + latitude: position.latitude, + longitude: position.longitude, + time: DateTimeUtils.convertHHMMSSToTod(line.substring(13, 19)), + }); + } + ResultFormatter.unknown(decodeResult, line.substring(19), '\r\n'); + } + + ResultFormatter.route(decodeResult, { waypoints: waypoints }); + // add position from the last waypoint for fun mapping + if (waypoints.length > 0) { + const lastWaypoint = waypoints[waypoints.length - 1]; + // should be true, making typescript happy + if (lastWaypoint.latitude && lastWaypoint.longitude) { + ResultFormatter.position(decodeResult, { + latitude: lastWaypoint.latitude, + longitude: lastWaypoint.longitude, + }); + } + + // should be true, making typescript happy + if (lastWaypoint.time) { + ResultFormatter.timestamp(decodeResult, lastWaypoint.time); + } + } + + decodeResult.decoded = true; + decodeResult.decoder.decodeLevel = 'partial'; + + return decodeResult; + } +} From 3a43bdb16a3c2719dd001e6f2ef47d0bc39ce216 Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Fri, 23 Oct 2026 10:06:16 -0400 Subject: [PATCH 2/3] hook up --- lib/MessageDecoder.ts | 3 ++- lib/plugins/official.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/MessageDecoder.ts b/lib/MessageDecoder.ts index b3da16c..e7bfa1c 100644 --- a/lib/MessageDecoder.ts +++ b/lib/MessageDecoder.ts @@ -58,7 +58,7 @@ const pluginClasses = [ Plugins.Label_4T_AGFSR, Plugins.Label_4T_ETA, Plugins.Label_B6_Forwardslash, - Plugins.Label_H2_02E, + Plugins.Label_H1_76401, Plugins.Label_H1_ATIS, Plugins.Label_H1_EZF, Plugins.Label_H1_FLR, @@ -68,6 +68,7 @@ const pluginClasses = [ Plugins.Label_H1_Paren, Plugins.Label_H1_WRN, Plugins.Label_H1_StarPOS, + Plugins.Label_H2_02E, Plugins.Label_HX, Plugins.Label_58, Plugins.Label_80, diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index e6205e5..5671ede 100644 --- a/lib/plugins/official.ts +++ b/lib/plugins/official.ts @@ -49,7 +49,7 @@ export * from './Label_83'; export * from './Label_8E'; export * from './Label_B6'; export * from './Label_ColonComma'; -export * from './Label_H2_02E'; +export * from './Label_H1_76401' export * from './Label_H1_ATIS'; export * from './Label_H1_EZF'; export * from './Label_H1_FLR'; @@ -59,6 +59,7 @@ export * from './Label_H1_OFP'; export * from './Label_H1_Paren'; export * from './Label_H1_StarPOS'; export * from './Label_H1_WRN'; +export * from './Label_H2_02E'; export * from './Label_HX'; export * from './Label_MA'; export * from './Label_SQ'; From b75367ca470be57b2389366454eaf962a814cb3f Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Fri, 23 Oct 2026 17:19:53 -0400 Subject: [PATCH 3/3] pr feedback --- lib/plugins/Label_H1_76401.ts | 9 +++++++-- lib/plugins/official.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/plugins/Label_H1_76401.ts b/lib/plugins/Label_H1_76401.ts index 6577c69..0b5f866 100644 --- a/lib/plugins/Label_H1_76401.ts +++ b/lib/plugins/Label_H1_76401.ts @@ -43,6 +43,8 @@ export class Label_H1_76401 extends DecoderPlugin { longitude: position.longitude, time: DateTimeUtils.convertHHMMSSToTod(line.substring(13, 19)), }); + } else { + ResultFormatter.unknown(decodeResult, line.substring(0, 19), ''); } ResultFormatter.unknown(decodeResult, line.substring(19), '\r\n'); } @@ -52,7 +54,10 @@ export class Label_H1_76401 extends DecoderPlugin { if (waypoints.length > 0) { const lastWaypoint = waypoints[waypoints.length - 1]; // should be true, making typescript happy - if (lastWaypoint.latitude && lastWaypoint.longitude) { + if ( + lastWaypoint.latitude !== undefined && + lastWaypoint.longitude !== undefined + ) { ResultFormatter.position(decodeResult, { latitude: lastWaypoint.latitude, longitude: lastWaypoint.longitude, @@ -60,7 +65,7 @@ export class Label_H1_76401 extends DecoderPlugin { } // should be true, making typescript happy - if (lastWaypoint.time) { + if (lastWaypoint.time !== undefined) { ResultFormatter.timestamp(decodeResult, lastWaypoint.time); } } diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index 5671ede..d55b539 100644 --- a/lib/plugins/official.ts +++ b/lib/plugins/official.ts @@ -49,7 +49,7 @@ export * from './Label_83'; export * from './Label_8E'; export * from './Label_B6'; export * from './Label_ColonComma'; -export * from './Label_H1_76401' +export * from './Label_H1_76401'; export * from './Label_H1_ATIS'; export * from './Label_H1_EZF'; export * from './Label_H1_FLR';