Skip to content

Commit 463b3fd

Browse files
authored
Label 4a (#169)
* non-4A updates * add Label 4A * add degrees to OATEMP formatting * fixes * make 4A invalid test case a real DIS01 message
1 parent c440fd4 commit 463b3fd

30 files changed

+810
-80
lines changed

lib/MessageDecoder.ts

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export class MessageDecoder {
3232
this.registerPlugin(new Plugins.Label_44_OFF(this));
3333
this.registerPlugin(new Plugins.Label_44_ON(this));
3434
this.registerPlugin(new Plugins.Label_44_POS(this));
35+
this.registerPlugin(new Plugins.Label_4A(this));
36+
this.registerPlugin(new Plugins.Label_4A_01(this));
37+
this.registerPlugin(new Plugins.Label_4A_DIS(this));
38+
this.registerPlugin(new Plugins.Label_4A_DOOR(this));
39+
this.registerPlugin(new Plugins.Label_4A_Slash_01(this));
3540
this.registerPlugin(new Plugins.Label_4N(this));
3641
this.registerPlugin(new Plugins.Label_B6_Forwardslash(this));
3742
this.registerPlugin(new Plugins.Label_H1_FLR(this));

lib/plugins/Label_21_POS.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('Label_21_POS', () => {
4141
expect(decodeResult.formatted.items[3].type).toBe('outside_air_temperature');
4242
expect(decodeResult.formatted.items[3].code).toBe('OATEMP');
4343
expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)');
44-
expect(decodeResult.formatted.items[3].value).toBe('4');
44+
expect(decodeResult.formatted.items[3].value).toBe('-4 degrees');
4545
expect(decodeResult.formatted.items[4].type).toBe('time_of_day');
4646
expect(decodeResult.formatted.items[4].code).toBe('ETA');
4747
expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival');
@@ -64,4 +64,4 @@ describe('Label_21_POS', () => {
6464
expect(decodeResult.formatted.description).toBe('Position Report');
6565
expect(decodeResult.message.text).toBe(text);
6666
});
67-
});
67+
});

lib/plugins/Label_21_POS.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class Label_21_POS extends DecoderPlugin {
3131
processPosition(decodeResult, fields[0].trim());
3232
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[2]));
3333
ResultFormatter.altitude( decodeResult, Number(fields[3]));
34-
ResultFormatter.temperature(decodeResult, fields[6]);
34+
ResultFormatter.temperature(decodeResult, fields[6].replace(/ /g, ""));
3535
ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[7]));
3636
ResultFormatter.arrivalAirport(decodeResult, fields[8]);
3737

lib/plugins/Label_24_Slash.test.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ describe('Label_24_Slash', () => {
3232
expect(decodeResult.raw.position.latitude).toBe(53.13);
3333
expect(decodeResult.raw.position.longitude).toBe(1.33);
3434
expect(decodeResult.raw.eta_time).toBe(39360);
35-
expect(decodeResult.formatted.items.length).toBe(3);
36-
expect(decodeResult.formatted.items[0].label).toBe('Altitude');
37-
expect(decodeResult.formatted.items[0].value).toBe('34962 feet');
38-
expect(decodeResult.formatted.items[1].label).toBe('Aircraft Position');
39-
expect(decodeResult.formatted.items[1].value).toBe('53.130 N, 1.330 E');
40-
expect(decodeResult.formatted.items[2].label).toBe('Estimated Time of Arrival');
41-
expect(decodeResult.formatted.items[2].value).toBe('10:56:00');
35+
expect(decodeResult.formatted.items.length).toBe(4);
36+
expect(decodeResult.formatted.items[0].label).toBe('Flight Number');
37+
expect(decodeResult.formatted.items[0].value).toBe('04WM');
38+
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
39+
expect(decodeResult.formatted.items[1].value).toBe('34962 feet');
40+
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Position');
41+
expect(decodeResult.formatted.items[2].value).toBe('53.130 N, 1.330 E');
42+
expect(decodeResult.formatted.items[3].label).toBe('Estimated Time of Arrival');
43+
expect(decodeResult.formatted.items[3].value).toBe('10:56:00');
4244

4345
expect(decodeResult.remaining.text).toBe('3374');
4446
});
@@ -52,4 +54,4 @@ describe('Label_24_Slash', () => {
5254
expect(decodeResult.decoder.decodeLevel).toBe('none');
5355
expect(decodeResult.message.text).toBe(text);
5456
});
55-
});
57+
});

lib/plugins/Label_4A.test.ts

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { MessageDecoder } from '../MessageDecoder';
2+
import { Label_4A } from './Label_4A';
3+
4+
test('matches Label 4A qualifiers', () => {
5+
const decoder = new MessageDecoder();
6+
const decoderPlugin = new Label_4A(decoder);
7+
8+
expect(decoderPlugin.decode).toBeDefined();
9+
expect(decoderPlugin.name).toBe('label-4a');
10+
expect(decoderPlugin.qualifiers).toBeDefined();
11+
expect(decoderPlugin.qualifiers()).toEqual({
12+
labels: ['4A'],
13+
});
14+
});
15+
16+
test('decodes Label 4A, variant 1', () => {
17+
const decoder = new MessageDecoder();
18+
const decoderPlugin = new Label_4A(decoder);
19+
20+
// https://app.airframes.io/messages/3451492279
21+
const text = '063200,1910,.N343FR,FFT2028,KSLC,KORD,1,0632,RT0,LT0,';
22+
const decodeResult = decoderPlugin.decode({ text: text });
23+
24+
expect(decodeResult.decoded).toBe(true);
25+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
26+
expect(decodeResult.decoder.name).toBe('label-4a');
27+
expect(decodeResult.formatted.description).toBe('Latest New Format');
28+
expect(decodeResult.message.text).toBe(text);
29+
expect(decodeResult.remaining.text).toBe('RT0,LT0,');
30+
expect(decodeResult.formatted.items.length).toBe(6);
31+
expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD');
32+
expect(decodeResult.formatted.items[0].value).toBe('06:32:00');
33+
expect(decodeResult.formatted.items[1].code).toBe('TAIL');
34+
expect(decodeResult.formatted.items[1].value).toBe('N343FR');
35+
expect(decodeResult.formatted.items[2].code).toBe('CALLSIGN');
36+
expect(decodeResult.formatted.items[2].value).toBe('FFT2028');
37+
expect(decodeResult.formatted.items[3].code).toBe('ORG');
38+
expect(decodeResult.formatted.items[3].value).toBe('KSLC');
39+
expect(decodeResult.formatted.items[4].code).toBe('DST');
40+
expect(decodeResult.formatted.items[4].value).toBe('KORD');
41+
});
42+
43+
test('decodes Label 4A, variant 1, no callsign', () => {
44+
const decoder = new MessageDecoder();
45+
const decoderPlugin = new Label_4A(decoder);
46+
47+
// https://app.airframes.io/messages/3452310240
48+
const text = '101606,1910,.N317FR,,KMDW,----,1,1016,RT0,LT1,';
49+
const decodeResult = decoderPlugin.decode({ text: text });
50+
51+
expect(decodeResult.decoded).toBe(true);
52+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
53+
expect(decodeResult.decoder.name).toBe('label-4a');
54+
expect(decodeResult.formatted.description).toBe('Latest New Format');
55+
expect(decodeResult.message.text).toBe(text);
56+
expect(decodeResult.remaining.text).toBe('RT0,LT1,');
57+
expect(decodeResult.formatted.items.length).toBe(5);
58+
expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD');
59+
expect(decodeResult.formatted.items[0].value).toBe('10:16:06');
60+
expect(decodeResult.formatted.items[1].code).toBe('TAIL');
61+
expect(decodeResult.formatted.items[1].value).toBe('N317FR');
62+
expect(decodeResult.formatted.items[2].code).toBe('ORG');
63+
expect(decodeResult.formatted.items[2].value).toBe('KMDW');
64+
expect(decodeResult.formatted.items[3].code).toBe('DST');
65+
expect(decodeResult.formatted.items[3].value).toBe('----');
66+
});
67+
68+
test('decodes Label 4A, variant 2', () => {
69+
const decoder = new MessageDecoder();
70+
const decoderPlugin = new Label_4A(decoder);
71+
72+
// https://app.airframes.io/messages/3461807403
73+
const text = 'N45129W093113MSP/07 ,204436123VECTORS,,P04,268044858,46904221';
74+
const decodeResult = decoderPlugin.decode({ text: text });
75+
76+
expect(decodeResult.decoded).toBe(true);
77+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
78+
expect(decodeResult.decoder.name).toBe('label-4a');
79+
expect(decodeResult.formatted.description).toBe('Latest New Format');
80+
expect(decodeResult.message.text).toBe(text);
81+
expect(decodeResult.remaining.text).toBe('268044858,46904221');
82+
expect(decodeResult.formatted.items.length).toBe(4);
83+
expect(decodeResult.formatted.items[0].code).toBe('POS');
84+
expect(decodeResult.formatted.items[0].value).toBe('45.129 N, 93.113 W');
85+
expect(decodeResult.formatted.items[1].code).toBe('ALT');
86+
expect(decodeResult.formatted.items[1].value).toBe('12300 feet');
87+
expect(decodeResult.formatted.items[2].code).toBe('ROUTE');
88+
expect(decodeResult.formatted.items[2].value).toBe('MSP/07@20:44:36 > VECTORS');
89+
expect(decodeResult.formatted.items[3].code).toBe('OATEMP');
90+
expect(decodeResult.formatted.items[3].value).toBe('4 degrees');
91+
});
92+
93+
test('decodes Label 4A, variant 2, C-Band', () => {
94+
const decoder = new MessageDecoder();
95+
const decoderPlugin = new Label_4A(decoder);
96+
97+
// https://app.airframes.io/messages/3461407615
98+
const text = 'M60ALH0752N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111';
99+
const decodeResult = decoderPlugin.decode({ text: text });
100+
101+
expect(decodeResult.decoded).toBe(true);
102+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
103+
expect(decodeResult.decoder.name).toBe('label-4a');
104+
expect(decodeResult.formatted.description).toBe('Latest New Format');
105+
expect(decodeResult.message.text).toBe(text);
106+
expect(decodeResult.remaining.text).toBe('275043309,85220111');
107+
expect(decodeResult.formatted.items.length).toBe(5);
108+
expect(decodeResult.formatted.items[0].code).toBe('FLIGHT');
109+
expect(decodeResult.formatted.items[0].value).toBe('LH752');
110+
expect(decodeResult.formatted.items[1].code).toBe('POS');
111+
expect(decodeResult.formatted.items[1].value).toBe('22.456 N, 77.014 E');
112+
expect(decodeResult.formatted.items[2].code).toBe('ALT');
113+
expect(decodeResult.formatted.items[2].value).toBe('37000 feet');
114+
expect(decodeResult.formatted.items[3].code).toBe('ROUTE');
115+
expect(decodeResult.formatted.items[3].value).toBe('OSE35@19:20:27 > VEX36@19:23:16');
116+
expect(decodeResult.formatted.items[4].code).toBe('OATEMP');
117+
expect(decodeResult.formatted.items[4].value).toBe('-46 degrees');
118+
});
119+
120+
test('decodes Label 4A, variant 3', () => {
121+
const decoder = new MessageDecoder();
122+
const decoderPlugin = new Label_4A(decoder);
123+
124+
// https://globe.adsbexchange.com/?icao=A39AC6&showTrace=2024-09-22&timestamp=1727009085
125+
const text = '124442,1320, 138,33467,N 41.093,W 72.677';
126+
const decodeResult = decoderPlugin.decode({ text: text });
127+
128+
expect(decodeResult.decoded).toBe(true);
129+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
130+
expect(decodeResult.decoder.name).toBe('label-4a');
131+
expect(decodeResult.formatted.description).toBe('Latest New Format');
132+
expect(decodeResult.message.text).toBe(text);
133+
expect(decodeResult.remaining.text).toBe(' 138');
134+
expect(decodeResult.formatted.items.length).toBe(4);
135+
expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD');
136+
expect(decodeResult.formatted.items[0].value).toBe('12:44:42');
137+
expect(decodeResult.formatted.items[1].code).toBe('ETA');
138+
expect(decodeResult.formatted.items[1].value).toBe('13:20:00');
139+
expect(decodeResult.formatted.items[2].code).toBe('ALT');
140+
expect(decodeResult.formatted.items[2].value).toBe('33467 feet');
141+
expect(decodeResult.formatted.items[3].code).toBe('POS');
142+
expect(decodeResult.formatted.items[3].value).toBe('41.093 N, 72.677 W');
143+
});
144+
145+
test('decodes Label 4A_DIS <invalid>', () => {
146+
const decoder = new MessageDecoder();
147+
const decoderPlugin = new Label_4A(decoder);
148+
149+
// https://app.airframes.io/messages/3449413366
150+
const text = 'DIS01,182103,WEN3100,WRONG CREW HAHAHA';
151+
const decodeResult = decoderPlugin.decode({ text: text });
152+
153+
expect(decodeResult.decoded).toBe(false);
154+
expect(decodeResult.decoder.decodeLevel).toBe('none');
155+
expect(decodeResult.decoder.name).toBe('label-4a');
156+
expect(decodeResult.formatted.description).toBe('Latest New Format');
157+
expect(decodeResult.formatted.items.length).toBe(0);
158+
});

lib/plugins/Label_4A.ts

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { DecoderPlugin } from '../DecoderPlugin';
2+
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
3+
import { CoordinateUtils } from '../utils/coordinate_utils';
4+
import { DateTimeUtils } from '../DateTimeUtils';
5+
import { RouteUtils } from '../utils/route_utils';
6+
import { ResultFormatter } from '../utils/result_formatter';
7+
8+
export class Label_4A extends DecoderPlugin {
9+
name = 'label-4a';
10+
11+
qualifiers() { // eslint-disable-line class-methods-use-this
12+
return {
13+
labels: ['4A'],
14+
};
15+
}
16+
17+
decode(message: Message, options: Options = {}) : DecodeResult {
18+
const decodeResult = this.defaultResult();
19+
decodeResult.decoder.name = this.name;
20+
decodeResult.message = message;
21+
decodeResult.formatted.description = 'Latest New Format';
22+
23+
24+
// Inmarsat C-band seems to prefix normal messages with a message number and flight number
25+
let text = message.text;
26+
if (text.match(/^M\d{2}A\w{6}/)) {
27+
ResultFormatter.flightNumber(decodeResult, message.text.substring(4, 10).replace(/^([A-Z]+)0*/g, "$1"));
28+
text = text.substring(10);
29+
}
30+
31+
decodeResult.decoded = true;
32+
const fields = text.split(",");
33+
if (fields.length === 11) {
34+
// variant 1
35+
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[0]));
36+
ResultFormatter.tail(decodeResult, fields[2].replace(".", ""));
37+
if (fields[3])
38+
ResultFormatter.callsign(decodeResult, fields[3]);
39+
ResultFormatter.departureAirport(decodeResult, fields[4]);
40+
ResultFormatter.arrivalAirport(decodeResult, fields[5]);
41+
ResultFormatter.altitude(decodeResult, text.substring(48, 51) * 100);
42+
decodeResult.remaining.text = fields.slice(8).join(",");
43+
} else if (fields.length === 6) {
44+
if (fields[0].match(/^[NS]/)) {
45+
// variant 2
46+
ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinates(fields[0].substring(0, 13)));
47+
let wp1 = {
48+
name: fields[0].substring(13).trim(),
49+
time: DateTimeUtils.convertHHMMSSToTod(fields[1].substring(0, 6)),
50+
timeFormat: 'tod',
51+
};
52+
ResultFormatter.altitude(decodeResult, fields[1].substring(6, 9) * 100);
53+
let wp2 = {
54+
name: fields[1].substring(9).trim(),
55+
time: DateTimeUtils.convertHHMMSSToTod(fields[2]),
56+
timeFormat: 'tod',
57+
};
58+
decodeResult.raw.route = {waypoints: [wp1, wp2]};
59+
decodeResult.formatted.items.push({
60+
type: 'aircraft_route',
61+
code: 'ROUTE',
62+
label: 'Aircraft Route',
63+
value: RouteUtils.routeToString(decodeResult.raw.route),
64+
});
65+
ResultFormatter.temperature(decodeResult, fields[3]);
66+
decodeResult.remaining.text = fields.slice(4).join(",");
67+
} else {
68+
// variant 3
69+
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[0]));
70+
ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[1]));
71+
decodeResult.remaining.text = fields[2];
72+
ResultFormatter.altitude(decodeResult, fields[3]);
73+
ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinates((fields[4]+fields[5]).replace(/[ \.]/g, "")));
74+
}
75+
} else {
76+
decodeResult.decoded = false;
77+
decodeResult.remaining.text = text;
78+
}
79+
80+
if (decodeResult.decoded) {
81+
if(!decodeResult.remaining.text)
82+
decodeResult.decoder.decodeLevel = 'full';
83+
else
84+
decodeResult.decoder.decodeLevel = 'partial';
85+
} else {
86+
decodeResult.decoder.decodeLevel = 'none';
87+
}
88+
89+
return decodeResult;
90+
}
91+
}
92+
93+
export default {};

lib/plugins/Label_4A_01.test.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { MessageDecoder } from '../MessageDecoder';
2+
import { Label_4A_01 } from './Label_4A_01';
3+
4+
test('matches Label 4A_01 qualifiers', () => {
5+
const decoder = new MessageDecoder();
6+
const decoderPlugin = new Label_4A_01(decoder);
7+
8+
expect(decoderPlugin.decode).toBeDefined();
9+
expect(decoderPlugin.name).toBe('label-4a-01');
10+
expect(decoderPlugin.qualifiers).toBeDefined();
11+
expect(decoderPlugin.qualifiers()).toEqual({
12+
labels: ['4A'],
13+
preambles: ['01'],
14+
});
15+
});
16+
17+
test('decodes Label 4A_01', () => {
18+
const decoder = new MessageDecoder();
19+
const decoderPlugin = new Label_4A_01(decoder);
20+
21+
// https://app.airframes.io/messages/3450562911
22+
const text = '01DCAP VIR41R/190203EGLLKSFO\r\n+ 1418158.0+ 24.8';
23+
const decodeResult = decoderPlugin.decode({ text: text });
24+
25+
expect(decodeResult.decoded).toBe(true);
26+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
27+
expect(decodeResult.decoder.name).toBe('label-4a-01');
28+
expect(decodeResult.formatted.description).toBe('Latest New Format');
29+
expect(decodeResult.message.text).toBe(text);
30+
expect(decodeResult.remaining.text).toBe('158.0');
31+
expect(decodeResult.formatted.items.length).toBe(7);
32+
expect(decodeResult.formatted.items[0].code).toBe('STATE_CHANGE');
33+
expect(decodeResult.formatted.items[0].value).toBe('Descent -> Approach');
34+
expect(decodeResult.formatted.items[1].code).toBe('CALLSIGN');
35+
expect(decodeResult.formatted.items[1].value).toBe('VIR41R');
36+
expect(decodeResult.formatted.items[2].code).toBe('MSG_TOD');
37+
expect(decodeResult.formatted.items[2].value).toBe('19:02:03');
38+
expect(decodeResult.formatted.items[3].code).toBe('ORG');
39+
expect(decodeResult.formatted.items[3].value).toBe('EGLL');
40+
expect(decodeResult.formatted.items[4].code).toBe('DST');
41+
expect(decodeResult.formatted.items[4].value).toBe('KSFO');
42+
expect(decodeResult.formatted.items[5].code).toBe('ALT');
43+
expect(decodeResult.formatted.items[5].value).toBe("1418 feet");
44+
expect(decodeResult.formatted.items[6].code).toBe('OATEMP');
45+
expect(decodeResult.formatted.items[6].value).toBe('24.8 degrees');
46+
});
47+
48+
// disabled because all messages should decode
49+
xtest('decodes Label 4A_01 <invalid>', () => {
50+
const decoder = new MessageDecoder();
51+
const decoderPlugin = new Label_4A_01(decoder);
52+
53+
const text = '4A_01 Bogus message';
54+
const decodeResult = decoderPlugin.decode({ text: text });
55+
56+
expect(decodeResult.decoded).toBe(false);
57+
expect(decodeResult.decoder.decodeLevel).toBe('none');
58+
expect(decodeResult.decoder.name).toBe('label-4a-01');
59+
expect(decodeResult.formatted.description).toBe('Latest New Format');
60+
expect(decodeResult.formatted.items.length).toBe(0);
61+
});

0 commit comments

Comments
 (0)