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/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..0b5f866 --- /dev/null +++ b/lib/plugins/Label_H1_76401.ts @@ -0,0 +1,78 @@ +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)), + }); + } else { + ResultFormatter.unknown(decodeResult, line.substring(0, 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 !== undefined && + lastWaypoint.longitude !== undefined + ) { + ResultFormatter.position(decodeResult, { + latitude: lastWaypoint.latitude, + longitude: lastWaypoint.longitude, + }); + } + + // should be true, making typescript happy + if (lastWaypoint.time !== undefined) { + ResultFormatter.timestamp(decodeResult, lastWaypoint.time); + } + } + + decodeResult.decoded = true; + decodeResult.decoder.decodeLevel = 'partial'; + + return decodeResult; + } +} diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index e6205e5..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_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';