From 2fb6482278dd3c1011a297d86c677188f8e1a1e7 Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Thu, 5 Sep 2024 11:17:45 -0400 Subject: [PATCH] additional POS parsing (#116) also parsing for 4J messages --- lib/plugins/Label_H1_POS.test.ts | 87 +++++++++++++------ lib/plugins/Label_H1_POS.ts | 6 +- lib/plugins/Label_H1_PRG.ts | 30 +------ lib/utils/flight_plan_utils.ts | 142 ++++++++++++++++--------------- 4 files changed, 142 insertions(+), 123 deletions(-) diff --git a/lib/plugins/Label_H1_POS.test.ts b/lib/plugins/Label_H1_POS.test.ts index 28f2164..aa50524 100644 --- a/lib/plugins/Label_H1_POS.test.ts +++ b/lib/plugins/Label_H1_POS.test.ts @@ -9,7 +9,7 @@ test('matches Label H1 Preamble POS qualifiers', () => { expect(decoderPlugin.name).toBe('label-h1-pos'); expect(decoderPlugin.qualifiers).toBeDefined(); expect(decoderPlugin.qualifiers()).toEqual({ - labels: ['H1'], + labels: ['H1', '4J'], preambles: ['POS', '#M1BPOS', '/.POS'], }); }); @@ -327,18 +327,21 @@ test('decodes Label H1 Preamble POS variant 7', () => { expect(decodeResult.decoder.decodeLevel).toBe('partial'); expect(decodeResult.decoder.name).toBe('label-h1-pos'); expect(decodeResult.formatted.description).toBe('Position Report'); - expect(decodeResult.formatted.items.length).toBe(5); - expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); - expect(decodeResult.formatted.items[0].value).toBe('39.277 N, 77.359 W'); - expect(decodeResult.formatted.items[1].label).toBe('Aircraft Route'); - expect(decodeResult.formatted.items[1].value).toBe('(39.300 N, 77.110 W)@2024-03-03T14:28:00Z > (38.560 N, 77.150 W)@2024-03-03T03:14:30Z > ?'); - expect(decodeResult.formatted.items[2].label).toBe('Altitude'); - expect(decodeResult.formatted.items[2].value).toBe('24000 feet'); - expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)'); - expect(decodeResult.formatted.items[3].value).toBe('-28'); - expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); - expect(decodeResult.formatted.items[4].value).toBe('0x9071'); - expect(decodeResult.remaining.text).toBe('/ID91459S,BANKR31,142813/MR64,0,/ET31539,27619,MT370/CG311,160,350/FB732/VR32'); + expect(decodeResult.raw.flight_number).toBe('BANKR31'); + expect(decodeResult.formatted.items.length).toBe(6); + expect(decodeResult.formatted.items[0].label).toBe('Tail'); + expect(decodeResult.formatted.items[0].value).toBe('91459S'); + expect(decodeResult.formatted.items[1].label).toBe('Aircraft Position'); + expect(decodeResult.formatted.items[1].value).toBe('39.277 N, 77.359 W'); + expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); + expect(decodeResult.formatted.items[2].value).toBe('(39.300 N, 77.110 W)@2024-03-03T14:28:00Z > (38.560 N, 77.150 W)@2024-03-03T03:14:30Z > ?'); + expect(decodeResult.formatted.items[3].label).toBe('Altitude'); + expect(decodeResult.formatted.items[3].value).toBe('24000 feet'); + expect(decodeResult.formatted.items[4].label).toBe('Outside Air Temperature (C)'); + expect(decodeResult.formatted.items[4].value).toBe('-28'); + expect(decodeResult.formatted.items[5].label).toBe('Message Checksum'); + expect(decodeResult.formatted.items[5].value).toBe('0x9071'); + expect(decodeResult.remaining.text).toBe(',142813/MR64,0,/ET31539,27619,MT370/CG311,160,350/FB732/VR32'); }); test('decodes Label H1 Preamble #M1BPOS variant 7', () => { @@ -354,19 +357,21 @@ test('decodes Label H1 Preamble #M1BPOS variant 7', () => { expect(decodeResult.decoder.decodeLevel).toBe('partial'); expect(decodeResult.decoder.name).toBe('label-h1-pos'); expect(decodeResult.formatted.description).toBe('Position Report'); - expect(decodeResult.raw.flight_number).toBe('AMCLL93'); - expect(decodeResult.formatted.items.length).toBe(5); - expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); - expect(decodeResult.formatted.items[0].value).toBe('42.579 N, 108.090 W'); - expect(decodeResult.formatted.items[1].label).toBe('Aircraft Route'); - expect(decodeResult.formatted.items[1].value).toBe('WAIDE@2024-03-03T17:32:07Z > WEDAK@2024-03-03T03:17:59Z > ?'); - expect(decodeResult.formatted.items[2].label).toBe('Altitude'); - expect(decodeResult.formatted.items[2].value).toBe('32000 feet'); - expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)'); - expect(decodeResult.formatted.items[3].value).toBe('-49'); - expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); - expect(decodeResult.formatted.items[4].value).toBe('0x4e17'); - expect(decodeResult.remaining.text).toBe('F37#M1B/ID746026,,173207/MR1,,/ET031846,267070,T468/CG264,110,360/FB742/VR32'); + expect(decodeResult.raw.flight_number).toBe(''); + expect(decodeResult.formatted.items.length).toBe(6); + expect(decodeResult.formatted.items[0].label).toBe('Tail'); + expect(decodeResult.formatted.items[0].value).toBe('746026'); + expect(decodeResult.formatted.items[1].label).toBe('Aircraft Position'); + expect(decodeResult.formatted.items[1].value).toBe('42.579 N, 108.090 W'); + expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); + expect(decodeResult.formatted.items[2].value).toBe('WAIDE@2024-03-03T17:32:07Z > WEDAK@2024-03-03T03:17:59Z > ?'); + expect(decodeResult.formatted.items[3].label).toBe('Altitude'); + expect(decodeResult.formatted.items[3].value).toBe('32000 feet'); + expect(decodeResult.formatted.items[4].label).toBe('Outside Air Temperature (C)'); + expect(decodeResult.formatted.items[4].value).toBe('-49'); + expect(decodeResult.formatted.items[5].label).toBe('Message Checksum'); + expect(decodeResult.formatted.items[5].value).toBe('0x4e17'); + expect(decodeResult.remaining.text).toBe('F37#M1B,173207/MR1,,/ET031846,267070,T468/CG264,110,360/FB742/VR32'); }); test('decodes Label H1 Preamble POS variant 8', () => { @@ -490,6 +495,36 @@ test('decodes Label H1 Preamble /.POS variant 2', () => { expect(decodeResult.remaining.text).toBe('/.POS,27282,241,MANUAL,0,813'); }); +test('decodes Label 4J Preamble POS variant 7', () => { + const decoder = new MessageDecoder(); + const decoderPlugin = new Label_H1_POS(decoder); + + // https://app.airframes.io/messages/3157551384 + const text = 'POS/ID91517S,WIDE21,7PZWTCP21222/DC09082024,140706/MR238,2/ET91456/PSN37375W077368,140700,300,JAXSN,091417,LOOEY,M26,21329,M080T490/CG293,160,350/FB583/VR32C696'; + const decodeResult = decoderPlugin.decode({ text: text }); + console.log(JSON.stringify(decodeResult, null, 2)); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.decoder.name).toBe('label-h1-pos'); + expect(decodeResult.formatted.description).toBe('Position Report'); + expect(decodeResult.raw.flight_number).toBe('WIDE21'); + expect(decodeResult.formatted.items.length).toBe(6); + expect(decodeResult.formatted.items[0].label).toBe('Tail'); + expect(decodeResult.formatted.items[0].value).toBe('91517S'); + expect(decodeResult.formatted.items[1].label).toBe('Aircraft Position'); + expect(decodeResult.formatted.items[1].value).toBe('37.375 N, 77.368 W'); + expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); + expect(decodeResult.formatted.items[2].value).toBe('JAXSN@14:07:00 > LOOEY@09:14:17 > ?'); + expect(decodeResult.formatted.items[3].label).toBe('Altitude'); + expect(decodeResult.formatted.items[3].value).toBe('30000 feet'); + expect(decodeResult.formatted.items[4].label).toBe('Outside Air Temperature (C)'); + expect(decodeResult.formatted.items[4].value).toBe('-26'); + expect(decodeResult.formatted.items[5].label).toBe('Message Checksum'); + expect(decodeResult.formatted.items[5].value).toBe('0xc696'); + expect(decodeResult.remaining.text).toBe(',7PZWTCP21222/DC09082024,140706/MR238,2,/ET91456,21329,M080T490/CG293,160,350/FB583/VR32'); +}); + test('decodes Label H1 Preamble #M1BPOS ', () => { const decoder = new MessageDecoder(); const decoderPlugin = new Label_H1_POS(decoder); diff --git a/lib/plugins/Label_H1_POS.ts b/lib/plugins/Label_H1_POS.ts index c565ba9..082608e 100644 --- a/lib/plugins/Label_H1_POS.ts +++ b/lib/plugins/Label_H1_POS.ts @@ -11,7 +11,7 @@ export class Label_H1_POS extends DecoderPlugin { qualifiers() { // eslint-disable-line class-methods-use-this return { - labels: ["H1"], + labels: ["H1", '4J'], preambles: ['POS', '#M1BPOS', '/.POS'], //TODO - support data before # }; } @@ -91,8 +91,8 @@ export class Label_H1_POS extends DecoderPlugin { decodeResult.decoded = true; decodeResult.decoder.decodeLevel = 'partial'; - } else if(parts.length === 15) { // variant 6 - processUnknown(decodeResult, parts[1]); + } else if(parts.length === 15) { // variant 7 + decodeResult.raw.flight_number = parts[1]; let date = undefined; if(parts[2].startsWith('/DC')) { date = parts[2].substring(3); diff --git a/lib/plugins/Label_H1_PRG.ts b/lib/plugins/Label_H1_PRG.ts index 4499c08..7adced7 100644 --- a/lib/plugins/Label_H1_PRG.ts +++ b/lib/plugins/Label_H1_PRG.ts @@ -27,7 +27,7 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca const data = message.text.substring(3, message.text.length-4); const fields = data.split(','); if(fields.length === 5) { - const allKnownFields = parseHeader(decodeResult, fields[0]); + const allKnownFields = FlightPlanUtils.parseHeader(decodeResult, fields[0]); processRunway(decodeResult, fields[1]); processCurrentFuel(decodeResult, fields[2]); processETA(decodeResult, fields[3]); @@ -37,7 +37,7 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca decodeResult.decoded = true; decodeResult.decoder.decodeLevel = allKnownFields ? 'full' : 'partial'; } else if(fields.length === 6) { - const allKnownFields = parseHeader(decodeResult, fields[0]); + const allKnownFields = FlightPlanUtils.parseHeader(decodeResult, fields[0]); processRunway(decodeResult, fields[1]); processCurrentFuel(decodeResult, fields[2]); processETA(decodeResult, fields[3]); @@ -47,7 +47,7 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca decodeResult.decoded = true; decodeResult.decoder.decodeLevel = allKnownFields ? 'full' : 'partial'; } else if(fields.length === 19) { - const allKnownFields = parseHeader(decodeResult, fields[0]); + const allKnownFields = FlightPlanUtils.parseHeader(decodeResult, fields[0]); processUnknown(decodeResult, fields[1]); processFlightNumber(decodeResult, fields[2]); processDeptApt(decodeResult, fields[3]); @@ -59,7 +59,7 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca decodeResult.decoded = true; decodeResult.decoder.decodeLevel = 'partial'; } else if(fields.length === 21) { - const allKnownFields = parseHeader(decodeResult, fields[0]); + const allKnownFields = FlightPlanUtils.parseHeader(decodeResult, fields[0]); processRunway(decodeResult, fields[1]); processUnknown(decodeResult, fields.slice(2, 21).join(',')); FlightPlanUtils.processFlightPlan(decodeResult, fields[21].split(':')); @@ -83,28 +83,6 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca export default {}; -function parseHeader(decodeResult: DecodeResult, header: string): boolean { - let allKnownFields = true; - const fields = header.split('/'); - // fields[0] is msg type - we already know this - for(let i=1; i 2) { + addRoute(decodeResult, fields[i].substring(2)); + } + } else if (fields[i] == 'RP') { + decodeResult.raw.route_status = 'RP'; + decodeResult.formatted.items.push({ + type: 'status', + code: 'ROUTE_STATUS', + label: 'Route Status', + value: 'Route Planned', + }); + decodeResult.raw.route_status = fields[i]; + } else if (fields[i] == 'RI') { + decodeResult.raw.route_status = 'RI'; + decodeResult.formatted.items.push({ + type: 'status', + code: 'ROUTE_STATUS', + label: 'Route Status', + value: 'Route Inactive', + }); + } else { + decodeResult.remaining.text += '/' + fields[i]; + allKnownFields = false + } + } + return allKnownFields; + }; } function parseMessageType(decodeResult: any, messageType: string): boolean { @@ -84,73 +157,6 @@ function parseMessageType(decodeResult: any, messageType: string): boolean { return false; } -function parseHeader(decodeResult: any, header: string): boolean { - let allKnownFields = true; - const fields = header.split('/'); - allKnownFields = allKnownFields && parseMessageType(decodeResult, fields[0]); - for (let i = 1; i < fields.length; ++i) { - if (fields[i].startsWith('FN')) { - decodeResult.raw.flight_number = fields[i].substring(2); // Strip off 'FN' - } else if (fields[i].startsWith('SN')) { - decodeResult.raw.serial_number = fields[i].substring(2); // Strip off 'SN' - } else if (fields[i].startsWith('TS')) { - const ts = fields[i].substring(2).split(','); // Strip off PS - let time = DateTimeUtils.convertDateTimeToEpoch(ts[0], ts[1]); - - if(Number.isNaN(time)) { // convert DDMMYY to MMDDYY - TODO figure out a better way to determine - const date = ts[1].substring(2,4) + ts[1].substring(0,2) + ts[1].substring(4,6); - time = DateTimeUtils.convertDateTimeToEpoch(ts[0], date); - } - decodeResult.raw.message_timestamp = time; - } else if (fields[i].startsWith('PS')) { - const pos = fields[i].substring(2); // Strip off PS - allKnownFields == allKnownFields && processPosition(decodeResult, pos); - } else if (fields[i].startsWith('DT')) { - const icao = fields[i].substring(2); // Strip off DT - decodeResult.raw.arrival_icao = icao; - decodeResult.formatted.items.push({ - type: 'destination', - code: 'DST', - label: 'Destination', - value: decodeResult.raw.arrival_icao, - }); - } else if (fields[i].startsWith('RF')) { - decodeResult.formatted.items.push({ - type: 'status', - code: 'ROUTE_STATUS', - label: 'Route Status', - value: 'Route Filed', - }); - decodeResult.raw.route_status = 'RF'; - if (fields[i].length > 2) { - addRoute(decodeResult, fields[i].substring(2)); - } - } else if (fields[i] == 'RP') { - decodeResult.raw.route_status = 'RP'; - decodeResult.formatted.items.push({ - type: 'status', - code: 'ROUTE_STATUS', - label: 'Route Status', - value: 'Route Planned', - }); - decodeResult.raw.route_status = fields[i]; - } else if (fields[i] == 'RI') { - decodeResult.raw.route_status = 'RI'; - decodeResult.formatted.items.push({ - type: 'status', - code: 'ROUTE_STATUS', - label: 'Route Status', - value: 'Route Inactive', - }); - } else { - decodeResult.remaining.text += '/' + fields[i]; - allKnownFields = false - } - } - return allKnownFields; -}; - - function processPosition(decodeResult: any, value: string): boolean { const position = CoordinateUtils.decodeStringCoordinates(value); if (position) {