Skip to content

Commit

Permalink
Label 4a (#169)
Browse files Browse the repository at this point in the history
* non-4A updates

* add Label 4A

* add degrees to OATEMP formatting

* fixes

* make 4A invalid test case a real DIS01 message
  • Loading branch information
rpatel3001 authored Oct 24, 2024
1 parent c440fd4 commit 463b3fd
Show file tree
Hide file tree
Showing 30 changed files with 810 additions and 80 deletions.
5 changes: 5 additions & 0 deletions lib/MessageDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export class MessageDecoder {
this.registerPlugin(new Plugins.Label_44_OFF(this));
this.registerPlugin(new Plugins.Label_44_ON(this));
this.registerPlugin(new Plugins.Label_44_POS(this));
this.registerPlugin(new Plugins.Label_4A(this));
this.registerPlugin(new Plugins.Label_4A_01(this));
this.registerPlugin(new Plugins.Label_4A_DIS(this));
this.registerPlugin(new Plugins.Label_4A_DOOR(this));
this.registerPlugin(new Plugins.Label_4A_Slash_01(this));
this.registerPlugin(new Plugins.Label_4N(this));
this.registerPlugin(new Plugins.Label_B6_Forwardslash(this));
this.registerPlugin(new Plugins.Label_H1_FLR(this));
Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/Label_21_POS.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('Label_21_POS', () => {
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('4');
expect(decodeResult.formatted.items[3].value).toBe('-4 degrees');
expect(decodeResult.formatted.items[4].type).toBe('time_of_day');
expect(decodeResult.formatted.items[4].code).toBe('ETA');
expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival');
Expand All @@ -64,4 +64,4 @@ describe('Label_21_POS', () => {
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.message.text).toBe(text);
});
});
});
2 changes: 1 addition & 1 deletion lib/plugins/Label_21_POS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class Label_21_POS extends DecoderPlugin {
processPosition(decodeResult, fields[0].trim());
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[2]));
ResultFormatter.altitude( decodeResult, Number(fields[3]));
ResultFormatter.temperature(decodeResult, fields[6]);
ResultFormatter.temperature(decodeResult, fields[6].replace(/ /g, ""));
ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[7]));
ResultFormatter.arrivalAirport(decodeResult, fields[8]);

Expand Down
18 changes: 10 additions & 8 deletions lib/plugins/Label_24_Slash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ describe('Label_24_Slash', () => {
expect(decodeResult.raw.position.latitude).toBe(53.13);
expect(decodeResult.raw.position.longitude).toBe(1.33);
expect(decodeResult.raw.eta_time).toBe(39360);
expect(decodeResult.formatted.items.length).toBe(3);
expect(decodeResult.formatted.items[0].label).toBe('Altitude');
expect(decodeResult.formatted.items[0].value).toBe('34962 feet');
expect(decodeResult.formatted.items[1].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[1].value).toBe('53.130 N, 1.330 E');
expect(decodeResult.formatted.items[2].label).toBe('Estimated Time of Arrival');
expect(decodeResult.formatted.items[2].value).toBe('10:56:00');
expect(decodeResult.formatted.items.length).toBe(4);
expect(decodeResult.formatted.items[0].label).toBe('Flight Number');
expect(decodeResult.formatted.items[0].value).toBe('04WM');
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
expect(decodeResult.formatted.items[1].value).toBe('34962 feet');
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[2].value).toBe('53.130 N, 1.330 E');
expect(decodeResult.formatted.items[3].label).toBe('Estimated Time of Arrival');
expect(decodeResult.formatted.items[3].value).toBe('10:56:00');

expect(decodeResult.remaining.text).toBe('3374');
});
Expand All @@ -52,4 +54,4 @@ describe('Label_24_Slash', () => {
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.message.text).toBe(text);
});
});
});
158 changes: 158 additions & 0 deletions lib/plugins/Label_4A.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { MessageDecoder } from '../MessageDecoder';
import { Label_4A } from './Label_4A';

test('matches Label 4A qualifiers', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A(decoder);

expect(decoderPlugin.decode).toBeDefined();
expect(decoderPlugin.name).toBe('label-4a');
expect(decoderPlugin.qualifiers).toBeDefined();
expect(decoderPlugin.qualifiers()).toEqual({
labels: ['4A'],
});
});

test('decodes Label 4A, variant 1', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A(decoder);

// https://app.airframes.io/messages/3451492279
const text = '063200,1910,.N343FR,FFT2028,KSLC,KORD,1,0632,RT0,LT0,';
const decodeResult = decoderPlugin.decode({ text: text });

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.text).toBe(text);
expect(decodeResult.remaining.text).toBe('RT0,LT0,');
expect(decodeResult.formatted.items.length).toBe(6);
expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD');
expect(decodeResult.formatted.items[0].value).toBe('06:32:00');
expect(decodeResult.formatted.items[1].code).toBe('TAIL');
expect(decodeResult.formatted.items[1].value).toBe('N343FR');
expect(decodeResult.formatted.items[2].code).toBe('CALLSIGN');
expect(decodeResult.formatted.items[2].value).toBe('FFT2028');
expect(decodeResult.formatted.items[3].code).toBe('ORG');
expect(decodeResult.formatted.items[3].value).toBe('KSLC');
expect(decodeResult.formatted.items[4].code).toBe('DST');
expect(decodeResult.formatted.items[4].value).toBe('KORD');
});

test('decodes Label 4A, variant 1, no callsign', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A(decoder);

// https://app.airframes.io/messages/3452310240
const text = '101606,1910,.N317FR,,KMDW,----,1,1016,RT0,LT1,';
const decodeResult = decoderPlugin.decode({ text: text });

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.text).toBe(text);
expect(decodeResult.remaining.text).toBe('RT0,LT1,');
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD');
expect(decodeResult.formatted.items[0].value).toBe('10:16:06');
expect(decodeResult.formatted.items[1].code).toBe('TAIL');
expect(decodeResult.formatted.items[1].value).toBe('N317FR');
expect(decodeResult.formatted.items[2].code).toBe('ORG');
expect(decodeResult.formatted.items[2].value).toBe('KMDW');
expect(decodeResult.formatted.items[3].code).toBe('DST');
expect(decodeResult.formatted.items[3].value).toBe('----');
});

test('decodes Label 4A, variant 2', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A(decoder);

// https://app.airframes.io/messages/3461807403
const text = 'N45129W093113MSP/07 ,204436123VECTORS,,P04,268044858,46904221';
const decodeResult = decoderPlugin.decode({ text: text });

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.text).toBe(text);
expect(decodeResult.remaining.text).toBe('268044858,46904221');
expect(decodeResult.formatted.items.length).toBe(4);
expect(decodeResult.formatted.items[0].code).toBe('POS');
expect(decodeResult.formatted.items[0].value).toBe('45.129 N, 93.113 W');
expect(decodeResult.formatted.items[1].code).toBe('ALT');
expect(decodeResult.formatted.items[1].value).toBe('12300 feet');
expect(decodeResult.formatted.items[2].code).toBe('ROUTE');
expect(decodeResult.formatted.items[2].value).toBe('MSP/07@20:44:36 > VECTORS');
expect(decodeResult.formatted.items[3].code).toBe('OATEMP');
expect(decodeResult.formatted.items[3].value).toBe('4 degrees');
});

test('decodes Label 4A, variant 2, C-Band', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A(decoder);

// https://app.airframes.io/messages/3461407615
const text = 'M60ALH0752N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111';
const decodeResult = decoderPlugin.decode({ text: text });

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.text).toBe(text);
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 Label 4A, variant 3', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A(decoder);

// https://globe.adsbexchange.com/?icao=A39AC6&showTrace=2024-09-22&timestamp=1727009085
const text = '124442,1320, 138,33467,N 41.093,W 72.677';
const decodeResult = decoderPlugin.decode({ text: text });

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.text).toBe(text);
expect(decodeResult.remaining.text).toBe(' 138');
expect(decodeResult.formatted.items.length).toBe(4);
expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD');
expect(decodeResult.formatted.items[0].value).toBe('12:44:42');
expect(decodeResult.formatted.items[1].code).toBe('ETA');
expect(decodeResult.formatted.items[1].value).toBe('13:20:00');
expect(decodeResult.formatted.items[2].code).toBe('ALT');
expect(decodeResult.formatted.items[2].value).toBe('33467 feet');
expect(decodeResult.formatted.items[3].code).toBe('POS');
expect(decodeResult.formatted.items[3].value).toBe('41.093 N, 72.677 W');
});

test('decodes Label 4A_DIS <invalid>', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A(decoder);

// https://app.airframes.io/messages/3449413366
const text = 'DIS01,182103,WEN3100,WRONG CREW HAHAHA';
const decodeResult = decoderPlugin.decode({ text: text });

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.decoder.name).toBe('label-4a');
expect(decodeResult.formatted.description).toBe('Latest New Format');
expect(decodeResult.formatted.items.length).toBe(0);
});
93 changes: 93 additions & 0 deletions lib/plugins/Label_4A.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { DecoderPlugin } from '../DecoderPlugin';
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
import { CoordinateUtils } from '../utils/coordinate_utils';
import { DateTimeUtils } from '../DateTimeUtils';
import { RouteUtils } from '../utils/route_utils';
import { ResultFormatter } from '../utils/result_formatter';

export class Label_4A extends DecoderPlugin {
name = 'label-4a';

qualifiers() { // eslint-disable-line class-methods-use-this
return {
labels: ['4A'],
};
}

decode(message: Message, options: Options = {}) : DecodeResult {
const decodeResult = this.defaultResult();
decodeResult.decoder.name = this.name;
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(",");
if (fields.length === 11) {
// variant 1
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[0]));
ResultFormatter.tail(decodeResult, fields[2].replace(".", ""));
if (fields[3])
ResultFormatter.callsign(decodeResult, fields[3]);
ResultFormatter.departureAirport(decodeResult, fields[4]);
ResultFormatter.arrivalAirport(decodeResult, fields[5]);
ResultFormatter.altitude(decodeResult, text.substring(48, 51) * 100);
decodeResult.remaining.text = fields.slice(8).join(",");
} else if (fields.length === 6) {
if (fields[0].match(/^[NS]/)) {
// variant 2
ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinates(fields[0].substring(0, 13)));
let wp1 = {
name: fields[0].substring(13).trim(),
time: DateTimeUtils.convertHHMMSSToTod(fields[1].substring(0, 6)),
timeFormat: 'tod',
};
ResultFormatter.altitude(decodeResult, fields[1].substring(6, 9) * 100);
let wp2 = {
name: fields[1].substring(9).trim(),
time: DateTimeUtils.convertHHMMSSToTod(fields[2]),
timeFormat: 'tod',
};
decodeResult.raw.route = {waypoints: [wp1, wp2]};
decodeResult.formatted.items.push({
type: 'aircraft_route',
code: 'ROUTE',
label: 'Aircraft Route',
value: RouteUtils.routeToString(decodeResult.raw.route),
});
ResultFormatter.temperature(decodeResult, fields[3]);
decodeResult.remaining.text = fields.slice(4).join(",");
} else {
// variant 3
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[0]));
ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[1]));
decodeResult.remaining.text = fields[2];
ResultFormatter.altitude(decodeResult, fields[3]);
ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinates((fields[4]+fields[5]).replace(/[ \.]/g, "")));
}
} else {
decodeResult.decoded = false;
decodeResult.remaining.text = text;
}

if (decodeResult.decoded) {
if(!decodeResult.remaining.text)
decodeResult.decoder.decodeLevel = 'full';
else
decodeResult.decoder.decodeLevel = 'partial';
} else {
decodeResult.decoder.decodeLevel = 'none';
}

return decodeResult;
}
}

export default {};
61 changes: 61 additions & 0 deletions lib/plugins/Label_4A_01.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { MessageDecoder } from '../MessageDecoder';
import { Label_4A_01 } from './Label_4A_01';

test('matches Label 4A_01 qualifiers', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A_01(decoder);

expect(decoderPlugin.decode).toBeDefined();
expect(decoderPlugin.name).toBe('label-4a-01');
expect(decoderPlugin.qualifiers).toBeDefined();
expect(decoderPlugin.qualifiers()).toEqual({
labels: ['4A'],
preambles: ['01'],
});
});

test('decodes Label 4A_01', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A_01(decoder);

// https://app.airframes.io/messages/3450562911
const text = '01DCAP VIR41R/190203EGLLKSFO\r\n+ 1418158.0+ 24.8';
const decodeResult = decoderPlugin.decode({ text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('label-4a-01');
expect(decodeResult.formatted.description).toBe('Latest New Format');
expect(decodeResult.message.text).toBe(text);
expect(decodeResult.remaining.text).toBe('158.0');
expect(decodeResult.formatted.items.length).toBe(7);
expect(decodeResult.formatted.items[0].code).toBe('STATE_CHANGE');
expect(decodeResult.formatted.items[0].value).toBe('Descent -> Approach');
expect(decodeResult.formatted.items[1].code).toBe('CALLSIGN');
expect(decodeResult.formatted.items[1].value).toBe('VIR41R');
expect(decodeResult.formatted.items[2].code).toBe('MSG_TOD');
expect(decodeResult.formatted.items[2].value).toBe('19:02:03');
expect(decodeResult.formatted.items[3].code).toBe('ORG');
expect(decodeResult.formatted.items[3].value).toBe('EGLL');
expect(decodeResult.formatted.items[4].code).toBe('DST');
expect(decodeResult.formatted.items[4].value).toBe('KSFO');
expect(decodeResult.formatted.items[5].code).toBe('ALT');
expect(decodeResult.formatted.items[5].value).toBe("1418 feet");
expect(decodeResult.formatted.items[6].code).toBe('OATEMP');
expect(decodeResult.formatted.items[6].value).toBe('24.8 degrees');
});

// disabled because all messages should decode
xtest('decodes Label 4A_01 <invalid>', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_4A_01(decoder);

const text = '4A_01 Bogus message';
const decodeResult = decoderPlugin.decode({ text: text });

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.decoder.name).toBe('label-4a-01');
expect(decodeResult.formatted.description).toBe('Latest New Format');
expect(decodeResult.formatted.items.length).toBe(0);
});
Loading

0 comments on commit 463b3fd

Please sign in to comment.