Skip to content

Commit 116c288

Browse files
committed
Adding Label H1 header 02E20 parser
1 parent b47d71e commit 116c288

File tree

7 files changed

+311
-19
lines changed

7 files changed

+311
-19
lines changed

lib/MessageDecoder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class MessageDecoder {
5656
this.registerPlugin(new Plugins.Label_4T_AGFSR(this));
5757
this.registerPlugin(new Plugins.Label_4T_ETA(this));
5858
this.registerPlugin(new Plugins.Label_B6_Forwardslash(this));
59+
this.registerPlugin(new Plugins.Label_H1_02E20(this));
5960
this.registerPlugin(new Plugins.Label_H1_FLR(this));
6061
this.registerPlugin(new Plugins.Label_H1_OHMA(this));
6162
this.registerPlugin(new Plugins.Label_H1_WRN(this));

lib/plugins/Label_H1_02E20.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { MessageDecoder } from "../MessageDecoder";
2+
import { Label_H1_02E20 } from "./Label_H1_02E20";
3+
4+
describe("Label_H1 02E20", () => {
5+
let plugin: Label_H1_02E20;
6+
7+
beforeEach(() => {
8+
const decoder = new MessageDecoder();
9+
plugin = new Label_H1_02E20(decoder);
10+
});
11+
test("matches qualifiers", () => {
12+
expect(plugin.decode).toBeDefined();
13+
expect(plugin.name).toBe("label-h1-02e20");
14+
expect(plugin.qualifiers).toBeDefined();
15+
expect(plugin.qualifiers()).toEqual({
16+
labels: ["H1"],
17+
preambles: ["02E20"],
18+
});
19+
});
20+
21+
test("decodes discord example 1", () => {
22+
const text =
23+
"02E20HEGNLKPRN40359E02208116253601M627259020G QN41179E02134316323599M617247037G QN41591E02100516393603M610266040G QN42393E02026716463602M600276033G QN43197E01954316533598M592299037G QN44023E01929517003596M587313033G Q";
24+
const decodeResult = plugin.decode({ text: text });
25+
/*
26+
Route: HEGN-LKPR
27+
1 40°35.9'N, 022°08.1'E 16:25 FL360 36,000 ft -62.7°C 259°/20kts
28+
2 41°17.9'N, 021°34.3'E 16:32 FL359 35,900 ft -61.7°C 247°/37kts
29+
3 41°59.1'N, 021°00.5'E 16:39 FL360 36,000 ft -61.0°C 266°/40kts
30+
4 42°39.3'N, 020°26.7'E 16:46 FL360 36,000 ft -60.0°C 276°/33kts
31+
5 43°19.7'N, 019°54.3'E 16:53 FL359 35,900 ft -59.2°C 299°/37kts
32+
6 44°02.3'N, 019°29.5'E 17:00 FL359 35,900 ft -58.7°C 313°/33kts
33+
*/
34+
expect(decodeResult.decoded).toBe(true);
35+
expect(decodeResult.decoder.decodeLevel).toBe("full");
36+
expect(decodeResult.formatted.description).toBe("Weather Report");
37+
expect(decodeResult.message.text).toBe(text);
38+
const weather = decodeResult.raw.wind_data;
39+
expect(weather.length).toBe(6);
40+
expect(decodeResult.formatted.items[0].label).toBe("Origin");
41+
expect(decodeResult.formatted.items[0].value).toBe("HEGN");
42+
expect(decodeResult.formatted.items[1].label).toBe("Destination");
43+
expect(decodeResult.formatted.items[1].value).toBe("LKPR");
44+
expect(decodeResult.formatted.items[2].label).toBe("Wind Data");
45+
expect(decodeResult.formatted.items[2].value).toBe(
46+
"N40359E02208(40.598 N, 22.135 E)@16:25:00 at FL360: 259° at 20kt, -62.7°C at FL360"
47+
);
48+
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
49+
expect(decodeResult.formatted.items[3].value).toBe(
50+
"N41179E02134(41.298 N, 21.572 E)@16:32:00 at FL359: 247° at 37kt, -61.7°C at FL359"
51+
);
52+
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
53+
expect(decodeResult.formatted.items[4].value).toBe(
54+
"N41591E02100(41.985 N, 21.008 E)@16:39:00 at FL360: 266° at 40kt, -61°C at FL360"
55+
);
56+
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
57+
expect(decodeResult.formatted.items[5].value).toBe(
58+
"N42393E02026(42.655 N, 20.445 E)@16:46:00 at FL360: 276° at 33kt, -60°C at FL360"
59+
);
60+
expect(decodeResult.formatted.items[6].label).toBe("Wind Data");
61+
expect(decodeResult.formatted.items[6].value).toBe(
62+
"N43197E01954(43.328 N, 19.905 E)@16:53:00 at FL359: 299° at 37kt, -59.2°C at FL359"
63+
);
64+
expect(decodeResult.formatted.items[7].label).toBe("Wind Data");
65+
expect(decodeResult.formatted.items[7].value).toBe(
66+
"N44023E01929(44.038 N, 19.492 E)@17:00:00 at FL359: 313° at 33kt, -58.7°C at FL359"
67+
);
68+
});
69+
70+
test("decodes discord example 2", () => {
71+
const text =
72+
"02E20EGKKLBSFN45081E01757116493501M577327021G QN44401E01903016563499M575352028G QN44115E02008017033468M550319029G QN43420E02112317103296M525299036G QN43125E02214517172023M277271022G Q";
73+
const decodeResult = plugin.decode({ text: text });
74+
75+
/*
76+
Route: EGKK-LBSF
77+
1 FL350 ~35,000 ft -57.7°C 327°/21kts
78+
2 FL349 ~34,900 ft -57.5°C 352°/28kts
79+
3 FL346 ~34,600 ft -55.0°C 319°/29kts
80+
4 FL329 ~32,900 ft -52.5°C 299°/36kts
81+
5 FL202 ~20,200 ft -27.7°C 271°/22kts
82+
*/
83+
expect(decodeResult.decoded).toBe(true);
84+
expect(decodeResult.decoder.decodeLevel).toBe("full");
85+
expect(decodeResult.formatted.description).toBe("Weather Report");
86+
expect(decodeResult.message.text).toBe(text);
87+
const weather = decodeResult.raw.wind_data;
88+
expect(weather.length).toBe(5);
89+
expect(decodeResult.formatted.items[0].label).toBe("Origin");
90+
expect(decodeResult.formatted.items[0].value).toBe("EGKK");
91+
expect(decodeResult.formatted.items[1].label).toBe("Destination");
92+
expect(decodeResult.formatted.items[1].value).toBe("LBSF");
93+
expect(decodeResult.formatted.items[2].label).toBe("Wind Data");
94+
expect(decodeResult.formatted.items[2].value).toBe(
95+
"N45081E01757(45.135 N, 17.952 E)@16:49:00 at FL350: 327° at 21kt, -57.7°C at FL350"
96+
);
97+
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
98+
expect(decodeResult.formatted.items[3].value).toBe(
99+
"N44401E01903(44.668 N, 19.050 E)@16:56:00 at FL349: 352° at 28kt, -57.5°C at FL349"
100+
);
101+
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
102+
expect(decodeResult.formatted.items[4].value).toBe(
103+
"N44115E02008(44.192 N, 20.133 E)@17:03:00 at FL346: 319° at 29kt, -55°C at FL346"
104+
);
105+
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
106+
expect(decodeResult.formatted.items[5].value).toBe(
107+
"N43420E02112(43.700 N, 21.205 E)@17:10:00 at FL329: 299° at 36kt, -52.5°C at FL329"
108+
);
109+
expect(decodeResult.formatted.items[6].label).toBe("Wind Data");
110+
expect(decodeResult.formatted.items[6].value).toBe(
111+
"N43125E02214(43.208 N, 22.242 E)@17:17:00 at FL202: 271° at 22kt, -27.7°C at FL202"
112+
);
113+
});
114+
115+
test("decodes website example", () => {
116+
// https://app.airframes.io/messages/6025352132
117+
const text =
118+
"02E20EIDWKORDN44087W08505523383800M470251091G QN43210W08520623452813M442251113G QN42461W08539523522189M295256121G QN42380W08623723591780M227266100G Q";
119+
const decodeResult = plugin.decode({ text: text });
120+
121+
expect(decodeResult.decoded).toBe(true);
122+
expect(decodeResult.decoder.decodeLevel).toBe("full");
123+
expect(decodeResult.formatted.description).toBe("Weather Report");
124+
expect(decodeResult.message.text).toBe(text);
125+
const weather = decodeResult.raw.wind_data;
126+
expect(weather.length).toBe(4);
127+
expect(decodeResult.formatted.items[0].label).toBe("Origin");
128+
expect(decodeResult.formatted.items[0].value).toBe("EIDW");
129+
expect(decodeResult.formatted.items[1].label).toBe("Destination");
130+
expect(decodeResult.formatted.items[1].value).toBe("KORD");
131+
expect(decodeResult.formatted.items[2].label).toBe("Wind Data");
132+
expect(decodeResult.formatted.items[2].value).toBe(
133+
"N44087W08505(44.145 N, 85.092 W)@23:38:00 at FL380: 251° at 91kt, -47°C at FL380"
134+
);
135+
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
136+
expect(decodeResult.formatted.items[3].value).toBe(
137+
"N43210W08520(43.350 N, 85.343 W)@23:45:00 at FL281: 251° at 113kt, -44.2°C at FL281"
138+
);
139+
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
140+
expect(decodeResult.formatted.items[4].value).toBe(
141+
"N42461W08539(42.768 N, 85.658 W)@23:52:00 at FL218: 256° at 121kt, -29.5°C at FL218"
142+
);
143+
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
144+
expect(decodeResult.formatted.items[5].value).toBe(
145+
"N42380W08623(42.633 N, 86.395 W)@23:59:00 at FL178: 266° at 100kt, -22.7°C at FL178"
146+
);
147+
});
148+
149+
test("decodes invalid message", () => {
150+
const text = "02E20INVALID MESSAGE TEXT";
151+
const decodeResult = plugin.decode({ text: text });
152+
153+
expect(decodeResult.decoded).toBe(false);
154+
expect(decodeResult.decoder.decodeLevel).toBe("none");
155+
expect(decodeResult.formatted.description).toBe("Weather Report");
156+
expect(decodeResult.message.text).toBe(text);
157+
expect(decodeResult.remaining.text).toBe("02E20INVALID MESSAGE TEXT");
158+
});
159+
});

lib/plugins/Label_H1_02E20.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { DateTimeUtils } from '../DateTimeUtils';
2+
import { DecoderPlugin } from '../DecoderPlugin';
3+
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
4+
import { Wind } from '../types/wind';
5+
import { CoordinateUtils } from '../utils/coordinate_utils';
6+
import { ResultFormatter } from '../utils/result_formatter';
7+
8+
export class Label_H1_02E20 extends DecoderPlugin {
9+
name = 'label-h1-02e20';
10+
11+
qualifiers() { // eslint-disable-line class-methods-use-this
12+
return {
13+
labels: ["H1"],
14+
preambles: ['02E20'],
15+
};
16+
}
17+
18+
decode(message: Message, options: Options = {}) : DecodeResult {
19+
let decodeResult = this.defaultResult();
20+
decodeResult.decoder.name = this.name;
21+
decodeResult.formatted.description = 'Weather Report';
22+
decodeResult.message = message;
23+
24+
const parts = message.text.split(' ');
25+
26+
if(parts[parts.length-1] !== 'Q') {
27+
// not a valid message
28+
decodeResult.remaining.text = message.text;
29+
decodeResult.decoded = false;
30+
decodeResult.decoder.decodeLevel = 'none';
31+
return decodeResult;
32+
}
33+
34+
const windData: Wind[] = [];
35+
decodeResult.remaining.text = '';
36+
37+
const header = parts[0];
38+
// header.substring(0,5) is '02E20'
39+
ResultFormatter.departureAirport(decodeResult, header.substring(5,9));
40+
ResultFormatter.arrivalAirport(decodeResult, header.substring(9,13));
41+
const firstWind = this.parseWeatherReport(header.substring(13));
42+
if(firstWind) {
43+
windData.push(firstWind);
44+
} else {
45+
decodeResult.remaining.text += (decodeResult.remaining.text ? ' ' : '') + header.substring(13);
46+
}
47+
48+
for(let i=1; i<parts.length-1; i++) {
49+
const part = parts[i];
50+
if(part[0] !== 'Q') {
51+
decodeResult.remaining.text += (decodeResult.remaining.text ? ' ' : '') + part;
52+
continue
53+
}
54+
const wind = this.parseWeatherReport(part.substring(1));
55+
if(wind) {
56+
windData.push(wind);
57+
} else {
58+
decodeResult.remaining.text += (decodeResult.remaining.text ? ' ' : '') + part;
59+
}
60+
}
61+
62+
63+
64+
ResultFormatter.windData(decodeResult, windData);
65+
decodeResult.decoded = true;
66+
decodeResult.decoder.decodeLevel = decodeResult.remaining.text.length === 0 ? 'full' : 'partial';
67+
return decodeResult;
68+
}
69+
70+
parseWeatherReport(text: string): Wind | null {
71+
72+
//N40359E02208116253601M627259020G
73+
74+
//40°35.9'N, 022°08.1'E 16:25 FL360 36,000 ft -62.7°C 259°/20kts
75+
76+
const pos = CoordinateUtils.decodeStringCoordinatesDecimalMinutes(text.substring(0,13));
77+
if (text.length !== 32 || !pos) {
78+
return null;
79+
}
80+
const hhmm = text.substring(13,17);
81+
const flightLevel = parseInt(text.substring(17,20), 10);
82+
// const altitude = parseInt(text.substring(17,21), 10) * 10; // use FL instead
83+
const tempSign = text[21] === 'M' ? -1 : 1;
84+
const tempDegreesRaw = parseInt(text.substring(22,25), 10);
85+
const tempDegrees = tempSign * (tempDegreesRaw / 10);
86+
const windDirection = parseInt(text.substring(25,28), 10);
87+
const windSpeed = parseInt(text.substring(28,31), 10);
88+
// G?
89+
if(text[31] !== 'G') {
90+
return null;
91+
}
92+
return {
93+
waypoint: {
94+
name: text.substring(0,12),
95+
latitude: pos.latitude,
96+
longitude: pos.longitude,
97+
time: DateTimeUtils.convertHHMMSSToTod(hhmm),
98+
timeFormat: 'tod',
99+
},
100+
flightLevel: flightLevel,
101+
windDirection: windDirection,
102+
windSpeed: windSpeed,
103+
temperature: {
104+
flightLevel: flightLevel,
105+
degreesC: tempDegrees,
106+
},
107+
};
108+
}
109+
}
110+
export default {};

lib/plugins/official.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export * from './Label_8E';
4747
export * from './Label_B6';
4848
export * from './Label_ColonComma';
4949
export * from './Label_H1';
50+
export * from './Label_H1_02E20';
5051
export * from './Label_H1_FLR';
5152
export * from './Label_H1_OHMA';
5253
export * from './Label_H1_Slash';

lib/types/wind.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Waypoint } from "./waypoint";
2+
3+
export interface Wind {
4+
waypoint: Waypoint;
5+
flightLevel: number;
6+
windDirection: number;
7+
windSpeed: number;
8+
temperature?: {
9+
flightLevel: number;
10+
degreesC: number;
11+
};
12+
}

lib/utils/h1_helper.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DateTimeUtils } from "../DateTimeUtils";
22
import { DecodeResult } from "../DecoderPluginInterface";
33
import { Waypoint } from "../types/waypoint";
4+
import { Wind } from "../types/wind";
45
import { CoordinateUtils } from "./coordinate_utils";
56
import { FlightPlanUtils } from "./flight_plan_utils";
67
import { ResultFormatter } from "./result_formatter";
@@ -300,14 +301,13 @@ function processRoute(decodeResult: DecodeResult, last: string, time: string, ne
300301

301302

302303
function processWindData(decodeResult: DecodeResult, message: string) {
303-
if (decodeResult.raw.wind_data === undefined) {
304-
decodeResult.raw.wind_data = [];
305-
}
304+
const wind = [] as Wind[];
305+
306306
const flightLevel = Number(message.slice(0, 3));
307307
const fields = message.slice(4).split('.'); // strip off altitude and comma
308308
fields.forEach((field) => {
309309
const data = field.split(',');
310-
const waypoint = data[0];
310+
const waypoint = {name: data[0]};
311311
const windData = data[1];
312312
const windDirection = Number(windData.slice(0, 3));
313313
const windSpeed = Number(windData.slice(3));
@@ -317,7 +317,7 @@ function processWindData(decodeResult: DecodeResult, message: string) {
317317
const tempFlightLevel = Number(tempData.slice(0, 3));
318318
const tempString = tempData.slice(3);
319319
const tempDegrees = Number(tempString.substring(1)) * (tempString.charAt(0) === 'M' ? -1 : 1);
320-
decodeResult.raw.wind_data.push({
320+
wind.push({
321321
waypoint: waypoint,
322322
flightLevel: flightLevel,
323323
windDirection: windDirection,
@@ -327,25 +327,16 @@ function processWindData(decodeResult: DecodeResult, message: string) {
327327
degreesC: tempDegrees
328328
},
329329
});
330-
decodeResult.formatted.items.push({
331-
type: 'wind_data',
332-
code: 'WIND',
333-
label: 'Wind Data',
334-
value: `${waypoint} at FL${flightLevel}: ${windDirection}° at ${windSpeed}kt, ${tempDegrees}°C at FL${tempFlightLevel}`,
335-
});
330+
336331
} else {
337-
decodeResult.raw.wind_data.push({
332+
wind.push({
338333
waypoint: waypoint,
339334
flightLevel: flightLevel,
340335
windDirection: windDirection,
341336
windSpeed: windSpeed,
342337
});
343-
decodeResult.formatted.items.push({
344-
type: 'wind_data',
345-
code: 'WIND',
346-
label: 'Wind Data',
347-
value: `${waypoint} at FL${flightLevel}: ${windDirection}° at ${windSpeed}kt`,
348-
});
349338
}
350339
});
340+
341+
ResultFormatter.wind_data(decodeResult, wind);
351342
}

0 commit comments

Comments
 (0)