Skip to content

Commit d5f68ca

Browse files
authored
Adding Label 16 Decoders (#232)
altitude no longer persisted as it was blank/NaN
1 parent 900b335 commit d5f68ca

File tree

9 files changed

+290
-3
lines changed

9 files changed

+290
-3
lines changed

lib/MessageDecoder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export class MessageDecoder {
2424
this.registerPlugin(new Plugins.Label_15(this));
2525
this.registerPlugin(new Plugins.Label_15_FST(this));
2626
this.registerPlugin(new Plugins.Label_16_N_Space(this));
27+
this.registerPlugin(new Plugins.Label_16_POSA1(this));
28+
this.registerPlugin(new Plugins.Label_16_TOD(this));
2729
this.registerPlugin(new Plugins.Label_1L_3Line(this));
2830
this.registerPlugin(new Plugins.Label_1L_070(this));
2931
this.registerPlugin(new Plugins.Label_1L_660(this));

lib/plugins/Label_16_POSA1.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { MessageDecoder } from '../MessageDecoder';
2+
import { Label_16_POSA1 } from './Label_16_POSA1';
3+
4+
describe('Label 16 POSA1', () => {
5+
6+
let plugin: Label_16_POSA1;
7+
8+
beforeEach(() => {
9+
const decoder = new MessageDecoder();
10+
plugin = new Label_16_POSA1(decoder);
11+
});
12+
13+
test('matches qualifiers', () => {
14+
expect(plugin.decode).toBeDefined();
15+
expect(plugin.name).toBe('label-16-posa1');
16+
expect(plugin.qualifiers).toBeDefined();
17+
expect(plugin.qualifiers()).toEqual({
18+
labels: ['16'],
19+
preambles: ['POSA1'],
20+
});
21+
});
22+
test('decodes variant 1', () => {
23+
const text = 'POSA1N37358W 77279,GEARS ,221626,370,BBOBO ,222053,,-61,139,1174,829';
24+
const decodeResult = plugin.decode({ text: text });
25+
26+
expect(decodeResult.decoded).toBe(true);
27+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
28+
expect(decodeResult.decoder.name).toBe('label-16-posa1');
29+
expect(decodeResult.formatted.description).toBe('Position Report');
30+
expect(decodeResult.message.text).toBe(text);
31+
expect(decodeResult.formatted.items.length).toBe(3);
32+
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
33+
expect(decodeResult.formatted.items[0].value).toBe('37.358 N, 77.279 W');
34+
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
35+
expect(decodeResult.formatted.items[1].value).toBe('37000 feet');
36+
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
37+
expect(decodeResult.formatted.items[2].value).toBe('GEARS@22:16:26 > BBOBO@22:20:53');
38+
expect(decodeResult.remaining.text).toBe(',-61,139,1174,829');
39+
});
40+
41+
test('decodes redacted', () => {
42+
const text = 'POSA1N38843W 78790,RONZZ ,005159,390,RAMAY ,010055,,*****,*****, 744, 0';
43+
const decodeResult = plugin.decode({ text: text });
44+
45+
expect(decodeResult.decoded).toBe(true);
46+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
47+
expect(decodeResult.decoder.name).toBe('label-16-posa1');
48+
expect(decodeResult.formatted.description).toBe('Position Report');
49+
expect(decodeResult.message.text).toBe(text);
50+
expect(decodeResult.formatted.items.length).toBe(3);
51+
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
52+
expect(decodeResult.formatted.items[0].value).toBe('38.843 N, 78.790 W');
53+
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
54+
expect(decodeResult.formatted.items[1].value).toBe('39000 feet');
55+
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
56+
expect(decodeResult.formatted.items[2].value).toBe('RONZZ@00:51:59 > RAMAY@01:00:55');
57+
expect(decodeResult.remaining.text).toBe(',*****,*****, 744, 0');
58+
});
59+
60+
test('decodes Label 16 variant <invalid>', () => {
61+
const text = 'N Bogus message';
62+
const decodeResult = plugin.decode({ text: text });
63+
64+
expect(decodeResult.decoded).toBe(false);
65+
expect(decodeResult.decoder.decodeLevel).toBe('none');
66+
expect(decodeResult.decoder.name).toBe('label-16-posa1');
67+
expect(decodeResult.formatted.description).toBe('Position Report');
68+
expect(decodeResult.message.text).toBe(text);
69+
});
70+
});

lib/plugins/Label_16_POSA1.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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_16_POSA1 extends DecoderPlugin {
8+
name = 'label-16-posa1';
9+
10+
qualifiers() { // eslint-disable-line class-methods-use-this
11+
return {
12+
labels: ["16"],
13+
preambles: ['POSA1'],
14+
};
15+
}
16+
17+
decode(message: Message, options: Options = {}) : DecodeResult {
18+
const decodeResult = this.defaultResult();
19+
decodeResult.decoder.name = this.name;
20+
decodeResult.formatted.description = 'Position Report';
21+
decodeResult.message = message;
22+
23+
const fields = message.text.split(',');
24+
if (fields.length !== 11 || !fields[0].startsWith('POSA1')) {
25+
if (options.debug) {
26+
console.log(`Decoder: Unknown 16 message: ${message.text}`);
27+
}
28+
decodeResult.remaining.text = message.text;
29+
decodeResult.decoded = false;
30+
decodeResult.decoder.decodeLevel = 'none';
31+
return decodeResult;
32+
}
33+
34+
ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinates(fields[0].substring(5))); // strip 'POSA1'
35+
const waypoint = fields[1].trim();
36+
const time = DateTimeUtils.convertHHMMSSToTod(fields[2]);
37+
ResultFormatter.altitude(decodeResult, Number(fields[3])*100);
38+
const nextWaypoint = fields[4].trim();
39+
const nextTime = DateTimeUtils.convertHHMMSSToTod(fields[5]);
40+
ResultFormatter.unknownArr(decodeResult, fields.slice(6), ',');
41+
ResultFormatter.route(decodeResult, {waypoints: [
42+
{name: waypoint, time: time, timeFormat: 'tod'},
43+
{name: nextWaypoint, time: nextTime, timeFormat: 'tod'}
44+
]});
45+
decodeResult.decoded = true;
46+
decodeResult.decoder.decodeLevel = 'partial';
47+
48+
49+
return decodeResult;
50+
}
51+
}
52+
53+
export default {};

lib/plugins/Label_16_TOD.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { MessageDecoder } from '../MessageDecoder';
2+
import { Label_16_TOD } from './Label_16_TOD';
3+
4+
describe('Label 16 Time of Day', () => {
5+
6+
let plugin: Label_16_TOD;
7+
8+
beforeEach(() => {
9+
const decoder = new MessageDecoder();
10+
plugin = new Label_16_TOD(decoder);
11+
});
12+
13+
test('matches qualifiers', () => {
14+
expect(plugin.decode).toBeDefined();
15+
expect(plugin.name).toBe('label-16-tod');
16+
expect(plugin.qualifiers).toBeDefined();
17+
expect(plugin.qualifiers()).toEqual({
18+
labels: ['16'],
19+
});
20+
});
21+
22+
test('decodes variant 1', () => {
23+
const text = '005236,36787,0135, 97,N 38.364 W 75.226';
24+
const decodeResult = plugin.decode({ text: text });
25+
26+
expect(decodeResult.decoded).toBe(true);
27+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
28+
expect(decodeResult.decoder.name).toBe('label-16-tod');
29+
expect(decodeResult.formatted.description).toBe('Position Report');
30+
expect(decodeResult.message.text).toBe(text);
31+
expect(decodeResult.formatted.items.length).toBe(4);
32+
expect(decodeResult.formatted.items[0].label).toBe('Message Timestamp');
33+
expect(decodeResult.formatted.items[0].value).toBe('00:52:36');
34+
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
35+
expect(decodeResult.formatted.items[1].value).toBe('36787 feet');
36+
expect(decodeResult.formatted.items[2].label).toBe('Estimated Time of Arrival');
37+
expect(decodeResult.formatted.items[2].value).toBe('01:35:00');
38+
expect(decodeResult.formatted.items[3].label).toBe('Aircraft Position');
39+
expect(decodeResult.formatted.items[3].value).toBe('38.364 N, 75.226 W');
40+
expect(decodeResult.remaining.text).toBe(' 97');
41+
});
42+
43+
test('decodes variant 2', () => {
44+
// https://app.airframes.io/messages/4260590297
45+
const text = '110112,36000,1206, 51,N 45.140 E 16.341/SXS7SL';
46+
const decodeResult = plugin.decode({ text: text });
47+
48+
expect(decodeResult.decoded).toBe(true);
49+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
50+
expect(decodeResult.decoder.name).toBe('label-16-tod');
51+
expect(decodeResult.formatted.description).toBe('Position Report');
52+
expect(decodeResult.message.text).toBe(text);
53+
expect(decodeResult.formatted.items.length).toBe(5);
54+
expect(decodeResult.formatted.items[0].label).toBe('Message Timestamp');
55+
expect(decodeResult.formatted.items[0].value).toBe('11:01:12');
56+
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
57+
expect(decodeResult.formatted.items[1].value).toBe('36000 feet');
58+
expect(decodeResult.formatted.items[2].label).toBe('Estimated Time of Arrival');
59+
expect(decodeResult.formatted.items[2].value).toBe('12:06:00');
60+
expect(decodeResult.formatted.items[3].label).toBe('Aircraft Position');
61+
expect(decodeResult.formatted.items[3].value).toBe('45.140 N, 16.341 E');
62+
expect(decodeResult.formatted.items[4].label).toBe('Flight Number');
63+
expect(decodeResult.formatted.items[4].value).toBe('SXS7SL');
64+
expect(decodeResult.remaining.text).toBe(' 51');
65+
});
66+
67+
68+
test('decodes no position', () => {
69+
// https://app.airframes.io/messages/4260590899
70+
const text = '110122,,1206, 92,N . MMMM.MMM';
71+
const decodeResult = plugin.decode({ text: text });
72+
73+
expect(decodeResult.decoded).toBe(true);
74+
expect(decodeResult.decoder.decodeLevel).toBe('partial');
75+
expect(decodeResult.decoder.name).toBe('label-16-tod');
76+
expect(decodeResult.formatted.description).toBe('Position Report');
77+
expect(decodeResult.message.text).toBe(text);
78+
expect(decodeResult.formatted.items.length).toBe(2);
79+
expect(decodeResult.formatted.items[0].label).toBe('Message Timestamp');
80+
expect(decodeResult.formatted.items[0].value).toBe('11:01:22');
81+
expect(decodeResult.formatted.items[1].label).toBe('Estimated Time of Arrival');
82+
expect(decodeResult.formatted.items[1].value).toBe('12:06:00');
83+
expect(decodeResult.remaining.text).toBe(' 92');
84+
});
85+
86+
test('decodes Label 16 variant <invalid>', () => {
87+
const text = 'N Bogus message';
88+
const decodeResult = plugin.decode({ text: text });
89+
90+
expect(decodeResult.decoded).toBe(false);
91+
expect(decodeResult.decoder.decodeLevel).toBe('none');
92+
expect(decodeResult.decoder.name).toBe('label-16-tod');
93+
expect(decodeResult.formatted.description).toBe('Position Report');
94+
expect(decodeResult.message.text).toBe(text);
95+
});
96+
});

lib/plugins/Label_16_TOD.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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_16_TOD extends DecoderPlugin {
8+
name = 'label-16-tod';
9+
10+
qualifiers() { // eslint-disable-line class-methods-use-this
11+
return {
12+
labels: ["16"],
13+
};
14+
}
15+
16+
decode(message: Message, options: Options = {}) : DecodeResult {
17+
const decodeResult = this.defaultResult();
18+
decodeResult.decoder.name = this.name;
19+
decodeResult.formatted.description = 'Position Report';
20+
decodeResult.message = message;
21+
22+
const fields = message.text.split(',');
23+
const time = DateTimeUtils.convertHHMMSSToTod(fields[0])
24+
if (fields.length !== 5 || Number.isNaN(time)) {
25+
if (options.debug) {
26+
console.log(`Decoder: Unknown 16 message: ${message.text}`);
27+
}
28+
decodeResult.remaining.text = message.text;
29+
decodeResult.decoded = false;
30+
decodeResult.decoder.decodeLevel = 'none';
31+
return decodeResult;
32+
}
33+
34+
ResultFormatter.time_of_day(decodeResult, time);
35+
if(fields[1] !== '') {
36+
ResultFormatter.altitude(decodeResult, Number(fields[1]));
37+
}
38+
ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[2]));
39+
ResultFormatter.unknown(decodeResult, fields[3]);
40+
const temp = fields[4].split('/');
41+
const posFields = temp[0].split(' ');
42+
ResultFormatter.position(decodeResult, {
43+
latitude: CoordinateUtils.getDirection(posFields[0]) * Number(posFields[1]),
44+
longitude: CoordinateUtils.getDirection(posFields[2]) * Number(posFields[3]),
45+
});
46+
47+
if(temp.length > 1) {
48+
ResultFormatter.flightNumber(decodeResult, temp[1]);
49+
}
50+
decodeResult.decoded = true;
51+
decodeResult.decoder.decodeLevel = 'partial';
52+
53+
54+
return decodeResult;
55+
}
56+
}
57+
58+
export default {};

lib/plugins/Label_4A.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ test('decodes Label 4A, variant 1', () => {
2727
expect(decodeResult.formatted.description).toBe('Latest New Format');
2828
expect(decodeResult.message.text).toBe(text);
2929
expect(decodeResult.remaining.text).toBe('RT0,LT0,');
30-
expect(decodeResult.formatted.items.length).toBe(6);
30+
expect(decodeResult.formatted.items.length).toBe(5);
3131
expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD');
3232
expect(decodeResult.formatted.items[0].value).toBe('06:32:00');
3333
expect(decodeResult.formatted.items[1].code).toBe('TAIL');
@@ -54,7 +54,7 @@ test('decodes Label 4A, variant 1, no callsign', () => {
5454
expect(decodeResult.formatted.description).toBe('Latest New Format');
5555
expect(decodeResult.message.text).toBe(text);
5656
expect(decodeResult.remaining.text).toBe('RT0,LT1,');
57-
expect(decodeResult.formatted.items.length).toBe(5);
57+
expect(decodeResult.formatted.items.length).toBe(4);
5858
expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD');
5959
expect(decodeResult.formatted.items[0].value).toBe('10:16:06');
6060
expect(decodeResult.formatted.items[1].code).toBe('TAIL');

lib/plugins/Label_4A.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ export class Label_4A extends DecoderPlugin {
3838
ResultFormatter.callsign(decodeResult, fields[3]);
3939
ResultFormatter.departureAirport(decodeResult, fields[4]);
4040
ResultFormatter.arrivalAirport(decodeResult, fields[5]);
41-
ResultFormatter.altitude(decodeResult, Number(text.substring(48, 51)) * 100);
41+
const alt = text.substring(48, 51);
42+
if(alt !== '') {
43+
ResultFormatter.altitude(decodeResult, Number(alt) * 100);
44+
}
4245
ResultFormatter.unknownArr(decodeResult, fields.slice(8));
4346
} else if (fields.length === 6) {
4447
if (fields[0].match(/^[NS]/)) {

lib/plugins/official.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export * from './Label_13Through18_Slash';
88
export * from './Label_15';
99
export * from './Label_15_FST';
1010
export * from './Label_16_N_Space';
11+
export * from './Label_16_POSA1';
12+
export * from './Label_16_TOD';
1113
export * from './Label_1M_Slash';
1214
export * from './Label_1L_3-line';
1315
export * from './Label_1L_070';

lib/utils/result_formatter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ export class ResultFormatter {
7474
}
7575

7676
static altitude(decodeResult: DecodeResult, value: number) {
77+
if(isNaN(value)) {
78+
return;
79+
}
7780
decodeResult.raw.altitude = value;
7881
decodeResult.formatted.items.push({
7982
type: 'altitude',

0 commit comments

Comments
 (0)