From b59042b10dec5fc866898a25f0ba30ff133a9960 Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Sun, 8 Feb 2026 17:25:56 -0500 Subject: [PATCH 1/2] parsing slash messages --- lib/MessageDecoder.ts | 1 - lib/plugins/Label_H1.ts | 16 +++++- lib/plugins/Label_H1_Slash.test.ts | 22 ++------ lib/plugins/Label_H1_Slash.ts | 88 ------------------------------ lib/plugins/official.ts | 1 - lib/utils/h1_helper.ts | 61 +++++++++++++++++---- 6 files changed, 71 insertions(+), 118 deletions(-) delete mode 100644 lib/plugins/Label_H1_Slash.ts diff --git a/lib/MessageDecoder.ts b/lib/MessageDecoder.ts index 1b1199a..8179eb0 100644 --- a/lib/MessageDecoder.ts +++ b/lib/MessageDecoder.ts @@ -66,7 +66,6 @@ export class MessageDecoder { this.registerPlugin(new Plugins.Label_H1_OHMA(this)); this.registerPlugin(new Plugins.Label_H1_WRN(this)); this.registerPlugin(new Plugins.Label_H1(this)); - this.registerPlugin(new Plugins.Label_H1_Slash(this)); this.registerPlugin(new Plugins.Label_H1_StarPOS(this)); this.registerPlugin(new Plugins.Label_HX(this)); this.registerPlugin(new Plugins.Label_58(this)); diff --git a/lib/plugins/Label_H1.ts b/lib/plugins/Label_H1.ts index 67a3214..101a98e 100644 --- a/lib/plugins/Label_H1.ts +++ b/lib/plugins/Label_H1.ts @@ -19,7 +19,21 @@ export class Label_H1 extends DecoderPlugin { const msg = message.text.replace(/\n|\r/g, ''); const parts = msg.split('#'); let decoded = false; - if (parts.length === 1) { + if (msg.startsWith('/')) { + const headerData = msg.split('.'); + const decoded = H1Helper.decodeH1Message( + decodeResult, + headerData.slice(1).join('.'), + ); // skip up to # and then a little more + if (decoded) { + decodeResult.remaining.text = + headerData[0] + '.' + decodeResult.remaining.text; + decodeResult.decoded = decoded; + decodeResult.decoder.decodeLevel = 'partial'; + } + + return decodeResult; + } else if (parts.length === 1) { decoded = H1Helper.decodeH1Message(decodeResult, msg); } else if (parts.length == 2) { const offset = isNaN(parseInt(parts[1][1])) ? 3 : 4; diff --git a/lib/plugins/Label_H1_Slash.test.ts b/lib/plugins/Label_H1_Slash.test.ts index 8276160..4401260 100644 --- a/lib/plugins/Label_H1_Slash.test.ts +++ b/lib/plugins/Label_H1_Slash.test.ts @@ -1,23 +1,13 @@ import { MessageDecoder } from '../MessageDecoder'; -import { Label_H1_Slash } from './Label_H1_Slash'; +import { Label_H1 } from './Label_H1'; describe('Label H1 /', () => { - let plugin: Label_H1_Slash; + let plugin: Label_H1; const message = { label: 'H1', text: '' }; beforeEach(() => { const decoder = new MessageDecoder(); - plugin = new Label_H1_Slash(decoder); - }); - - test('matches qualifiers', () => { - expect(plugin.decode).toBeDefined(); - expect(plugin.name).toBe('label-h1-slash'); - expect(plugin.qualifiers).toBeDefined(); - expect(plugin.qualifiers()).toEqual({ - labels: ['H1'], - preambles: ['/'], - }); + plugin = new Label_H1(decoder); }); test('decodes variant 1', () => { @@ -45,7 +35,7 @@ describe('Label H1 /', () => { expect(decodeResult.formatted.items[3].value).toBe('-23 degrees'); expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); expect(decodeResult.formatted.items[4].value).toBe('0xe711'); - expect(decodeResult.remaining.text).toBe('27282,241,780,MANUAL,0,813'); + expect(decodeResult.remaining.text).toBe('/.27282,241,780,MANUAL,0,813'); }); test('decodes variant 2', () => { @@ -72,7 +62,7 @@ describe('Label H1 /', () => { expect(decodeResult.formatted.items[3].value).toBe('-45 degrees'); expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); expect(decodeResult.formatted.items[4].value).toBe('0x0721'); - expect(decodeResult.remaining.text).toBe('HDQDLUA,20967,194/GAHDQDLUA/CA'); + expect(decodeResult.remaining.text).toBe('/HDQDLUA.20967,194/GAHDQDLUA/CA'); }); test('decodes variant 3', () => { @@ -99,7 +89,7 @@ describe('Label H1 /', () => { expect(decodeResult.formatted.items[3].value).toBe('-56 degrees'); expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); expect(decodeResult.formatted.items[4].value).toBe('0x6763'); - expect(decodeResult.remaining.text).toBe('24739,127'); + expect(decodeResult.remaining.text).toBe('/.24739,127'); }); test('does not decode invalid', () => { diff --git a/lib/plugins/Label_H1_Slash.ts b/lib/plugins/Label_H1_Slash.ts deleted file mode 100644 index 03cd260..0000000 --- a/lib/plugins/Label_H1_Slash.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { DecoderPlugin } from '../DecoderPlugin'; -import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; -import { H1Helper } from '../utils/h1_helper'; -import { ResultFormatter } from '../utils/result_formatter'; - -export class Label_H1_Slash extends DecoderPlugin { - name = 'label-h1-slash'; - qualifiers() { - return { - labels: ['H1'], - preambles: ['/'], - }; - } - - decode(message: Message, options: Options = {}): DecodeResult { - let decodeResult = this.defaultResult(); - decodeResult.decoder.name = this.name; - decodeResult.formatted.description = 'Position Report'; - decodeResult.message = message; - - const checksum = message.text.slice(-4); - const data = message.text.slice(0, message.text.length - 4); - - const fields = data.split('/'); - - if (fields[0] !== '') { - ResultFormatter.unknown(decodeResult, message.text); - decodeResult.decoded = false; - decodeResult.decoder.decodeLevel = 'none'; - return decodeResult; - } - - const headerData = fields[1].split('.'); - ResultFormatter.unknown(decodeResult, headerData[0]); - if ( - headerData[1] === 'POS' && - fields[2].startsWith('TS') && - fields[2].length > 15 - ) { - // variant 3 hack - // rip out the timestamp and process the rest - H1Helper.processPosition( - decodeResult, - fields[2].substring(15).split(','), - ); - } else if (headerData[1] === 'POS') { - // do nothing - } else if (headerData[1].startsWith('POS')) { - H1Helper.processPosition( - decodeResult, - headerData[1].substring(3).split(','), - ); - } else { - ResultFormatter.unknown(decodeResult, headerData[1], '.'); - } - - for (let i = 2; i < fields.length; i++) { - const field = fields[i]; - if (field.startsWith('TS')) { - H1Helper.processTimeStamp( - decodeResult, - field.substring(2, 15).split(','), - ); - } else if (field.startsWith('PS')) { - H1Helper.processPS(decodeResult, field.substring(2).split(',')); - } else { - ResultFormatter.unknown(decodeResult, field, '/'); - } - } - - if (decodeResult.formatted.items.length === 0) { - if (options.debug) { - console.log(`Decoder: Unknown H1 message: ${message.text}`); - } - ResultFormatter.unknown(decodeResult, message.text); - decodeResult.decoded = false; - decodeResult.decoder.decodeLevel = 'none'; - return decodeResult; - } - - ResultFormatter.checksum(decodeResult, checksum); - decodeResult.decoded = true; - decodeResult.decoder.decodeLevel = !decodeResult.remaining.text - ? 'full' - : 'partial'; - return decodeResult; - } -} diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index ea07375..33ea930 100644 --- a/lib/plugins/official.ts +++ b/lib/plugins/official.ts @@ -52,7 +52,6 @@ export * from './Label_H1'; export * from './Label_H2_02E'; export * from './Label_H1_FLR'; export * from './Label_H1_OHMA'; -export * from './Label_H1_Slash'; export * from './Label_H1_StarPOS'; export * from './Label_H1_WRN'; export * from './Label_HX'; diff --git a/lib/utils/h1_helper.ts b/lib/utils/h1_helper.ts index 321a77d..1ce9c91 100644 --- a/lib/utils/h1_helper.ts +++ b/lib/utils/h1_helper.ts @@ -17,7 +17,6 @@ export class H1Helper { return false; } - const fields = data.split('/'); const canDecode = parseMessageType(decodeResult, fields[0]); if (!canDecode) { @@ -33,15 +32,27 @@ export class H1Helper { case 'AF': processAirField(decodeResult, data.split(',')); break; + case 'AK': + ResultFormatter.unknown(decodeResult, fields[i], '/'); + // processAK(decodeResult, data.split(',')); + break; case 'CG': processCenterOfGravity(decodeResult, data.split(',')); break; case 'DC': processDateCode(decodeResult, data.split(',')); break; + case 'DI': + ResultFormatter.unknown(decodeResult, fields[i], '/'); + // processDI(decodeResult, data.split(',')); + break; case 'DT': //processDestination? processDT(decodeResult, data.split(',')); break; + case 'DQ': + ResultFormatter.unknown(decodeResult, fields[i], '/'); + // processDQ(decodeResult, data.split(',')); + break; case 'ET': processETA(data, decodeResult, fields, i); break; @@ -54,6 +65,10 @@ export class H1Helper { case 'FX': ResultFormatter.freetext(decodeResult, data); break; + case 'GA': + ResultFormatter.unknown(decodeResult, fields[i], '/'); + // processGA(decodeResult, data.split(',')); + break; case 'ID': processIdentification(decodeResult, data.split(',')); break; @@ -77,6 +92,10 @@ export class H1Helper { case 'SN': decodeResult.raw.serial_number = data; break; + case 'SP': + ResultFormatter.unknown(decodeResult, fields[i], '/'); + // processSP(decodeResult, data.split(',')); + break; case 'TD': processTimeOfDeparture(decodeResult, data.split(',')); break; @@ -89,6 +108,10 @@ export class H1Helper { case 'WD': processWindData(decodeResult, data); break; + case 'WQ': + ResultFormatter.unknown(decodeResult, fields[i], '/'); + // processWindQuery(decodeResult, data); + break; default: ResultFormatter.unknown(decodeResult, fields[i], '/'); } @@ -151,7 +174,15 @@ export class H1Helper { } public static processTimeStamp(decodeResult: DecodeResult, data: string[]) { - let time = DateTimeUtils.convertDateTimeToEpoch(data[0], data[1]); + if (data.length > 2) { + const positionData = data.slice(1); + positionData[0] = positionData[0].substring(6); // strip time from position field + this.processPosition(decodeResult, positionData); + } + let time = DateTimeUtils.convertDateTimeToEpoch( + data[0], + data[1].substring(0, 6), + ); if (Number.isNaN(time)) { // convert DDMMYY to MMDDYY - TODO figure out a better way to determine @@ -294,19 +325,21 @@ function processCenterOfGravity(decodeResult: DecodeResult, data: string[]) { function parseMessageType( decodeResult: DecodeResult, - messageType: string, + messagePart: string, ): boolean { + const messageType = messagePart.split(',')[0]; if (messageType.startsWith('POS')) { - H1Helper.processPosition(decodeResult, messageType.substring(3).split(',')); + H1Helper.processPosition(decodeResult, messagePart.substring(3).split(',')); return processMessageType(decodeResult, 'POS'); - } else if (messageType.length === 13) { - if (processMessageType(decodeResult, messageType.substring(10))) { - ResultFormatter.unknown(decodeResult, messageType.substring(0, 4)); - ResultFormatter.flightNumber(decodeResult, messageType.slice(4, 10)); - return true; - } + } else if (messageType.length === 6) { + const part1 = processMessageType(decodeResult, messageType.substring(0, 3)); + const description = decodeResult.formatted.description; + const part2 = processMessageType(decodeResult, messageType.substring(3, 6)); + decodeResult.formatted.description = + description + ' for ' + decodeResult.formatted.description; + return part1 && part2; } - return processMessageType(decodeResult, messageType.substring(0, 3)); + return processMessageType(decodeResult, messageType); } function processMessageType(decodeResult: DecodeResult, type: string): boolean { @@ -324,6 +357,12 @@ function processMessageType(decodeResult: DecodeResult, type: string): boolean { decodeResult.formatted.description = 'Progress Report'; } else if (type === 'PWI') { decodeResult.formatted.description = 'Pilot Weather Information'; + } else if (type === 'REJ') { + decodeResult.formatted.description = 'Reject'; + } else if (type === 'REQ') { + decodeResult.formatted.description = 'Request'; + } else if (type === 'RES') { + decodeResult.formatted.description = 'Response'; } else { decodeResult.formatted.description = 'Unknown H1 Message'; return false; From fdcb3e5f8604fb72f0fcaebd6a7143c444bd4487 Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Mon, 9 Feb 2026 11:21:08 -0500 Subject: [PATCH 2/2] Add RES/REQ/REJ decoding --- lib/plugins/Label_H1_POS.test.ts | 16 +++++-- lib/plugins/Label_H1_REJ.test.ts | 66 +++++++++++++++++++++++++++ lib/plugins/Label_H1_REQ.test.ts | 71 ++++++++++++++++++++++++++++++ lib/plugins/Label_H1_RES.test.ts | 34 ++++++++++++++ lib/plugins/Label_H1_Slash.test.ts | 25 ++++------- lib/utils/flight_plan_utils.ts | 11 +++++ lib/utils/h1_helper.ts | 71 +++++++++++++++++++++--------- lib/utils/result_formatter.ts | 60 +++++++++++++++++++++++++ 8 files changed, 312 insertions(+), 42 deletions(-) create mode 100644 lib/plugins/Label_H1_REJ.test.ts create mode 100644 lib/plugins/Label_H1_REQ.test.ts create mode 100644 lib/plugins/Label_H1_RES.test.ts diff --git a/lib/plugins/Label_H1_POS.test.ts b/lib/plugins/Label_H1_POS.test.ts index 56eb685..6393b83 100644 --- a/lib/plugins/Label_H1_POS.test.ts +++ b/lib/plugins/Label_H1_POS.test.ts @@ -488,15 +488,23 @@ describe('Label_H1 POS', () => { ); }); - test('does not decode /.POS', () => { + test('decodes /.POS', () => { // https://app.airframes.io/messages/2500488708 message.text = '/.POS/TS100316,210324/PSS35333W058220,,100316,250,S37131W059150,101916,S39387W060377,M23,27282,241,780,MANUAL,0,813E711'; const decodeResult = plugin.decode(message); - expect(decodeResult.decoded).toBe(false); - expect(decodeResult.decoder.decodeLevel).toBe('none'); - expect(decodeResult.formatted.description).toBe('Unknown'); + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.formatted.description).toBe('Position Report'); + expect(decodeResult.raw.message_timestamp).toBe(1711015396); + expect(decodeResult.raw.position.latitude).toBeCloseTo(-35.555, 3); + expect(decodeResult.raw.position.longitude).toBeCloseTo(-58.367, 3); + expect(decodeResult.raw.altitude).toBe(25000); + expect(decodeResult.raw.route.waypoints.length).toBe(3); + expect(decodeResult.raw.checksum).toBe(0xe711); + expect(decodeResult.formatted.items.length).toBe(5); + expect(decodeResult.remaining.text).toBe('/.27282,241,780,MANUAL,0,813'); }); test('decodes duplicate data', () => { diff --git a/lib/plugins/Label_H1_REJ.test.ts b/lib/plugins/Label_H1_REJ.test.ts new file mode 100644 index 0000000..ec4555b --- /dev/null +++ b/lib/plugins/Label_H1_REJ.test.ts @@ -0,0 +1,66 @@ +import { MessageDecoder } from '../MessageDecoder'; +import { Label_H1 } from './Label_H1'; + +describe('Label H1 Preamble REJ', () => { + let plugin: Label_H1; + const message = { label: 'H1', text: '' }; + + beforeEach(() => { + const decoder = new MessageDecoder(); + plugin = new Label_H1(decoder); + }); + + test('decodes PWI variant 1', () => { + message.text = 'REJPWI,141147,A,70,MCRAY,97E16'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.checksum).toBe(0x7e16); + expect(decodeResult.formatted.items.length).toBe(1); + expect(decodeResult.remaining.text).toBe('141147,A,70,MCRAY,9'); + }); + + test('decodes PWI variant 2', () => { + message.text = + '/HDQDLUA.REJPWI,104916,053,,CB007,CLIMB.053,,CB007,CLIMB.053,,CB007,CLIMB.053,,CB007,CLIMB.053,,CB007,CRUISE/GAHDQDLUA/TS113028,070226FE61'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.ground_address).toBe('HDQDLUA'); + expect(decodeResult.raw.checksum).toBe(0xfe61); + expect(decodeResult.formatted.items.length).toBe(2); + expect(decodeResult.remaining.text).toBe( + '/HDQDLUA.104916,053,,CB007,CLIMB.053,,CB007,CLIMB.053,,CB007,CLIMB.053,,CB007,CLIMB.053,,CB007,CRUISE', + ); + }); + + test('decodes POS variant 1', () => { + message.text = + 'REJPOS,100719,130,219,RF005,N36089W076173/TS100719,090226E850'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.checksum).toBe(0xe850); + expect(decodeResult.formatted.items.length).toBe(1); + expect(decodeResult.remaining.text).toBe( + '100719,130,219,RF005,N36089W076173', + ); + }); + + test('decodes POS variant 2', () => { + message.text = + 'REJPOS,041509,130,219,RF005,SWANN.130,219,RF005,BROSS.130,219,RF005,MYFOO.130,219,RF005,YAHOO.130,219,RF005,4650N.130,219,RF005,4840N.130,219,RF005,5030N.130,219,RF005,5120NA55A'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.checksum).toBe(0xa55a); + expect(decodeResult.formatted.items.length).toBe(1); + expect(decodeResult.remaining.text).toBe( + '041509,130,219,RF005,SWANN.130,219,RF005,BROSS.130,219,RF005,MYFOO.130,219,RF005,YAHOO.130,219,RF005,4650N.130,219,RF005,4840N.130,219,RF005,5030N.130,219,RF005,5120N', + ); + }); +}); diff --git a/lib/plugins/Label_H1_REQ.test.ts b/lib/plugins/Label_H1_REQ.test.ts new file mode 100644 index 0000000..0e4f2fa --- /dev/null +++ b/lib/plugins/Label_H1_REQ.test.ts @@ -0,0 +1,71 @@ +import { MessageDecoder } from '../MessageDecoder'; +import { Label_H1 } from './Label_H1'; + +describe('Label H1 preamble REQ', () => { + let plugin: Label_H1; + const message = { label: 'H1', text: '' }; + + beforeEach(() => { + const decoder = new MessageDecoder(); + plugin = new Label_H1(decoder); + }); + + test('decodes PWI', () => { + message.text = + 'REQPWI/WQ320:GEQUE.HACKS.XUB.VHP.KK60K.KIVDE.KK60E.KOVJY.KD54Y.ACZES.KD48S.DVC.KITTN.KATTS.RUMPS.OAL.INYOE.DYAMD/DQ320/SPGEQUE77CE'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('full'); + expect(decodeResult.raw.requested_alts).toStrictEqual([32000]); + expect(decodeResult.raw.desired_alt).toBe(32000); + expect(decodeResult.raw.start_point).toBe('GEQUE'); + expect(decodeResult.raw.route.waypoints).toStrictEqual([ + { name: 'GEQUE' }, + { name: 'HACKS' }, + { name: 'XUB' }, + { name: 'VHP' }, + { name: 'KK60K' }, + { name: 'KIVDE' }, + { name: 'KK60E' }, + { name: 'KOVJY' }, + { name: 'KD54Y' }, + { name: 'ACZES' }, + { name: 'KD48S' }, + { name: 'DVC' }, + { name: 'KITTN' }, + { name: 'KATTS' }, + { name: 'RUMPS' }, + { name: 'OAL' }, + { name: 'INYOE' }, + { name: 'DYAMD' }, + ]); + expect(decodeResult.raw.checksum).toBe(0x77ce); + expect(decodeResult.formatted.items.length).toBe(5); + }); + + test('decodes POS', () => { + message.text = 'REQPOS037B'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('full'); + expect(decodeResult.raw.checksum).toBe(0x037b); + expect(decodeResult.formatted.items.length).toBe(1); + expect(decodeResult.remaining.text).toBeUndefined(); + }); + + test('decodes FPN', () => { + message.text = 'REQFPN/RN10001/RS:FP:Z5585 9736'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('full'); + expect(decodeResult.raw.route_number).toBe('10001'); + expect(decodeResult.raw.route_status).toBe('RS'); + expect(decodeResult.raw.flight_plan).toBe('Z5585'); + expect(decodeResult.raw.checksum).toBe(0x9736); + expect(decodeResult.formatted.items.length).toBe(4); + expect(decodeResult.remaining.text).toBeUndefined(); + }); +}); diff --git a/lib/plugins/Label_H1_RES.test.ts b/lib/plugins/Label_H1_RES.test.ts new file mode 100644 index 0000000..7df3a38 --- /dev/null +++ b/lib/plugins/Label_H1_RES.test.ts @@ -0,0 +1,34 @@ +import { MessageDecoder } from '../MessageDecoder'; +import { Label_H1 } from './Label_H1'; + +describe('Label H1 Preamble RES', () => { + let plugin: Label_H1; + const message = { label: 'H1', text: '' }; + + beforeEach(() => { + const decoder = new MessageDecoder(); + plugin = new Label_H1(decoder); + }); + + test('decodes PWI', () => { + message.text = 'RESPWI/AC,5B/TS140956,070226/DI140953,140956,140956128C'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.checksum).toBe(0x128c); + expect(decodeResult.formatted.items.length).toBe(1); + expect(decodeResult.remaining.text).toBe('AC,5B/DI140953,140956,140956'); + }); + + test('decodes POS', () => { + message.text = 'RESPOS/AK,0711909'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.raw.checksum).toBe(0x1909); + expect(decodeResult.formatted.items.length).toBe(1); + expect(decodeResult.remaining.text).toBe('AK,071'); + }); +}); diff --git a/lib/plugins/Label_H1_Slash.test.ts b/lib/plugins/Label_H1_Slash.test.ts index 4401260..77b8abd 100644 --- a/lib/plugins/Label_H1_Slash.test.ts +++ b/lib/plugins/Label_H1_Slash.test.ts @@ -47,22 +47,15 @@ describe('Label H1 /', () => { expect(decodeResult.decoder.decodeLevel).toBe('partial'); expect(decodeResult.formatted.description).toBe('Position Report'); expect(decodeResult.raw.message_timestamp).toBe(1731592673); - expect(decodeResult.formatted.items.length).toBe(5); - expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); - expect(decodeResult.formatted.items[0].value).toBe('38.553 N, 80.137 W'); - expect(decodeResult.formatted.items[1].label).toBe('Altitude'); - expect(decodeResult.formatted.items[1].value).toBe('32000 feet'); - expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); - expect(decodeResult.formatted.items[2].value).toBe( - 'RONZZ@13:57:53 > LEVII@14:04:54 > WISTA', - ); - expect(decodeResult.formatted.items[3].label).toBe( - 'Outside Air Temperature (C)', - ); - expect(decodeResult.formatted.items[3].value).toBe('-45 degrees'); - expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); - expect(decodeResult.formatted.items[4].value).toBe('0x0721'); - expect(decodeResult.remaining.text).toBe('/HDQDLUA.20967,194/GAHDQDLUA/CA'); + expect(decodeResult.raw.position.latitude).toBeCloseTo(38.553, 3); + expect(decodeResult.raw.position.longitude).toBeCloseTo(-80.137, 3); + expect(decodeResult.raw.route.waypoints.length).toBe(3); + expect(decodeResult.raw.altitude).toBe(32000); + expect(decodeResult.raw.outside_air_temperature).toBe(-45); + expect(decodeResult.raw.ground_address).toBe('HDQDLUA'); + expect(decodeResult.raw.checksum).toBe(0x0721); + expect(decodeResult.formatted.items.length).toBe(6); + expect(decodeResult.remaining.text).toBe('/HDQDLUA.20967,194/CA'); }); test('decodes variant 3', () => { diff --git a/lib/utils/flight_plan_utils.ts b/lib/utils/flight_plan_utils.ts index 7a15bda..5d2a1a9 100644 --- a/lib/utils/flight_plan_utils.ts +++ b/lib/utils/flight_plan_utils.ts @@ -43,6 +43,9 @@ export class FlightPlanUtils { case 'F': // First Waypoint addRoute(decodeResult, value); break; + case 'FP': + ResultFormatter.flightPlan(decodeResult, value.trim()); + break; case 'R': addRunway(decodeResult, value); break; @@ -100,6 +103,14 @@ export class FlightPlanUtils { label: 'Route Status', value: 'Route Mapped', }); + } else if (header.startsWith('RS')) { + decodeResult.raw.route_status = 'RS'; + decodeResult.formatted.items.push({ + type: 'status', + code: 'ROUTE_STATUS', + label: 'Route Status', + value: 'Route Saved', + }); } else { decodeResult.remaining.text += header; allKnownFields = false; diff --git a/lib/utils/h1_helper.ts b/lib/utils/h1_helper.ts index 1ce9c91..4200ed5 100644 --- a/lib/utils/h1_helper.ts +++ b/lib/utils/h1_helper.ts @@ -50,7 +50,10 @@ export class H1Helper { processDT(decodeResult, data.split(',')); break; case 'DQ': - ResultFormatter.unknown(decodeResult, fields[i], '/'); + ResultFormatter.desiredAltitude( + decodeResult, + parseInt(data, 10) * 100, + ); // processDQ(decodeResult, data.split(',')); break; case 'ET': @@ -66,8 +69,7 @@ export class H1Helper { ResultFormatter.freetext(decodeResult, data); break; case 'GA': - ResultFormatter.unknown(decodeResult, fields[i], '/'); - // processGA(decodeResult, data.split(',')); + ResultFormatter.groundAddress(decodeResult, data); break; case 'ID': processIdentification(decodeResult, data.split(',')); @@ -86,15 +88,18 @@ export class H1Helper { case 'RI': case 'RM': case 'RP': + case 'RS': // TODO - use key/data instead of whole message FlightPlanUtils.processFlightPlan(decodeResult, fields[i].split(':')); break; + case 'RN': + ResultFormatter.routeNumber(decodeResult, data); + break; case 'SN': decodeResult.raw.serial_number = data; break; case 'SP': - ResultFormatter.unknown(decodeResult, fields[i], '/'); - // processSP(decodeResult, data.split(',')); + ResultFormatter.startPoint(decodeResult, data); break; case 'TD': processTimeOfDeparture(decodeResult, data.split(',')); @@ -109,8 +114,7 @@ export class H1Helper { processWindData(decodeResult, data); break; case 'WQ': - ResultFormatter.unknown(decodeResult, fields[i], '/'); - // processWindQuery(decodeResult, data); + processWeatherQuery(decodeResult, data.split(':')); break; default: ResultFormatter.unknown(decodeResult, fields[i], '/'); @@ -127,13 +131,7 @@ export class H1Helper { data[0], ); if (position) { - decodeResult.raw.position = position; - decodeResult.formatted.items.push({ - type: 'aircraft_position', - code: 'POS', - label: 'Aircraft Position', - value: CoordinateUtils.coordinateString(position), - }); + ResultFormatter.position(decodeResult, position); } if (data.length === 9) { // variant 7 @@ -327,7 +325,8 @@ function parseMessageType( decodeResult: DecodeResult, messagePart: string, ): boolean { - const messageType = messagePart.split(',')[0]; + const messageParts = messagePart.split(','); + const messageType = messageParts[0]; if (messageType.startsWith('POS')) { H1Helper.processPosition(decodeResult, messagePart.substring(3).split(',')); return processMessageType(decodeResult, 'POS'); @@ -337,8 +336,22 @@ function parseMessageType( const part2 = processMessageType(decodeResult, messageType.substring(3, 6)); decodeResult.formatted.description = description + ' for ' + decodeResult.formatted.description; + if (messageParts.length > 1) { + // TODO handle REJPWI/REJPOS + ResultFormatter.unknown( + decodeResult, + messageParts.slice(1).join(','), + '/', + ); + } return part1 && part2; } + + if (messageParts.length > 1) { + // TODO handle + ResultFormatter.unknown(decodeResult, messageParts.slice(1).join(','), '/'); + } + return processMessageType(decodeResult, messageType); } @@ -387,6 +400,26 @@ function processDateCode(decodeResult: DecodeResult, data: string[]) { } } +function processWeatherQuery(decodeResult: DecodeResult, data: string[]) { + if (data.length !== 2) { + ResultFormatter.unknown(decodeResult, data.join(':'), 'WQ/'); + return; + } + + const alts = data[0].split('.'); + const route = data[1].split('.'); + + ResultFormatter.requestedAltitudes( + decodeResult, + alts.map((a) => parseInt(a, 10) * 100), + ); + + const waypoints = route.map((wp) => { + return { name: wp }; + }); + ResultFormatter.route(decodeResult, { waypoints: waypoints }); +} + function processRoute( decodeResult: DecodeResult, last: string, @@ -415,13 +448,7 @@ function processRoute( const thenWaypoint = RouteUtils.getWaypoint(then || '?'); const waypoints: Waypoint[] = [lastWaypoint, nextWaypoint, thenWaypoint]; - decodeResult.raw.route = { waypoints: waypoints }; - decodeResult.formatted.items.push({ - type: 'aircraft_route', - code: 'ROUTE', - label: 'Aircraft Route', - value: RouteUtils.routeToString(decodeResult.raw.route), - }); + ResultFormatter.route(decodeResult, { waypoints: waypoints }); } function processWindData(decodeResult: DecodeResult, message: string) { diff --git a/lib/utils/result_formatter.ts b/lib/utils/result_formatter.ts index 0425ccc..14f0fd7 100644 --- a/lib/utils/result_formatter.ts +++ b/lib/utils/result_formatter.ts @@ -543,6 +543,66 @@ export class ResultFormatter { }); } + static requestedAltitudes(decodeResult: DecodeResult, values: number[]) { + decodeResult.raw.requested_alts = values; + decodeResult.formatted.items.push({ + type: 'requested_altitudes', + code: 'REQ_ALTS', + label: 'Requested Altitudes', + value: `${decodeResult.raw.requested_alts.join(', ')}`, + }); + } + + static desiredAltitude(decodeResult: DecodeResult, value: number) { + decodeResult.raw.desired_alt = value; + decodeResult.formatted.items.push({ + type: 'desired_altitude', + code: 'DES_ALT', + label: 'Desired Altitude', + value: `${decodeResult.raw.desired_alt}`, + }); + } + + static startPoint(decodeResult: DecodeResult, value: string) { + decodeResult.raw.start_point = value; + decodeResult.formatted.items.push({ + type: 'start_point', + code: 'START', + label: 'Start Point', + value: `${decodeResult.raw.start_point}`, + }); + } + + static routeNumber(decodeResult: DecodeResult, value: string) { + decodeResult.raw.route_number = value; + decodeResult.formatted.items.push({ + type: 'route_number', + code: 'RTE_NUM', + label: 'Route Number', + value: `${decodeResult.raw.route_number}`, + }); + } + + static flightPlan(decodeResult: DecodeResult, value: string) { + decodeResult.raw.flight_plan = value; + decodeResult.formatted.items.push({ + type: 'flight_plan', + code: 'FPN', + label: 'Flight Plan', + value: `${decodeResult.raw.flight_plan}`, + }); + } + + static groundAddress(decodeResult: DecodeResult, value: string) { + decodeResult.raw.ground_address = value; + decodeResult.formatted.items.push({ + type: 'ground_address', + code: 'GND_ADDR', + label: 'Ground Address', + value: `${decodeResult.raw.ground_address}`, + }); + } + static unknown(decodeResult: DecodeResult, value: string, sep: string = ',') { if (!decodeResult.remaining.text) decodeResult.remaining.text = value; else decodeResult.remaining.text += sep + value;