diff --git a/lib/MessageDecoder.ts b/lib/MessageDecoder.ts index 640c5e8..f0bc8a3 100644 --- a/lib/MessageDecoder.ts +++ b/lib/MessageDecoder.ts @@ -53,6 +53,7 @@ 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_80(this)); diff --git a/lib/plugins/Label_H1_POS.test.ts b/lib/plugins/Label_H1_POS.test.ts index 9e78da9..b16a934 100644 --- a/lib/plugins/Label_H1_POS.test.ts +++ b/lib/plugins/Label_H1_POS.test.ts @@ -56,9 +56,8 @@ describe('Label_H1 POS', () => { expect(decodeResult.raw.position.latitude).toBe(45.348333333333336); expect(decodeResult.raw.position.longitude).toBe(-122.91666666666667); expect(decodeResult.raw.altitude).toBe(13400); - expect(decodeResult.raw.groundspeed).toBe(366); expect(decodeResult.raw.outside_air_temperature).toBe(-6); - expect(decodeResult.formatted.items.length).toBe(6); + expect(decodeResult.formatted.items.length).toBe(5); expect(decodeResult.formatted.items[0].type).toBe('aircraft_position'); expect(decodeResult.formatted.items[0].code).toBe('POS'); expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); @@ -75,12 +74,8 @@ describe('Label_H1 POS', () => { expect(decodeResult.formatted.items[3].code).toBe('OATEMP'); expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)'); expect(decodeResult.formatted.items[3].value).toBe('-6 degrees'); - expect(decodeResult.formatted.items[4].type).toBe('aircraft_groundspeed'); - expect(decodeResult.formatted.items[4].code).toBe('GSPD'); - expect(decodeResult.formatted.items[4].label).toBe('Aircraft Groundspeed'); - expect(decodeResult.formatted.items[4].value).toBe('366 knots'); - expect(decodeResult.formatted.items[5].label).toBe('Message Checksum'); - expect(decodeResult.formatted.items[5].value).toBe('0x0a5b'); + expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); + expect(decodeResult.formatted.items[4].value).toBe('0x0a5b'); }); test('variant 3', () => { @@ -428,32 +423,66 @@ describe('Label_H1 POS', () => { expect(decodeResult.remaining.text).toBe('290016,191/PR1496,150,370,191,,55,10,248028,M47,30,P19,P0/FHCIV,105208,273K,3226,175,M41,252027,450,N,221,62.MEDIL,105411,267K,3439,172,M44,250028,459,N,203,15.PITHI,105533,259K,3584,170,M47,249028,456,N,203,10.LESDO,105859,252K,3700,167,M47,248028,456,N,203,25.KOVIN,110153,252K,3700,164,M47,248028,456,N,203,21.DUCRA,110705,252K,3700,160,M47,248028,456,N,213,37.RESMI,111101,251K,3700,156,M47,248028,455,N,213,28.DEKOD,111325,251K,3700,154,M47,248028,455,N,192,17.DISAK,111438,251K,3700,153,M47,248028,454,N,172,9.DIRMO,112306,251K,3700,145,M47,248028,454,N,178,63.ETAMO,112514,250K,3700,143,M47,248028,453,N,158,16.ADEKA,113339,250K,3700,136,M47,248028,454,N,147,64.MOKDI,114139,251K,3700,129,M47,248028,454,N,181,59.MEN,114429,251K,3700,127,M47,248028,454,N,181,21.BADAM,114843,251K,3700,123,M47,248028,454,N,179,31.KANIG,120154,250K,3700,111,M47,248028,453,N,185,97.KENAS,121800,250K,3700,98,M47,248028,453,N,177,119.POS,122257,250K,3018,96,M45,248023,395,N,182,34.LEIB,124503,150K,2,89,P15,000000,161,N,231,103,LEIB,,89,124503,73'); }); - test('/.POS variant 2', () => { - + test('does not decode /.POS', () => { // https://app.airframes.io/messages/2500488708 const text = '/.POS/TS100316,210324/PSS35333W058220,,100316,250,S37131W059150,101916,S39387W060377,M23,27282,241,780,MANUAL,0,813E711'; const decodeResult = plugin.decode({ text: text }); + expect(decodeResult.decoded).toBe(false); + expect(decodeResult.decoder.decodeLevel).toBe('none'); + expect(decodeResult.formatted.description).toBe('Unknown H1 Message'); + }); + + test('decodes duplicate data', () => { + const text = 'POSN39220W078258,MRB18,044034,340,EYT19,044427,COL20,M49,27369,436,813/PSN39220W078258,MRB18,044034,340,EYT19,044427,COL20,M49,27369,436,813,ECON CRZ,0,25140035' + const decodeResult = plugin.decode({ text: text }); + expect(decodeResult.decoded).toBe(true); expect(decodeResult.decoder.decodeLevel).toBe('partial'); - expect(decodeResult.formatted.description).toBe('Unknown H1 Message'); - expect(decodeResult.raw.message_timestamp).toBe(1711015396); - expect(decodeResult.formatted.items.length).toBe(6); + expect(decodeResult.formatted.description).toBe('Position Report'); + + expect(decodeResult.formatted.items.length).toBe(9); + expect(decodeResult.formatted.items[0].type).toBe('aircraft_position'); + expect(decodeResult.formatted.items[0].code).toBe('POS'); expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); - expect(decodeResult.formatted.items[0].value).toBe('35.555 S, 58.367 W'); + expect(decodeResult.formatted.items[0].value).toBe('39.367 N, 78.430 W'); + expect(decodeResult.formatted.items[1].type).toBe('altitude'); + expect(decodeResult.formatted.items[1].code).toBe('ALT'); expect(decodeResult.formatted.items[1].label).toBe('Altitude'); - expect(decodeResult.formatted.items[1].value).toBe('25000 feet'); + expect(decodeResult.formatted.items[1].value).toBe('34000 feet'); + expect(decodeResult.formatted.items[2].type).toBe('aircraft_route'); + expect(decodeResult.formatted.items[2].code).toBe('ROUTE'); expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); - expect(decodeResult.formatted.items[2].value).toBe('(37.218 S, 59.250 W)@10:03:16 > (39.645 S, 60.628 W)@10:19:16 > ?'); + expect(decodeResult.formatted.items[2].value).toBe('MRB18@04:40:34 > EYT19@04:44:27 > COL20'); + expect(decodeResult.formatted.items[3].type).toBe('outside_air_temperature'); + expect(decodeResult.formatted.items[3].code).toBe('OATEMP'); expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)'); - expect(decodeResult.formatted.items[3].value).toBe('-23 degrees'); - expect(decodeResult.formatted.items[4].label).toBe('Aircraft Groundspeed'); - expect(decodeResult.formatted.items[4].value).toBe('780 knots'); - expect(decodeResult.formatted.items[5].label).toBe('Message Checksum'); - expect(decodeResult.formatted.items[5].value).toBe('0xe711'); - expect(decodeResult.remaining.text).toBe('.POS,,27282,241,MANUAL,0,813'); + expect(decodeResult.formatted.items[3].value).toBe('-49 degrees'); + // YAY DUPLICATES! + expect(decodeResult.formatted.items[4].type).toBe('aircraft_position'); + expect(decodeResult.formatted.items[4].code).toBe('POS'); + expect(decodeResult.formatted.items[4].label).toBe('Aircraft Position'); + expect(decodeResult.formatted.items[4].value).toBe('39.367 N, 78.430 W'); + expect(decodeResult.formatted.items[5].type).toBe('altitude'); + expect(decodeResult.formatted.items[5].code).toBe('ALT'); + expect(decodeResult.formatted.items[5].label).toBe('Altitude'); + expect(decodeResult.formatted.items[5].value).toBe('34000 feet'); + expect(decodeResult.formatted.items[6].type).toBe('aircraft_route'); + expect(decodeResult.formatted.items[6].code).toBe('ROUTE'); + expect(decodeResult.formatted.items[6].label).toBe('Aircraft Route'); + expect(decodeResult.formatted.items[6].value).toBe('MRB18@04:40:34 > EYT19@04:44:27 > COL20'); + expect(decodeResult.formatted.items[7].type).toBe('outside_air_temperature'); + expect(decodeResult.formatted.items[7].code).toBe('OATEMP'); + expect(decodeResult.formatted.items[7].label).toBe('Outside Air Temperature (C)'); + expect(decodeResult.formatted.items[7].value).toBe('-49 degrees'); + // And then the end + expect(decodeResult.formatted.items[8].label).toBe('Message Checksum'); + expect(decodeResult.formatted.items[8].value).toBe('0x0035'); + expect(decodeResult.remaining.text).toBe('27369,436,27369,436,813,ECON CRZ,0,2514'); + }); + test('decodes Label H1 Preamble #M1BPOS ', () => { const text = '#M1BPOS Bogus message'; diff --git a/lib/plugins/Label_H1_Slash.test.ts b/lib/plugins/Label_H1_Slash.test.ts new file mode 100644 index 0000000..4a80b25 --- /dev/null +++ b/lib/plugins/Label_H1_Slash.test.ts @@ -0,0 +1,99 @@ +import { decode } from 'punycode'; +import { MessageDecoder } from '../MessageDecoder'; +import { Label_H1_Slash } from './Label_H1_Slash'; + +describe('Label H1 /', () => { + let plugin: Label_H1_Slash; + + 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: ['/'], + }); + }); + + test('decodes variant 1', () => { + // https://app.airframes.io/messages/2500488708 + const text = '/.POS/TS100316,210324/PSS35333W058220,,100316,250,S37131W059150,101916,S39387W060377,M23,27282,241,780,MANUAL,0,813E711'; + const decodeResult = plugin.decode({ text: text }); + + 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.formatted.items.length).toBe(5); + expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); + expect(decodeResult.formatted.items[0].value).toBe('35.555 S, 58.367 W'); + expect(decodeResult.formatted.items[1].label).toBe('Altitude'); + expect(decodeResult.formatted.items[1].value).toBe('25000 feet'); + expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); + expect(decodeResult.formatted.items[2].value).toBe('@10:03:16 > (37.218 S, 59.250 W)@10:19:16 > (39.645 S, 60.628 W)'); + expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)'); + 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'); + }); + + test('decodes variant 2', () => { + const text = '/HDQDLUA.POSN38332W080082,RONZZ,135753,320,LEVII,140454,WISTA,M45,20967,194/GAHDQDLUA/CA/TS135753,1411240721'; + const decodeResult = plugin.decode({ text: text }); + + expect(decodeResult.decoded).toBe(true); + 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'); + }); + + test('decodes variant 3', () => { + const text = '/.POS/TS140122,141124N38321W078003,,140122,450,,140122,,M56,24739,127,8306763'; + const decodeResult = plugin.decode({ text: text }); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.formatted.description).toBe('Position Report'); + expect(decodeResult.raw.message_timestamp).toBe(1731592882); + expect(decodeResult.formatted.items.length).toBe(5); + expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); + expect(decodeResult.formatted.items[0].value).toBe('38.535 N, 78.005 W'); + expect(decodeResult.formatted.items[1].label).toBe('Altitude'); + expect(decodeResult.formatted.items[1].value).toBe('45000 feet'); + expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); + expect(decodeResult.formatted.items[2].value).toBe('@14:01:22 > @14:01:22 > ?'); + expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)'); + 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'); + }); + + test('does not decode invalid', () => { + + const text = '/.POS 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_H1_Slash.ts b/lib/plugins/Label_H1_Slash.ts new file mode 100644 index 0000000..58c7506 --- /dev/null +++ b/lib/plugins/Label_H1_Slash.ts @@ -0,0 +1,79 @@ +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 { H1Helper } from '../utils/h1_helper'; +import { ResultFormatter } from '../utils/result_formatter'; +import { RouteUtils } from '../utils/route_utils'; + +export class Label_H1_Slash extends DecoderPlugin { + name = 'label-h1-slash'; + qualifiers() { // eslint-disable-line class-methods-use-this + 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= 10) { // variant 1, short + ResultFormatter.altitude(decodeResult, Number(data[3]) * 100); + processRoute(decodeResult, data[1], data[2], data[4], data[5], data[6]); + ResultFormatter.temperature(decodeResult, data[7]); + ResultFormatter.unknown(decodeResult, data[8]); + ResultFormatter.unknown(decodeResult, data[9]); + } + if (data.length >= 14) { // variant 2,long + ResultFormatter.unknownArr(decodeResult, data.slice(10)); + } + } + + public static processTS(decodeResult: DecodeResult, data: string[]) { + let time = DateTimeUtils.convertDateTimeToEpoch(data[0], data[1]); + + if (Number.isNaN(time)) { // convert DDMMYY to MMDDYY - TODO figure out a better way to determine + const date = data[1].substring(2, 4) + data[1].substring(0, 2) + data[1].substring(4, 6); + time = DateTimeUtils.convertDateTimeToEpoch(data[0], date); + } + decodeResult.raw.message_date = data[1]; + decodeResult.raw.message_timestamp = time; + } } function processAirField(decodeResult: DecodeResult, data: string[]) { @@ -153,12 +204,12 @@ function processLR(decodeResult: DecodeResult, data: string[]) { }; -function parseMessageType(decodeResult: DecodeResult, messageType: string) { +function parseMessageType(decodeResult: DecodeResult, messageType: string): boolean { const parts = messageType.split('#'); if (parts.length == 1) { const type = parts[0].substring(0, 3); if (type === 'POS' && parts[0].length !== 3) { - processPosition(decodeResult, parts[0].substring(3).split(',')); + H1Helper.processPosition(decodeResult, parts[0].substring(3).split(',')); } return processMessageType(decodeResult, type); } else if (parts.length == 2) { @@ -170,16 +221,17 @@ function parseMessageType(decodeResult: DecodeResult, messageType: string) { // TODO - see if there's a better way to determine the type const type = parts[1].length == 5 ? parts[1].substring(2, 5) : parts[1].substring(3, 6); if (parts[1].substring(3, 6) === 'POS' && parts[1].length > 6) { - processPosition(decodeResult, parts[1].substring(6).split(',')); + H1Helper.processPosition(decodeResult, parts[1].substring(6).split(',')); } - processMessageType(decodeResult, type); + return processMessageType(decodeResult, type); } else { ResultFormatter.unknown(decodeResult, messageType); + return false; } } -function processMessageType(decodeResult: DecodeResult, type: string) { +function processMessageType(decodeResult: DecodeResult, type: string): boolean { if (type === 'FPN') { decodeResult.formatted.description = 'Flight Plan'; } else if (type === 'FTX') { @@ -192,7 +244,9 @@ function processMessageType(decodeResult: DecodeResult, type: string) { decodeResult.formatted.description = 'Progress Report'; } else { decodeResult.formatted.description = 'Unknown H1 Message'; + return false; } + return true; } function processDC(decodeResult: DecodeResult, data: string[]) { @@ -209,62 +263,6 @@ function processDC(decodeResult: DecodeResult, data: string[]) { } } -function processPS(decodeResult: DecodeResult, data: string[]) { - const position = CoordinateUtils.decodeStringCoordinatesDecimalMinutes(data[0]); - if (position) { - decodeResult.raw.position = position - decodeResult.formatted.items.push({ - type: 'aircraft_position', - code: 'POS', - label: 'Aircraft Position', - value: CoordinateUtils.coordinateString(position), - }); - } - if (data.length === 9) { // variant 7 - processRoute(decodeResult, data[3], data[1], data[5], data[4], undefined); - ResultFormatter.altitude(decodeResult, Number(data[2]) * 100); - ResultFormatter.temperature(decodeResult, data[6]); - ResultFormatter.unknown(decodeResult, data[7]); - ResultFormatter.unknown(decodeResult, data[8]); - } - if (data.length === 14) { // variant 2 - ResultFormatter.altitude(decodeResult, Number(data[3]) * 100); - processRoute(decodeResult, data[4], data[2], data[6], data[5], undefined); - ResultFormatter.temperature(decodeResult, data[7]); - ResultFormatter.groundspeed(decodeResult, Number(data[10])); - ResultFormatter.unknown(decodeResult, data[1]); - ResultFormatter.unknown(decodeResult, data[8]); - ResultFormatter.unknown(decodeResult, data[9]); - ResultFormatter.unknown(decodeResult, data.slice(11).join(',')); - } -} -function processPosition(decodeResult: DecodeResult, data: string[]) { - const position = CoordinateUtils.decodeStringCoordinatesDecimalMinutes(data[0]); - if (position) { - decodeResult.raw.position = position - decodeResult.formatted.items.push({ - type: 'aircraft_position', - code: 'POS', - label: 'Aircraft Position', - value: CoordinateUtils.coordinateString(position), - }); - } - if (data.length >= 10) { // variant 1, short - ResultFormatter.altitude(decodeResult, Number(data[3]) * 100); - processRoute(decodeResult, data[1], data[2], data[4], data[5], data[6]); - ResultFormatter.temperature(decodeResult, data[7]); - ResultFormatter.unknown(decodeResult, data[8]); - ResultFormatter.unknown(decodeResult, data[9]); - } - if (data.length >= 14) { // variant 2,long - ResultFormatter.groundspeed(decodeResult, Number(data[10])); - ResultFormatter.unknown(decodeResult, data[11]); - ResultFormatter.unknown(decodeResult, data[12]); - ResultFormatter.unknown(decodeResult, data[13]); - } -} - - function processRoute(decodeResult: DecodeResult, last: string, time: string, next: string, eta: string, then?: string, date?: string) { const lastTime = date ? DateTimeUtils.convertDateTimeToEpoch(time, date) : DateTimeUtils.convertHHMMSSToTod(time); const nextTime = date ? DateTimeUtils.convertDateTimeToEpoch(eta, date) : DateTimeUtils.convertHHMMSSToTod(eta);