Skip to content

Commit 56cebf0

Browse files
makrsmarkCopilot
andauthored
Adding Label H2 header 02E parser (#294)
slight refactor for formatting --------- Co-authored-by: Copilot <[email protected]>
1 parent b47d71e commit 56cebf0

File tree

7 files changed

+320
-19
lines changed

7 files changed

+320
-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_H2_02E(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_H2_02E.test.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { MessageDecoder } from "../MessageDecoder";
2+
import { Label_H2_02E } from "./Label_H2_02E";
3+
4+
describe("Label_H2 02E", () => {
5+
let plugin: Label_H2_02E;
6+
7+
beforeEach(() => {
8+
const decoder = new MessageDecoder();
9+
plugin = new Label_H2_02E(decoder);
10+
});
11+
test("matches qualifiers", () => {
12+
expect(plugin.decode).toBeDefined();
13+
expect(plugin.name).toBe("label-h2-02e");
14+
expect(plugin.qualifiers).toBeDefined();
15+
expect(plugin.qualifiers()).toEqual({
16+
labels: ["H2"],
17+
preambles: ["02E"],
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("Day of Month");
41+
expect(typeof decodeResult.formatted.items[0].value).toBe("string");
42+
expect(decodeResult.formatted.items[1].label).toBe("Origin");
43+
expect(decodeResult.formatted.items[1].value).toBe("HEGN");
44+
expect(decodeResult.formatted.items[2].label).toBe("Destination");
45+
expect(decodeResult.formatted.items[2].value).toBe("LKPR");
46+
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
47+
expect(decodeResult.formatted.items[3].value).toBe(
48+
"N40359E022081(40.598 N, 22.135 E)@16:25:00 at FL360: 259° at 20kt, -62.7°C at FL360"
49+
);
50+
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
51+
expect(decodeResult.formatted.items[4].value).toBe(
52+
"N41179E021343(41.298 N, 21.572 E)@16:32:00 at FL359: 247° at 37kt, -61.7°C at FL359"
53+
);
54+
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
55+
expect(decodeResult.formatted.items[5].value).toBe(
56+
"N41591E021005(41.985 N, 21.008 E)@16:39:00 at FL360: 266° at 40kt, -61°C at FL360"
57+
);
58+
expect(decodeResult.formatted.items[6].label).toBe("Wind Data");
59+
expect(decodeResult.formatted.items[6].value).toBe(
60+
"N42393E020267(42.655 N, 20.445 E)@16:46:00 at FL360: 276° at 33kt, -60°C at FL360"
61+
);
62+
expect(decodeResult.formatted.items[7].label).toBe("Wind Data");
63+
expect(decodeResult.formatted.items[7].value).toBe(
64+
"N43197E019543(43.328 N, 19.905 E)@16:53:00 at FL359: 299° at 37kt, -59.2°C at FL359"
65+
);
66+
expect(decodeResult.formatted.items[8].label).toBe("Wind Data");
67+
expect(decodeResult.formatted.items[8].value).toBe(
68+
"N44023E019295(44.038 N, 19.492 E)@17:00:00 at FL359: 313° at 33kt, -58.7°C at FL359"
69+
);
70+
});
71+
72+
test("decodes discord example 2", () => {
73+
const text =
74+
"02E20EGKKLBSFN45081E01757116493501M577327021G QN44401E01903016563499M575352028G QN44115E02008017033468M550319029G QN43420E02112317103296M525299036G QN43125E02214517172023M277271022G Q";
75+
const decodeResult = plugin.decode({ text: text });
76+
77+
/*
78+
Route: EGKK-LBSF
79+
1 FL350 ~35,000 ft -57.7°C 327°/21kts
80+
2 FL349 ~34,900 ft -57.5°C 352°/28kts
81+
3 FL346 ~34,600 ft -55.0°C 319°/29kts
82+
4 FL329 ~32,900 ft -52.5°C 299°/36kts
83+
5 FL202 ~20,200 ft -27.7°C 271°/22kts
84+
*/
85+
expect(decodeResult.decoded).toBe(true);
86+
expect(decodeResult.decoder.decodeLevel).toBe("full");
87+
expect(decodeResult.formatted.description).toBe("Weather Report");
88+
expect(decodeResult.message.text).toBe(text);
89+
const weather = decodeResult.raw.wind_data;
90+
expect(weather.length).toBe(5);
91+
expect(decodeResult.formatted.items[0].label).toBe("Day of Month");
92+
expect(typeof decodeResult.formatted.items[0].value).toBe("string");
93+
expect(decodeResult.formatted.items[1].label).toBe("Origin");
94+
expect(decodeResult.formatted.items[1].value).toBe("EGKK");
95+
expect(decodeResult.formatted.items[2].label).toBe("Destination");
96+
expect(decodeResult.formatted.items[2].value).toBe("LBSF");
97+
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
98+
expect(decodeResult.formatted.items[3].value).toBe(
99+
"N45081E017571(45.135 N, 17.952 E)@16:49:00 at FL350: 327° at 21kt, -57.7°C at FL350"
100+
);
101+
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
102+
expect(decodeResult.formatted.items[4].value).toBe(
103+
"N44401E019030(44.668 N, 19.050 E)@16:56:00 at FL349: 352° at 28kt, -57.5°C at FL349"
104+
);
105+
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
106+
expect(decodeResult.formatted.items[5].value).toBe(
107+
"N44115E020080(44.192 N, 20.133 E)@17:03:00 at FL346: 319° at 29kt, -55°C at FL346"
108+
);
109+
expect(decodeResult.formatted.items[6].label).toBe("Wind Data");
110+
expect(decodeResult.formatted.items[6].value).toBe(
111+
"N43420E021123(43.700 N, 21.205 E)@17:10:00 at FL329: 299° at 36kt, -52.5°C at FL329"
112+
);
113+
expect(decodeResult.formatted.items[7].label).toBe("Wind Data");
114+
expect(decodeResult.formatted.items[7].value).toBe(
115+
"N43125E022145(43.208 N, 22.242 E)@17:17:00 at FL202: 271° at 22kt, -27.7°C at FL202"
116+
);
117+
});
118+
119+
test("decodes website example", () => {
120+
// https://app.airframes.io/messages/6025352132
121+
const text =
122+
"02E20EIDWKORDN44087W08505523383800M470251091G QN43210W08520623452813M442251113G QN42461W08539523522189M295256121G QN42380W08623723591780M227266100G Q";
123+
const decodeResult = plugin.decode({ text: text });
124+
125+
expect(decodeResult.decoded).toBe(true);
126+
expect(decodeResult.decoder.decodeLevel).toBe("full");
127+
expect(decodeResult.formatted.description).toBe("Weather Report");
128+
expect(decodeResult.message.text).toBe(text);
129+
const weather = decodeResult.raw.wind_data;
130+
expect(weather.length).toBe(4);
131+
expect(decodeResult.formatted.items[0].label).toBe("Day of Month");
132+
expect(typeof decodeResult.formatted.items[0].value).toBe("string");
133+
expect(decodeResult.formatted.items[1].label).toBe("Origin");
134+
expect(decodeResult.formatted.items[1].value).toBe("EIDW");
135+
expect(decodeResult.formatted.items[2].label).toBe("Destination");
136+
expect(decodeResult.formatted.items[2].value).toBe("KORD");
137+
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
138+
expect(decodeResult.formatted.items[3].value).toBe(
139+
"N44087W085055(44.145 N, 85.092 W)@23:38:00 at FL380: 251° at 91kt, -47°C at FL380"
140+
);
141+
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
142+
expect(decodeResult.formatted.items[4].value).toBe(
143+
"N43210W085206(43.350 N, 85.343 W)@23:45:00 at FL281: 251° at 113kt, -44.2°C at FL281"
144+
);
145+
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
146+
expect(decodeResult.formatted.items[5].value).toBe(
147+
"N42461W085395(42.768 N, 85.658 W)@23:52:00 at FL218: 256° at 121kt, -29.5°C at FL218"
148+
);
149+
expect(decodeResult.formatted.items[6].label).toBe("Wind Data");
150+
expect(decodeResult.formatted.items[6].value).toBe(
151+
"N42380W086237(42.633 N, 86.395 W)@23:59:00 at FL178: 266° at 100kt, -22.7°C at FL178"
152+
);
153+
});
154+
155+
test("decodes invalid message", () => {
156+
const text = "02E20INVALID MESSAGE TEXT";
157+
const decodeResult = plugin.decode({ text: text });
158+
159+
expect(decodeResult.decoded).toBe(false);
160+
expect(decodeResult.decoder.decodeLevel).toBe("none");
161+
expect(decodeResult.formatted.description).toBe("Weather Report");
162+
expect(decodeResult.message.text).toBe(text);
163+
expect(decodeResult.remaining.text).toBe("02E20INVALID MESSAGE TEXT");
164+
});
165+
});

lib/plugins/Label_H2_02E.ts

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

0 commit comments

Comments
 (0)