Skip to content

Commit bdbc50d

Browse files
authored
Adding label 2P decoding (#225)
- FM3, FM4, FM5, and POS
1 parent 5a73232 commit bdbc50d

File tree

12 files changed

+537
-39
lines changed

12 files changed

+537
-39
lines changed

lib/MessageDecoder.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export class MessageDecoder {
3333
this.registerPlugin(new Plugins.Label_22_OFF(this));
3434
this.registerPlugin(new Plugins.Label_22_POS(this));
3535
this.registerPlugin(new Plugins.Label_24_Slash(this));
36+
this.registerPlugin(new Plugins.Label_2P_FM3(this));
37+
this.registerPlugin(new Plugins.Label_2P_FM4(this));
38+
this.registerPlugin(new Plugins.Label_2P_FM5(this));
39+
this.registerPlugin(new Plugins.Label_2P_POS(this));
3640
this.registerPlugin(new Plugins.Label_30_Slash_EA(this));
3741
this.registerPlugin(new Plugins.Label_44_ETA(this));
3842
this.registerPlugin(new Plugins.Label_44_IN(this));

lib/plugins/Label_2P_FM3.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { MessageDecoder } from '../MessageDecoder';
2+
import { Label_2P_FM3 } from './Label_2P_FM3';
3+
4+
describe('Label_2P Preamble FM3', () => {
5+
6+
let plugin: Label_2P_FM3;
7+
8+
beforeEach(() => {
9+
const decoder = new MessageDecoder();
10+
plugin = new Label_2P_FM3(decoder);
11+
});
12+
13+
test('variant 1', () => {
14+
// https://app.airframes.io/messages/4209002765
15+
const text = 'FM3 1217,1312,+ 43.77,- 70.18, 39981, 426, 25';
16+
const decodeResult = plugin.decode({ text: text });
17+
18+
expect(decodeResult.decoded).toBe(true);
19+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
20+
expect(decodeResult.formatted.description).toBe('Flight Report');
21+
expect(decodeResult.message.text).toBe(text);
22+
expect(decodeResult.formatted.items.length).toBe(4);
23+
expect(decodeResult.formatted.items[0].label).toBe('Message Timestamp');
24+
expect(decodeResult.formatted.items[0].value).toBe('12:17:00');
25+
expect(decodeResult.formatted.items[1].label).toBe('Estimated Time of Arrival');
26+
expect(decodeResult.formatted.items[1].value).toBe('13:12:00');
27+
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Position');
28+
expect(decodeResult.formatted.items[2].value).toBe('43.770 N, 70.180 W');
29+
expect(decodeResult.formatted.items[3].label).toBe('Altitude');
30+
expect(decodeResult.formatted.items[3].value).toBe('39981 feet');
31+
expect(decodeResult.remaining.text).toBe(' 426, 25');
32+
});
33+
34+
test('variant 2', () => {
35+
// https://app.airframes.io/messages/4209000440
36+
const text = 'M40AEY093CFM3 1216,1454,+057.31,-075.58, 38002, 469, 23';
37+
const decodeResult = plugin.decode({ text: text });
38+
39+
expect(decodeResult.decoded).toBe(true);
40+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
41+
expect(decodeResult.formatted.description).toBe('Flight Report');
42+
expect(decodeResult.message.text).toBe(text);
43+
expect(decodeResult.formatted.items.length).toBe(5);
44+
expect(decodeResult.formatted.items[0].label).toBe('Flight Number');
45+
expect(decodeResult.formatted.items[0].value).toBe('EY093C');
46+
expect(decodeResult.formatted.items[1].label).toBe('Message Timestamp');
47+
expect(decodeResult.formatted.items[1].value).toBe('12:16:00');
48+
expect(decodeResult.formatted.items[2].label).toBe('Estimated Time of Arrival');
49+
expect(decodeResult.formatted.items[2].value).toBe('14:54:00');
50+
expect(decodeResult.formatted.items[3].label).toBe('Aircraft Position');
51+
expect(decodeResult.formatted.items[3].value).toBe('57.310 N, 75.580 W');
52+
expect(decodeResult.formatted.items[4].label).toBe('Altitude');
53+
expect(decodeResult.formatted.items[4].value).toBe('38002 feet');
54+
expect(decodeResult.remaining.text).toBe('M40A, 469, 23');
55+
});
56+
57+
test('variant 3', () => {
58+
// https://app.airframes.io/messages/4217295798
59+
const text = 'FM3 133818,1607,N 45.206,E 17.726,34030, 440,98';
60+
const decodeResult = plugin.decode({ text: text });
61+
62+
63+
expect(decodeResult.decoded).toBe(true);
64+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
65+
expect(decodeResult.formatted.description).toBe('Flight Report');
66+
expect(decodeResult.message.text).toBe(text);
67+
expect(decodeResult.formatted.items.length).toBe(4);
68+
expect(decodeResult.formatted.items[0].label).toBe('Message Timestamp');
69+
expect(decodeResult.formatted.items[0].value).toBe('13:38:18');
70+
expect(decodeResult.formatted.items[1].label).toBe('Estimated Time of Arrival');
71+
expect(decodeResult.formatted.items[1].value).toBe('16:07:00');
72+
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Position');
73+
expect(decodeResult.formatted.items[2].value).toBe('45.206 N, 17.726 E');
74+
expect(decodeResult.formatted.items[3].label).toBe('Altitude');
75+
expect(decodeResult.formatted.items[3].value).toBe('34030 feet');
76+
expect(decodeResult.remaining.text).toBe(' 440,98');
77+
});
78+
79+
test('<invalid>', () => {
80+
81+
const text = 'FM4 Bogus message';
82+
const decodeResult = plugin.decode({ text: text });
83+
84+
expect(decodeResult.decoded).toBe(false);
85+
expect(decodeResult.decoder.decodeLevel).toBe('none');
86+
expect(decodeResult.formatted.description).toBe('Flight Report');
87+
expect(decodeResult.formatted.items.length).toBe(0);
88+
});
89+
});

lib/plugins/Label_2P_FM3.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { DateTimeUtils } from '../DateTimeUtils';
2+
import { DecoderPlugin } from '../DecoderPlugin';
3+
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
4+
import { CoordinateUtils } from '../utils/coordinate_utils';
5+
import { ResultFormatter } from '../utils/result_formatter';
6+
7+
export class Label_2P_FM3 extends DecoderPlugin {
8+
name = 'label-2p-fm3';
9+
10+
qualifiers() { // eslint-disable-line class-methods-use-this
11+
return {
12+
labels: ["2P"],
13+
};
14+
}
15+
16+
decode(message: Message, options: Options = {}) : DecodeResult {
17+
let decodeResult = this.defaultResult();
18+
decodeResult.decoder.name = this.name;
19+
decodeResult.formatted.description = 'Flight Report';
20+
decodeResult.message = message;
21+
22+
const parts = message.text.split(',');
23+
24+
if(parts.length === 7){
25+
26+
const header = parts[0].split('FM3 ');
27+
if(header.length == 0) {
28+
// can't use preambles, as there can be info before `FM4`
29+
// so let's check if we want to decode it here
30+
ResultFormatter.unknown(decodeResult, message.text);
31+
decodeResult.decoded = false;
32+
decodeResult.decoder.decodeLevel = 'none';
33+
return decodeResult;
34+
}
35+
36+
if(header[0].length > 0) {
37+
ResultFormatter.unknown(decodeResult, header[0].substring(0,4));
38+
ResultFormatter.flightNumber(decodeResult, header[0].substring(4));
39+
}
40+
console.log(header[1]);
41+
if(header[1].length === 4) {
42+
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(header[1]+'00'));
43+
} else {
44+
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(header[1]));
45+
}
46+
ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[1]+'00'));
47+
const lat = parts[2].replaceAll(' ','');
48+
const lon = parts[3].replaceAll(' ','');
49+
if(lat[0] === 'N' || lat[0] === 'S') {
50+
ResultFormatter.position(decodeResult, {
51+
latitude: CoordinateUtils.getDirection(lat[0]) * Number(lat.substring(1)),
52+
longitude: CoordinateUtils.getDirection(lon[0]) * Number(lon.substring(1)),
53+
});
54+
} else {
55+
ResultFormatter.position(decodeResult, {latitude: Number(lat), longitude: Number(lon)});
56+
}
57+
ResultFormatter.altitude(decodeResult, Number(parts[4]));
58+
// TODO: decode further
59+
ResultFormatter.unknown(decodeResult, parts[5]);
60+
ResultFormatter.unknown(decodeResult, parts[6]);
61+
62+
decodeResult.decoded = true;
63+
decodeResult.decoder.decodeLevel = 'partial';
64+
} else {
65+
// Unknown
66+
if (options.debug) {
67+
console.log(`Decoder: Unknown H1 message: ${message.text}`);
68+
}
69+
ResultFormatter.unknown(decodeResult, message.text);
70+
decodeResult.decoded = false;
71+
decodeResult.decoder.decodeLevel = 'none';
72+
}
73+
74+
return decodeResult;
75+
}
76+
}
77+
78+
export default {};

lib/plugins/Label_2P_FM4.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { MessageDecoder } from '../MessageDecoder';
2+
import { Label_2P_FM4 } from './Label_2P_FM4';
3+
4+
describe('Label_2P Preamble FM4', () => {
5+
6+
let plugin: Label_2P_FM4;
7+
8+
beforeEach(() => {
9+
const decoder = new MessageDecoder();
10+
plugin = new Label_2P_FM4(decoder);
11+
});
12+
13+
test('variant 1', () => {
14+
// https://app.airframes.io/messages/4206449201
15+
const text = 'FM4KIAD,OMAA,140256,1448, 39.43,- 75.62,23228,328, 43.5, 72500';
16+
const decodeResult = plugin.decode({ text: text });
17+
18+
expect(decodeResult.decoded).toBe(true);
19+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
20+
expect(decodeResult.formatted.description).toBe('Flight Report');
21+
expect(decodeResult.message.text).toBe(text);
22+
expect(decodeResult.formatted.items.length).toBe(8);
23+
expect(decodeResult.formatted.items[0].label).toBe('Origin');
24+
expect(decodeResult.formatted.items[0].value).toBe('KIAD');
25+
expect(decodeResult.formatted.items[1].label).toBe('Destination');
26+
expect(decodeResult.formatted.items[1].value).toBe('OMAA');
27+
expect(decodeResult.formatted.items[2].label).toBe('Day of Month');
28+
expect(decodeResult.formatted.items[2].value).toBe('14');
29+
expect(decodeResult.formatted.items[3].label).toBe('Message Timestamp');
30+
expect(decodeResult.formatted.items[3].value).toBe('02:56:00');
31+
expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival');
32+
expect(decodeResult.formatted.items[4].value).toBe('14:48:00');
33+
expect(decodeResult.formatted.items[5].label).toBe('Aircraft Position');
34+
expect(decodeResult.formatted.items[5].value).toBe('39.430 N, 75.620 W');
35+
expect(decodeResult.formatted.items[6].label).toBe('Altitude');
36+
expect(decodeResult.formatted.items[6].value).toBe('23228 feet');
37+
expect(decodeResult.formatted.items[7].label).toBe('Heading');
38+
expect(decodeResult.formatted.items[7].value).toBe('328');
39+
expect(decodeResult.remaining.text).toBe(' 43.5, 72500');
40+
});
41+
42+
test('variant 2', () => {
43+
// https://app.airframes.io/messages/4209103135
44+
const text = 'M58AEY0801FM4RJAA,OMAA,141234,2105, 38.92, 115.44,34099,296,-105.5, 52800';
45+
const decodeResult = plugin.decode({ text: text });
46+
47+
expect(decodeResult.decoded).toBe(true);
48+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
49+
expect(decodeResult.formatted.description).toBe('Flight Report');
50+
expect(decodeResult.message.text).toBe(text);
51+
expect(decodeResult.formatted.items.length).toBe(9);
52+
expect(decodeResult.formatted.items[0].label).toBe('Flight Number');
53+
expect(decodeResult.formatted.items[0].value).toBe('EY0801');
54+
expect(decodeResult.formatted.items[1].label).toBe('Origin');
55+
expect(decodeResult.formatted.items[1].value).toBe('RJAA');
56+
expect(decodeResult.formatted.items[2].label).toBe('Destination');
57+
expect(decodeResult.formatted.items[2].value).toBe('OMAA');
58+
expect(decodeResult.formatted.items[3].label).toBe('Day of Month');
59+
expect(decodeResult.formatted.items[3].value).toBe('14');
60+
expect(decodeResult.formatted.items[4].label).toBe('Message Timestamp');
61+
expect(decodeResult.formatted.items[4].value).toBe('12:34:00');
62+
expect(decodeResult.formatted.items[5].label).toBe('Estimated Time of Arrival');
63+
expect(decodeResult.formatted.items[5].value).toBe('21:05:00');
64+
expect(decodeResult.formatted.items[6].label).toBe('Aircraft Position');
65+
expect(decodeResult.formatted.items[6].value).toBe('38.920 N, 115.440 E');
66+
expect(decodeResult.formatted.items[7].label).toBe('Altitude');
67+
expect(decodeResult.formatted.items[7].value).toBe('34099 feet');
68+
expect(decodeResult.formatted.items[8].label).toBe('Heading');
69+
expect(decodeResult.formatted.items[8].value).toBe('296');
70+
expect(decodeResult.remaining.text).toBe('M58A,-105.5, 52800');
71+
});
72+
73+
test('<invalid>', () => {
74+
75+
const text = 'FM4 Bogus message';
76+
const decodeResult = plugin.decode({ text: text });
77+
78+
expect(decodeResult.decoded).toBe(false);
79+
expect(decodeResult.decoder.decodeLevel).toBe('none');
80+
expect(decodeResult.formatted.description).toBe('Flight Report');
81+
expect(decodeResult.formatted.items.length).toBe(0);
82+
});
83+
});

lib/plugins/Label_2P_FM4.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { DateTimeUtils } from '../DateTimeUtils';
2+
import { DecoderPlugin } from '../DecoderPlugin';
3+
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
4+
import { ResultFormatter } from '../utils/result_formatter';
5+
6+
export class Label_2P_FM4 extends DecoderPlugin {
7+
name = 'label-2p-fm4';
8+
9+
qualifiers() { // eslint-disable-line class-methods-use-this
10+
return {
11+
labels: ["2P"],
12+
};
13+
}
14+
15+
decode(message: Message, options: Options = {}) : DecodeResult {
16+
let decodeResult = this.defaultResult();
17+
decodeResult.decoder.name = this.name;
18+
decodeResult.formatted.description = 'Flight Report';
19+
decodeResult.message = message;
20+
21+
const parts = message.text.split(',');
22+
23+
if(parts.length === 10){
24+
const header = parts[0].split('FM4');
25+
if(header.length == 0) {
26+
// can't use preambles, as there can be info before `FM4`
27+
// so let's check if we want to decode it here
28+
ResultFormatter.unknown(decodeResult, message.text);
29+
decodeResult.decoded = false;
30+
decodeResult.decoder.decodeLevel = 'none';
31+
return decodeResult;
32+
}
33+
if(header[0].length > 0) {
34+
ResultFormatter.unknown(decodeResult, header[0].substring(0,4));
35+
ResultFormatter.flightNumber(decodeResult, header[0].substring(4));
36+
}
37+
ResultFormatter.departureAirport(decodeResult, header[1]);
38+
ResultFormatter.arrivalAirport(decodeResult, parts[1]);
39+
ResultFormatter.day(decodeResult, Number(parts[2].substring(0,2)));
40+
ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[2].substring(2)+'00'));
41+
ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[3]+'00'));
42+
ResultFormatter.position(decodeResult, {latitude: Number(parts[4].replaceAll(' ','')), longitude: Number(parts[5].replaceAll(' ',''))});
43+
ResultFormatter.altitude(decodeResult, Number(parts[6]));
44+
ResultFormatter.heading(decodeResult, Number(parts[7]));
45+
// TODO: decode further
46+
ResultFormatter.unknown(decodeResult, parts[8]);
47+
ResultFormatter.unknown(decodeResult, parts[9]);
48+
49+
50+
decodeResult.decoded = true;
51+
decodeResult.decoder.decodeLevel = 'partial';
52+
} else {
53+
// Unknown
54+
if (options.debug) {
55+
console.log(`Decoder: Unknown H1 message: ${message.text}`);
56+
}
57+
ResultFormatter.unknown(decodeResult, message.text);
58+
decodeResult.decoded = false;
59+
decodeResult.decoder.decodeLevel = 'none';
60+
}
61+
62+
return decodeResult;
63+
}
64+
}
65+
66+
export default {};

lib/plugins/Label_2P_FM5.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { MessageDecoder } from '../MessageDecoder';
2+
import { Label_2P_FM5 } from './Label_2P_FM5';
3+
4+
describe('Label_2P Preamble FM5', () => {
5+
6+
let plugin: Label_2P_FM5;
7+
8+
beforeEach(() => {
9+
const decoder = new MessageDecoder();
10+
plugin = new Label_2P_FM5(decoder);
11+
});
12+
13+
test('variant 1', () => {
14+
// https://app.airframes.io/messages/4208768180
15+
const text = 'FM5 EIDW,OMAA,113522,1540,+45.147, +23.384,35002,116.24,502 ,36900,ETD23N ,';
16+
const decodeResult = plugin.decode({ text: text });
17+
18+
expect(decodeResult.decoded).toBe(true);
19+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
20+
expect(decodeResult.formatted.description).toBe('Flight Report');
21+
expect(decodeResult.message.text).toBe(text);
22+
expect(decodeResult.formatted.items.length).toBe(7);
23+
expect(decodeResult.formatted.items[0].label).toBe('Origin');
24+
expect(decodeResult.formatted.items[0].value).toBe('EIDW');
25+
expect(decodeResult.formatted.items[1].label).toBe('Destination');
26+
expect(decodeResult.formatted.items[1].value).toBe('OMAA');
27+
expect(decodeResult.formatted.items[2].label).toBe('Message Timestamp');
28+
expect(decodeResult.formatted.items[2].value).toBe('11:35:22');
29+
expect(decodeResult.formatted.items[3].label).toBe('Estimated Time of Arrival');
30+
expect(decodeResult.formatted.items[3].value).toBe('15:40:00');
31+
expect(decodeResult.formatted.items[4].label).toBe('Aircraft Position');
32+
expect(decodeResult.formatted.items[4].value).toBe('45.147 N, 23.384 E');
33+
expect(decodeResult.formatted.items[5].label).toBe('Altitude');
34+
expect(decodeResult.formatted.items[5].value).toBe('35002 feet');
35+
expect(decodeResult.formatted.items[6].label).toBe('Flight Number');
36+
expect(decodeResult.formatted.items[6].value).toBe('ETD23N');
37+
expect(decodeResult.remaining.text).toBe('116.24,502 ,36900,');
38+
});
39+
40+
test('<invalid>', () => {
41+
42+
const text = 'FM4 Bogus message';
43+
const decodeResult = plugin.decode({ text: text });
44+
45+
expect(decodeResult.decoded).toBe(false);
46+
expect(decodeResult.decoder.decodeLevel).toBe('none');
47+
expect(decodeResult.formatted.description).toBe('Flight Report');
48+
expect(decodeResult.formatted.items.length).toBe(0);
49+
});
50+
});

0 commit comments

Comments
 (0)