diff --git a/lib/MessageDecoder.test.ts b/lib/MessageDecoder.test.ts index aefb7ce..a679ed3 100644 --- a/lib/MessageDecoder.test.ts +++ b/lib/MessageDecoder.test.ts @@ -13,8 +13,8 @@ describe('MessageDecoder', () => { text: "T02!<<,:/k.E`;FOV@!'s.16q6R+p(RK,|D2ujNJhRah?_qrNftWiI-V,@*RQs,tn,FYN$/V1!gNIc6CO;$D,1:.4?dF952;>XP$\"B\"Ok-Fr'0^k?rP]3&UGoPX;\\ { expect(decodeResult.raw.text).toContain('A350,000354'); }); + test('C-band core seamless decode', () => { + const message = { + label: '4N', + text: 'M85AUP0109285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3', + }; + + const decodeResult = decoder.decode(message); + + expect(decodeResult.decoded).toBe(true); + if (!decodeResult.message) { + expect(decodeResult.message).toBeDefined(); + return; + } + expect(decodeResult.message.label).toBe('4N'); + expect(decodeResult.message.sublabel).toBeUndefined(); + expect(decodeResult.message.text).toContain('M85AUP0109285'); + expect(decodeResult.formatted.items.length).toBe(7); + }); + test('Handles Multiple decodes', () => { const message = { label: 'H1', @@ -33,6 +52,7 @@ describe('MessageDecoder', () => { }; decoder.decode(message); + const decodeResult = decoder.decode(message); if (!decodeResult.message) { expect(decodeResult.message).toBeDefined(); diff --git a/lib/MessageDecoder.ts b/lib/MessageDecoder.ts index 1b1199a..0d65f9c 100644 --- a/lib/MessageDecoder.ts +++ b/lib/MessageDecoder.ts @@ -16,6 +16,7 @@ export class MessageDecoder { this.plugins = []; this.debug = false; + this.registerPlugin(new Plugins.CBand(this)); // first, for now, so it can wrap other plugins this.registerPlugin(new Plugins.Label_ColonComma(this)); this.registerPlugin(new Plugins.Label_5Z_Slash(this)); this.registerPlugin(new Plugins.Label_10_LDR(this)); @@ -89,19 +90,19 @@ export class MessageDecoder { } decode(message: Message, options: Options = {}): DecodeResult { - // console.log('All plugins'); - // console.log(this.plugins); const usablePlugins = this.plugins.filter((plugin) => { const qualifiers = plugin.qualifiers(); - if (qualifiers.labels.includes(message.label)) { + if ( + qualifiers.labels.includes(message.label) || + (qualifiers.labels.length === 1 && qualifiers.labels[0] === '*') + ) { if (qualifiers.preambles && qualifiers.preambles.length > 0) { const matching = qualifiers.preambles.filter((preamble: string) => { // console.log(message.text.substring(0, preamble.length)); // console.log(preamble); return message.text.substring(0, preamble.length) === preamble; }); - // console.log(matching); return matching.length >= 1; } else { return true; diff --git a/lib/plugins/CBand.test.ts b/lib/plugins/CBand.test.ts new file mode 100644 index 0000000..16998bf --- /dev/null +++ b/lib/plugins/CBand.test.ts @@ -0,0 +1,106 @@ +import { MessageDecoder } from '../MessageDecoder'; +import { CBand } from './CBand'; + +describe('CBand', () => { + let plugin: CBand; + + beforeEach(() => { + const decoder = new MessageDecoder(); + plugin = new CBand(decoder); + }); + + test('matches qualifiers', () => { + expect(plugin.decode).toBeDefined(); + expect(plugin.name).toBe('c-band'); + expect(plugin.qualifiers).toBeDefined(); + expect(plugin.qualifiers()).toEqual({ + labels: ['*'], + }); + }); + + test('decodes Label 4A', () => { + // https://app.airframes.io/messages/3461407615 + const text = + 'M60ALH0752N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111'; + const decodeResult = plugin.decode({ label: '4A', text: text }); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.decoder.name).toBe('c-band-label-4a'); + expect(decodeResult.formatted.description).toBe('Latest New Format'); + expect(decodeResult.raw.flight_number).toBe('LH752'); + expect(decodeResult.raw.position.latitude).toBe(22.456); + expect(decodeResult.raw.position.longitude).toBe(77.014); + expect(decodeResult.raw.route.waypoints.length).toBe(2); + expect(decodeResult.raw.route.waypoints[0].name).toBe('OSE35'); + expect(decodeResult.raw.route.waypoints[1].name).toBe('VEX36'); + expect(decodeResult.raw.outside_air_temperature).toBe(-46); + expect(decodeResult.formatted.items.length).toBe(5); + expect(decodeResult.remaining.text).toBe('275043309,85220111'); + }); + + test('decodes Label 4N variant 2C (C-band)', () => { + // https://app.airframes.io/messages/3422221702 + const text = + 'M85AUP0109285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3'; + const decodeResult = plugin.decode({ label: '4N', text: text }); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.decoder.name).toBe('c-band-label-4n'); + expect(decodeResult.formatted.description).toBe('Airline Defined'); + expect(decodeResult.raw.flight_number).toBe('UP109'); + expect(decodeResult.raw.date).toBe('10/12'); + expect(decodeResult.raw.departure_icao).toBe('NRT'); + expect(decodeResult.raw.arrival_icao).toBe('ANC'); + expect(decodeResult.raw.alternate_icao).toBe('ANC'); + expect(decodeResult.raw.arrival_runway).toBe('07R'); + expect(decodeResult.raw.alternate_runway).toBe('33'); + expect(decodeResult.raw.checksum).toBe(30195); + expect(decodeResult.remaining.text).toBe( + 'C,0,0,0,0,0,0,1,0,0,0,709.8,048.7,758.5', + ); + expect(decodeResult.formatted.items.length).toBe(7); + }); + + test('decodes Label 83 variant 1 (C-band)', () => { + // https://app.airframes.io/messages/3413113024 + const text = + 'M05AUA0007KIAH,RJAA,110012, 39.12,-175.10,39001,265,-107.6, 64900'; + const decodeResult = plugin.decode({ label: '83', text: text }); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.decoder.name).toBe('c-band-label-83'); + expect(decodeResult.formatted.description).toBe('Airline Defined'); + expect(decodeResult.raw.flight_number).toBe('UA7'); + expect(decodeResult.raw.departure_icao).toBe('KIAH'); + expect(decodeResult.raw.arrival_icao).toBe('RJAA'); + expect(decodeResult.raw.day).toBe('11'); + expect(decodeResult.raw.position.latitude).toBe(39.12); + expect(decodeResult.raw.position.longitude).toBe(-175.1); + expect(decodeResult.raw.altitude).toBe(39001); + expect(decodeResult.raw.groundspeed).toBe(265); + expect(decodeResult.raw.heading).toBe(-107.6); + expect(decodeResult.remaining.text).toBe('64900'); + expect(decodeResult.formatted.items.length).toBe(7); + }); + + test('decodes Label 83 variant 3 (C-band)', () => { + // https://app.airframes.io/messages/3413346742 + const text = 'M09AXA0001001PR11013423N0556.6E11603.0000000----'; + const decodeResult = plugin.decode({ label: '83', text: text }); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.decoder.name).toBe('c-band-label-83'); + expect(decodeResult.formatted.description).toBe('Airline Defined'); + expect(decodeResult.raw.flight_number).toBe('XA1'); + expect(decodeResult.raw.day).toBe('11'); + expect(decodeResult.raw.position.latitude).toBe(5.943333333333333); + expect(decodeResult.raw.position.longitude).toBe(116.05); + expect(decodeResult.raw.altitude).toBe(0); + expect(decodeResult.remaining.text).toBe('0----'); + expect(decodeResult.formatted.items.length).toBe(3); + }); +}); diff --git a/lib/plugins/CBand.ts b/lib/plugins/CBand.ts new file mode 100644 index 0000000..8e2ec9c --- /dev/null +++ b/lib/plugins/CBand.ts @@ -0,0 +1,51 @@ +import { DecoderPlugin } from '../DecoderPlugin'; +import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; +import { MIAMCoreUtils } from '../utils/miam'; +import { ResultFormatter } from '../utils/result_formatter'; + +export class CBand extends DecoderPlugin { + name = 'c-band'; + qualifiers() { + return { + labels: ['*'], + }; + } + + decode(message: Message, options: Options = {}): DecodeResult { + let decodeResult = this.defaultResult(); + decodeResult.decoder.name = this.name; + decodeResult.message = message; + + // C-Band puts a 10 char header in front of some message types + // First 4 chars are some kind of message number + // Last 6 chars are the flight number + let cband = message.text.match( + /^(?[A-Z]\d{2}[A-Z])(?[A-Z0-9]{2})(?[0-9]{4})/, + ); + if (cband?.groups) { + const messageText = message.text.substring(10); + const decoded = this.decoder.decode( + { + label: message.label, + sublabel: message.sublabel, + text: messageText, + }, + options, + ); + if (decoded.decoded) { + ResultFormatter.flightNumber( + decodeResult, + cband.groups.airline + Number(cband.groups.number), + ); + decodeResult.decoded = true; + decodeResult.decoder.decodeLevel = decoded.decoder.decodeLevel; + decodeResult.decoder.name = this.name + '-' + decoded.decoder.name; + decodeResult.raw = { ...decodeResult.raw, ...decoded.raw }; + decodeResult.formatted.description = decoded.formatted.description; + decodeResult.formatted.items.push(...decoded.formatted.items); + decodeResult.remaining = decoded.remaining; + } + } + return decodeResult; + } +} diff --git a/lib/plugins/Label_4A.test.ts b/lib/plugins/Label_4A.test.ts index 02bb52d..9d8abd7 100644 --- a/lib/plugins/Label_4A.test.ts +++ b/lib/plugins/Label_4A.test.ts @@ -90,33 +90,6 @@ describe('Label 4A', () => { expect(decodeResult.formatted.items[3].value).toBe('4 degrees'); }); - test('decodes variant 2, C-Band', () => { - // https://app.airframes.io/messages/3461407615 - message.text = - 'M60ALH0752N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111'; - const decodeResult = plugin.decode(message); - - expect(decodeResult.decoded).toBe(true); - expect(decodeResult.decoder.decodeLevel).toBe('partial'); - expect(decodeResult.decoder.name).toBe('label-4a'); - expect(decodeResult.formatted.description).toBe('Latest New Format'); - expect(decodeResult.message).toBe(message); - expect(decodeResult.remaining.text).toBe('275043309,85220111'); - expect(decodeResult.formatted.items.length).toBe(5); - expect(decodeResult.formatted.items[0].code).toBe('FLIGHT'); - expect(decodeResult.formatted.items[0].value).toBe('LH752'); - expect(decodeResult.formatted.items[1].code).toBe('POS'); - expect(decodeResult.formatted.items[1].value).toBe('22.456 N, 77.014 E'); - expect(decodeResult.formatted.items[2].code).toBe('ALT'); - expect(decodeResult.formatted.items[2].value).toBe('37000 feet'); - expect(decodeResult.formatted.items[3].code).toBe('ROUTE'); - expect(decodeResult.formatted.items[3].value).toBe( - 'OSE35@19:20:27 > VEX36@19:23:16', - ); - expect(decodeResult.formatted.items[4].code).toBe('OATEMP'); - expect(decodeResult.formatted.items[4].value).toBe('-46 degrees'); - }); - test('decodes variant 3', () => { // https://globe.adsbexchange.com/?icao=A39AC6&showTrace=2024-09-22×tamp=1727009085 message.text = '124442,1320, 138,33467,N 41.093,W 72.677'; diff --git a/lib/plugins/Label_4A.ts b/lib/plugins/Label_4A.ts index 8add037..cd473d5 100644 --- a/lib/plugins/Label_4A.ts +++ b/lib/plugins/Label_4A.ts @@ -20,18 +20,8 @@ export class Label_4A extends DecoderPlugin { decodeResult.message = message; decodeResult.formatted.description = 'Latest New Format'; - // Inmarsat C-band seems to prefix normal messages with a message number and flight number - let text = message.text; - if (text.match(/^M\d{2}A\w{6}/)) { - ResultFormatter.flightNumber( - decodeResult, - message.text.substring(4, 10).replace(/^([A-Z]+)0*/g, '$1'), - ); - text = text.substring(10); - } - decodeResult.decoded = true; - const fields = text.split(','); + const fields = message.text.split(','); if (fields.length === 11) { // variant 1 ResultFormatter.time_of_day( @@ -42,10 +32,7 @@ export class Label_4A extends DecoderPlugin { if (fields[3]) ResultFormatter.callsign(decodeResult, fields[3]); ResultFormatter.departureAirport(decodeResult, fields[4]); ResultFormatter.arrivalAirport(decodeResult, fields[5]); - const alt = text.substring(48, 51); - if (alt !== '') { - ResultFormatter.altitude(decodeResult, Number(alt) * 100); - } + // ResultFormatter.altitude(decodeResult, Number(alt) * 100); ResultFormatter.unknownArr(decodeResult, fields.slice(8)); } else if (fields.length === 6) { if (fields[0].match(/^[NS]/)) { @@ -92,7 +79,7 @@ export class Label_4A extends DecoderPlugin { } } else { decodeResult.decoded = false; - ResultFormatter.unknown(decodeResult, text); + ResultFormatter.unknown(decodeResult, message.text); } if (decodeResult.decoded) { diff --git a/lib/plugins/Label_4N.test.ts b/lib/plugins/Label_4N.test.ts index 33fccd0..ca3f442 100644 --- a/lib/plugins/Label_4N.test.ts +++ b/lib/plugins/Label_4N.test.ts @@ -103,39 +103,6 @@ describe('Label 4N', () => { expect(decodeResult.formatted.items[4].value).toBe('0x9bcd'); }); - test('decodes Label 4N variant 2C (C-band)', () => { - // https://app.airframes.io/messages/3422221702 - message.text = - 'M85AUP0109285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3'; - const decodeResult = plugin.decode(message); - - expect(decodeResult.decoded).toBe(true); - expect(decodeResult.decoder.decodeLevel).toBe('partial'); - expect(decodeResult.decoder.name).toBe('label-4n'); - expect(decodeResult.formatted.description).toBe('Airline Defined'); - expect(decodeResult.message).toBe(message); - expect(decodeResult.raw.flight_number).toBe('UP109'); - expect(decodeResult.raw.date).toBe('10/12'); - expect(decodeResult.remaining.text).toBe( - 'C,0,0,0,0,0,0,1,0,0,0,709.8,048.7,758.5', - ); - expect(decodeResult.formatted.items.length).toBe(7); - expect(decodeResult.formatted.items[0].code).toBe('FLIGHT'); - expect(decodeResult.formatted.items[0].value).toBe('UP109'); - expect(decodeResult.formatted.items[1].code).toBe('ORG'); - expect(decodeResult.formatted.items[1].value).toBe('NRT'); - expect(decodeResult.formatted.items[2].code).toBe('DST'); - expect(decodeResult.formatted.items[2].value).toBe('ANC'); - expect(decodeResult.formatted.items[3].code).toBe('ALT_DST'); - expect(decodeResult.formatted.items[3].value).toBe('ANC'); - expect(decodeResult.formatted.items[4].code).toBe('ARWY'); - expect(decodeResult.formatted.items[4].value).toBe('07R'); - expect(decodeResult.formatted.items[5].code).toBe('ALT_ARWY'); - expect(decodeResult.formatted.items[5].value).toBe('33'); - expect(decodeResult.formatted.items[6].code).toBe('CHECKSUM'); - expect(decodeResult.formatted.items[6].value).toBe('0x75f3'); - }); - test('decodes Label 4N ', () => { message.text = '4N Bogus message'; const decodeResult = plugin.decode(message); diff --git a/lib/plugins/Label_4N.ts b/lib/plugins/Label_4N.ts index 6cb9f5d..3727147 100644 --- a/lib/plugins/Label_4N.ts +++ b/lib/plugins/Label_4N.ts @@ -18,17 +18,8 @@ export class Label_4N extends DecoderPlugin { decodeResult.message = message; decodeResult.formatted.description = 'Airline Defined'; - // Inmarsat C-band seems to prefix normal messages with a message number and flight number - let text = message.text; - if (text.match(/^M\d{2}A\w{6}/)) { - ResultFormatter.flightNumber( - decodeResult, - message.text.substring(4, 10).replace(/^([A-Z]+)0*/g, '$1'), - ); - text = text.substring(10); - } - decodeResult.decoded = true; + const text = message.text; const fields = text.split(','); if (text.length === 51) { // variant 1 diff --git a/lib/plugins/Label_83.test.ts b/lib/plugins/Label_83.test.ts index 3dee744..2711336 100644 --- a/lib/plugins/Label_83.test.ts +++ b/lib/plugins/Label_83.test.ts @@ -1,6 +1,5 @@ import { MessageDecoder } from '../MessageDecoder'; import { Label_83 } from './Label_83'; - describe('Label 83', () => { let plugin: Label_83; const message = { label: '83', text: '' }; @@ -49,44 +48,6 @@ describe('Label 83', () => { expect(decodeResult.formatted.items[5].value).toBe('140'); }); - test('decodes Label 83 variant 1 (C-band)', () => { - // https://app.airframes.io/messages/3413113024 - message.text = - 'M05AUA0007KIAH,RJAA,110012, 39.12,-175.10,39001,265,-107.6, 64900'; - const decodeResult = plugin.decode(message); - - expect(decodeResult.decoded).toBe(true); - expect(decodeResult.decoder.decodeLevel).toBe('partial'); - expect(decodeResult.decoder.name).toBe('label-83'); - expect(decodeResult.formatted.description).toBe('Airline Defined'); - expect(decodeResult.message).toBe(message); - expect(decodeResult.raw.flight_number).toBe('UA7'); - expect(decodeResult.raw.departure_icao).toBe('KIAH'); - expect(decodeResult.raw.arrival_icao).toBe('RJAA'); - expect(decodeResult.raw.day).toBe('11'); - expect(decodeResult.raw.position.latitude).toBe(39.12); - expect(decodeResult.raw.position.longitude).toBe(-175.1); - expect(decodeResult.raw.altitude).toBe(39001); - expect(decodeResult.raw.groundspeed).toBe(265); - expect(decodeResult.raw.heading).toBe(-107.6); - expect(decodeResult.remaining.text).toBe('64900'); - expect(decodeResult.formatted.items.length).toBe(7); - expect(decodeResult.formatted.items[0].type).toBe('flight_number'); - expect(decodeResult.formatted.items[0].value).toBe('UA7'); - expect(decodeResult.formatted.items[1].type).toBe('icao'); - expect(decodeResult.formatted.items[1].value).toBe('KIAH'); - expect(decodeResult.formatted.items[2].type).toBe('icao'); - expect(decodeResult.formatted.items[2].value).toBe('RJAA'); - expect(decodeResult.formatted.items[3].type).toBe('aircraft_position'); - expect(decodeResult.formatted.items[3].value).toBe('39.120 N, 175.100 W'); - expect(decodeResult.formatted.items[4].type).toBe('altitude'); - expect(decodeResult.formatted.items[4].value).toBe('39001 feet'); - expect(decodeResult.formatted.items[5].type).toBe('aircraft_groundspeed'); - expect(decodeResult.formatted.items[5].value).toBe('265 knots'); - expect(decodeResult.formatted.items[6].type).toBe('heading'); - expect(decodeResult.formatted.items[6].value).toBe('-107.6'); - }); - test('decodes Label 83 variant 2', () => { // https://globe.adsbexchange.com/?icao=478F43&showTrace=2024-09-22×tamp=1727022863 message.text = '4DH3 ETAT2 0907/22 ENGM/KEWR .LN-RKO\r\n/ETA 1641'; @@ -139,34 +100,6 @@ describe('Label 83', () => { expect(decodeResult.formatted.items[1].value).toBe('2925 feet'); }); - test('decodes Label 83 variant 3 (C-band)', () => { - const decoder = new MessageDecoder(); - const decoderPlugin = new Label_83(decoder); - - // https://app.airframes.io/messages/3413346742 - message.text = 'M09AXA0001001PR11013423N0556.6E11603.0000000----'; - const decodeResult = decoderPlugin.decode(message); - - expect(decodeResult.decoded).toBe(true); - expect(decodeResult.decoder.decodeLevel).toBe('partial'); - expect(decodeResult.decoder.name).toBe('label-83'); - expect(decodeResult.formatted.description).toBe('Airline Defined'); - expect(decodeResult.message).toBe(message); - expect(decodeResult.raw.flight_number).toBe('XA1'); - expect(decodeResult.raw.day).toBe('11'); - expect(decodeResult.raw.position.latitude).toBe(5.943333333333333); - expect(decodeResult.raw.position.longitude).toBe(116.05); - expect(decodeResult.raw.altitude).toBe(0); - expect(decodeResult.remaining.text).toBe('0----'); - expect(decodeResult.formatted.items.length).toBe(3); - expect(decodeResult.formatted.items[0].type).toBe('flight_number'); - expect(decodeResult.formatted.items[0].value).toBe('XA1'); - expect(decodeResult.formatted.items[1].type).toBe('aircraft_position'); - expect(decodeResult.formatted.items[1].value).toBe('5.943 N, 116.050 E'); - expect(decodeResult.formatted.items[2].type).toBe('altitude'); - expect(decodeResult.formatted.items[2].value).toBe('0 feet'); - }); - test('decodes Label 83 ', () => { message.text = '83 Bogus message'; const decodeResult = plugin.decode(message); diff --git a/lib/plugins/Label_83.ts b/lib/plugins/Label_83.ts index eb65c33..136b553 100644 --- a/lib/plugins/Label_83.ts +++ b/lib/plugins/Label_83.ts @@ -19,17 +19,8 @@ export class Label_83 extends DecoderPlugin { decodeResult.message = message; decodeResult.formatted.description = 'Airline Defined'; - // Inmarsat C-band seems to prefix normal messages with a message number and flight number - let text = message.text; - if (text.match(/^M\d{2}A\w{6}/)) { - ResultFormatter.flightNumber( - decodeResult, - message.text.substring(4, 10).replace(/^([A-Z]+)0*/g, '$1'), - ); - text = text.substring(10); - } - decodeResult.decoded = true; + const text = message.text; if (text.substring(0, 10) === '4DH3 ETAT2') { // variant 2 const fields = text.split(/\s+/); @@ -74,7 +65,7 @@ export class Label_83 extends DecoderPlugin { ResultFormatter.unknown(decodeResult, fields[8]); } else { decodeResult.decoded = false; - ResultFormatter.unknown(decodeResult, message.text); + ResultFormatter.unknown(decodeResult, text); } } diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index ea07375..1ce564a 100644 --- a/lib/plugins/official.ts +++ b/lib/plugins/official.ts @@ -1,3 +1,4 @@ +export * from './CBand'; export * from './Label_5Z_Slash'; export * from './Label_10_LDR'; export * from './Label_10_POS';