Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/MessageDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class MessageDecoder {
this.registerPlugin(new Plugins.Label_4T_AGFSR(this));
this.registerPlugin(new Plugins.Label_4T_ETA(this));
this.registerPlugin(new Plugins.Label_B6_Forwardslash(this));
this.registerPlugin(new Plugins.Label_H2_02E(this));
this.registerPlugin(new Plugins.Label_H1_FLR(this));
this.registerPlugin(new Plugins.Label_H1_OHMA(this));
this.registerPlugin(new Plugins.Label_H1_WRN(this));
Expand Down
165 changes: 165 additions & 0 deletions lib/plugins/Label_H2_02E.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { MessageDecoder } from "../MessageDecoder";
import { Label_H2_02E } from "./Label_H2_02E";

describe("Label_H2 02E", () => {
let plugin: Label_H2_02E;

beforeEach(() => {
const decoder = new MessageDecoder();
plugin = new Label_H2_02E(decoder);
});
test("matches qualifiers", () => {
expect(plugin.decode).toBeDefined();
expect(plugin.name).toBe("label-h2-02e");
expect(plugin.qualifiers).toBeDefined();
expect(plugin.qualifiers()).toEqual({
labels: ["H2"],
preambles: ["02E"],
});
});

test("decodes discord example 1", () => {
const text =
"02E20HEGNLKPRN40359E02208116253601M627259020G QN41179E02134316323599M617247037G QN41591E02100516393603M610266040G QN42393E02026716463602M600276033G QN43197E01954316533598M592299037G QN44023E01929517003596M587313033G Q";
const decodeResult = plugin.decode({ text: text });
/*
Route: HEGN-LKPR
1 40°35.9'N, 022°08.1'E 16:25 FL360 36,000 ft -62.7°C 259°/20kts
2 41°17.9'N, 021°34.3'E 16:32 FL359 35,900 ft -61.7°C 247°/37kts
3 41°59.1'N, 021°00.5'E 16:39 FL360 36,000 ft -61.0°C 266°/40kts
4 42°39.3'N, 020°26.7'E 16:46 FL360 36,000 ft -60.0°C 276°/33kts
5 43°19.7'N, 019°54.3'E 16:53 FL359 35,900 ft -59.2°C 299°/37kts
6 44°02.3'N, 019°29.5'E 17:00 FL359 35,900 ft -58.7°C 313°/33kts
*/
expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe("full");
expect(decodeResult.formatted.description).toBe("Weather Report");
expect(decodeResult.message.text).toBe(text);
const weather = decodeResult.raw.wind_data;
expect(weather.length).toBe(6);
expect(decodeResult.formatted.items[0].label).toBe("Day of Month");
expect(typeof decodeResult.formatted.items[0].value).toBe("string");
expect(decodeResult.formatted.items[1].label).toBe("Origin");
expect(decodeResult.formatted.items[1].value).toBe("HEGN");
expect(decodeResult.formatted.items[2].label).toBe("Destination");
expect(decodeResult.formatted.items[2].value).toBe("LKPR");
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
expect(decodeResult.formatted.items[3].value).toBe(
"N40359E022081(40.598 N, 22.135 E)@16:25:00 at FL360: 259° at 20kt, -62.7°C at FL360"
);
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
expect(decodeResult.formatted.items[4].value).toBe(
"N41179E021343(41.298 N, 21.572 E)@16:32:00 at FL359: 247° at 37kt, -61.7°C at FL359"
);
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
expect(decodeResult.formatted.items[5].value).toBe(
"N41591E021005(41.985 N, 21.008 E)@16:39:00 at FL360: 266° at 40kt, -61°C at FL360"
);
expect(decodeResult.formatted.items[6].label).toBe("Wind Data");
expect(decodeResult.formatted.items[6].value).toBe(
"N42393E020267(42.655 N, 20.445 E)@16:46:00 at FL360: 276° at 33kt, -60°C at FL360"
);
expect(decodeResult.formatted.items[7].label).toBe("Wind Data");
expect(decodeResult.formatted.items[7].value).toBe(
"N43197E019543(43.328 N, 19.905 E)@16:53:00 at FL359: 299° at 37kt, -59.2°C at FL359"
);
expect(decodeResult.formatted.items[8].label).toBe("Wind Data");
expect(decodeResult.formatted.items[8].value).toBe(
"N44023E019295(44.038 N, 19.492 E)@17:00:00 at FL359: 313° at 33kt, -58.7°C at FL359"
);
});

test("decodes discord example 2", () => {
const text =
"02E20EGKKLBSFN45081E01757116493501M577327021G QN44401E01903016563499M575352028G QN44115E02008017033468M550319029G QN43420E02112317103296M525299036G QN43125E02214517172023M277271022G Q";
const decodeResult = plugin.decode({ text: text });

/*
Route: EGKK-LBSF
1 FL350 ~35,000 ft -57.7°C 327°/21kts
2 FL349 ~34,900 ft -57.5°C 352°/28kts
3 FL346 ~34,600 ft -55.0°C 319°/29kts
4 FL329 ~32,900 ft -52.5°C 299°/36kts
5 FL202 ~20,200 ft -27.7°C 271°/22kts
*/
expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe("full");
expect(decodeResult.formatted.description).toBe("Weather Report");
expect(decodeResult.message.text).toBe(text);
const weather = decodeResult.raw.wind_data;
expect(weather.length).toBe(5);
expect(decodeResult.formatted.items[0].label).toBe("Day of Month");
expect(typeof decodeResult.formatted.items[0].value).toBe("string");
expect(decodeResult.formatted.items[1].label).toBe("Origin");
expect(decodeResult.formatted.items[1].value).toBe("EGKK");
expect(decodeResult.formatted.items[2].label).toBe("Destination");
expect(decodeResult.formatted.items[2].value).toBe("LBSF");
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
expect(decodeResult.formatted.items[3].value).toBe(
"N45081E017571(45.135 N, 17.952 E)@16:49:00 at FL350: 327° at 21kt, -57.7°C at FL350"
);
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
expect(decodeResult.formatted.items[4].value).toBe(
"N44401E019030(44.668 N, 19.050 E)@16:56:00 at FL349: 352° at 28kt, -57.5°C at FL349"
);
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
expect(decodeResult.formatted.items[5].value).toBe(
"N44115E020080(44.192 N, 20.133 E)@17:03:00 at FL346: 319° at 29kt, -55°C at FL346"
);
expect(decodeResult.formatted.items[6].label).toBe("Wind Data");
expect(decodeResult.formatted.items[6].value).toBe(
"N43420E021123(43.700 N, 21.205 E)@17:10:00 at FL329: 299° at 36kt, -52.5°C at FL329"
);
expect(decodeResult.formatted.items[7].label).toBe("Wind Data");
expect(decodeResult.formatted.items[7].value).toBe(
"N43125E022145(43.208 N, 22.242 E)@17:17:00 at FL202: 271° at 22kt, -27.7°C at FL202"
);
});

test("decodes website example", () => {
// https://app.airframes.io/messages/6025352132
const text =
"02E20EIDWKORDN44087W08505523383800M470251091G QN43210W08520623452813M442251113G QN42461W08539523522189M295256121G QN42380W08623723591780M227266100G Q";
const decodeResult = plugin.decode({ text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe("full");
expect(decodeResult.formatted.description).toBe("Weather Report");
expect(decodeResult.message.text).toBe(text);
const weather = decodeResult.raw.wind_data;
expect(weather.length).toBe(4);
expect(decodeResult.formatted.items[0].label).toBe("Day of Month");
expect(typeof decodeResult.formatted.items[0].value).toBe("string");
expect(decodeResult.formatted.items[1].label).toBe("Origin");
expect(decodeResult.formatted.items[1].value).toBe("EIDW");
expect(decodeResult.formatted.items[2].label).toBe("Destination");
expect(decodeResult.formatted.items[2].value).toBe("KORD");
expect(decodeResult.formatted.items[3].label).toBe("Wind Data");
expect(decodeResult.formatted.items[3].value).toBe(
"N44087W085055(44.145 N, 85.092 W)@23:38:00 at FL380: 251° at 91kt, -47°C at FL380"
);
expect(decodeResult.formatted.items[4].label).toBe("Wind Data");
expect(decodeResult.formatted.items[4].value).toBe(
"N43210W085206(43.350 N, 85.343 W)@23:45:00 at FL281: 251° at 113kt, -44.2°C at FL281"
);
expect(decodeResult.formatted.items[5].label).toBe("Wind Data");
expect(decodeResult.formatted.items[5].value).toBe(
"N42461W085395(42.768 N, 85.658 W)@23:52:00 at FL218: 256° at 121kt, -29.5°C at FL218"
);
expect(decodeResult.formatted.items[6].label).toBe("Wind Data");
expect(decodeResult.formatted.items[6].value).toBe(
"N42380W086237(42.633 N, 86.395 W)@23:59:00 at FL178: 266° at 100kt, -22.7°C at FL178"
);
});

test("decodes invalid message", () => {
const text = "02E20INVALID MESSAGE TEXT";
const decodeResult = plugin.decode({ text: text });

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe("none");
expect(decodeResult.formatted.description).toBe("Weather Report");
expect(decodeResult.message.text).toBe(text);
expect(decodeResult.remaining.text).toBe("02E20INVALID MESSAGE TEXT");
});
});
113 changes: 113 additions & 0 deletions lib/plugins/Label_H2_02E.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { DateTimeUtils } from "../DateTimeUtils";
import { DecoderPlugin } from "../DecoderPlugin";
import { DecodeResult, Message, Options } from "../DecoderPluginInterface";
import { Wind } from "../types/wind";
import { CoordinateUtils } from "../utils/coordinate_utils";
import { ResultFormatter } from "../utils/result_formatter";

export class Label_H2_02E extends DecoderPlugin {
name = "label-h2-02e";

qualifiers() {
// eslint-disable-line class-methods-use-this
return {
labels: ["H2"],
preambles: ["02E"],
};
}

decode(message: Message, options: Options = {}): DecodeResult {
let decodeResult = this.defaultResult();
decodeResult.decoder.name = this.name;
decodeResult.formatted.description = "Weather Report";
decodeResult.message = message;

const parts = message.text.split(" ");

if (parts[parts.length - 1] !== "Q") {
// not a valid message
decodeResult.remaining.text = message.text;
decodeResult.decoded = false;
decodeResult.decoder.decodeLevel = "none";
return decodeResult;
}

const windData: Wind[] = [];
decodeResult.remaining.text = "";

const header = parts[0];
if (header.length === 45) {
// header.substring(0,3) is '02E'
ResultFormatter.day(decodeResult, parseInt(header.substring(3, 5), 10));
ResultFormatter.departureAirport(decodeResult, header.substring(5, 9));
ResultFormatter.arrivalAirport(decodeResult, header.substring(9, 13));
const firstWind = this.parseWeatherReport(header.substring(13));
if (firstWind) {
windData.push(firstWind);
} else {
decodeResult.remaining.text +=
(decodeResult.remaining.text ? " " : "") + header.substring(13);
}
}

for (let i = 1; i < parts.length - 1; i++) {
const part = parts[i];
if (part[0] !== "Q") {
decodeResult.remaining.text +=
(decodeResult.remaining.text ? " " : "") + part;
continue;
}
const wind = this.parseWeatherReport(part.substring(1));
if (wind) {
windData.push(wind);
} else {
decodeResult.remaining.text +=
(decodeResult.remaining.text ? " " : "") + part;
}
}

ResultFormatter.windData(decodeResult, windData);
decodeResult.decoded = true;
decodeResult.decoder.decodeLevel =
decodeResult.remaining.text.length === 0 ? "full" : "partial";
return decodeResult;
}

private parseWeatherReport(text: string): Wind | null {
const posString = text.substring(0, 13);
const pos =
CoordinateUtils.decodeStringCoordinatesDecimalMinutes(posString);
if (text.length !== 32 || !pos) {
return null;
}
const tod = DateTimeUtils.convertHHMMSSToTod(text.substring(13, 17));
const flightLevel = parseInt(text.substring(17, 20), 10);
// const altitude = parseInt(text.substring(17,21), 10) * 10; // use FL instead
const tempSign = text[21] === "M" ? -1 : 1;
const tempDegreesRaw = parseInt(text.substring(22, 25), 10);
const tempDegrees = tempSign * (tempDegreesRaw / 10);
const windDirection = parseInt(text.substring(25, 28), 10);
const windSpeed = parseInt(text.substring(28, 31), 10);
// G?
if (text[31] !== "G") {
return null;
}
return {
waypoint: {
name: posString,
latitude: pos.latitude,
longitude: pos.longitude,
time: tod,
timeFormat: "tod",
},
flightLevel: flightLevel,
windDirection: windDirection,
windSpeed: windSpeed,
temperature: {
flightLevel: flightLevel,
degreesC: tempDegrees,
},
};
}
}
export default {};
1 change: 1 addition & 0 deletions lib/plugins/official.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export * from './Label_8E';
export * from './Label_B6';
export * from './Label_ColonComma';
export * from './Label_H1';
export * from './Label_H2_02E';
export * from './Label_H1_FLR';
export * from './Label_H1_OHMA';
export * from './Label_H1_Slash';
Expand Down
12 changes: 12 additions & 0 deletions lib/types/wind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Waypoint } from "./waypoint";

export interface Wind {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to make this Weather and have an altitude with units (flight level or feet)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me think further about us needing to find a standard unit (with conversion capability) in everything we do!

waypoint: Waypoint;
flightLevel: number;
windDirection: number;
windSpeed: number;
temperature?: {
flightLevel: number;
degreesC: number;
};
}
27 changes: 9 additions & 18 deletions lib/utils/h1_helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DateTimeUtils } from "../DateTimeUtils";
import { DecodeResult } from "../DecoderPluginInterface";
import { Waypoint } from "../types/waypoint";
import { Wind } from "../types/wind";
import { CoordinateUtils } from "./coordinate_utils";
import { FlightPlanUtils } from "./flight_plan_utils";
import { ResultFormatter } from "./result_formatter";
Expand Down Expand Up @@ -300,14 +301,13 @@ function processRoute(decodeResult: DecodeResult, last: string, time: string, ne


function processWindData(decodeResult: DecodeResult, message: string) {
if (decodeResult.raw.wind_data === undefined) {
decodeResult.raw.wind_data = [];
}
const wind = [] as Wind[];

const flightLevel = Number(message.slice(0, 3));
const fields = message.slice(4).split('.'); // strip off altitude and comma
fields.forEach((field) => {
const data = field.split(',');
const waypoint = data[0];
const waypoint = {name: data[0]};
const windData = data[1];
const windDirection = Number(windData.slice(0, 3));
const windSpeed = Number(windData.slice(3));
Expand All @@ -317,7 +317,7 @@ function processWindData(decodeResult: DecodeResult, message: string) {
const tempFlightLevel = Number(tempData.slice(0, 3));
const tempString = tempData.slice(3);
const tempDegrees = Number(tempString.substring(1)) * (tempString.charAt(0) === 'M' ? -1 : 1);
decodeResult.raw.wind_data.push({
wind.push({
waypoint: waypoint,
flightLevel: flightLevel,
windDirection: windDirection,
Expand All @@ -327,25 +327,16 @@ function processWindData(decodeResult: DecodeResult, message: string) {
degreesC: tempDegrees
},
});
decodeResult.formatted.items.push({
type: 'wind_data',
code: 'WIND',
label: 'Wind Data',
value: `${waypoint} at FL${flightLevel}: ${windDirection}° at ${windSpeed}kt, ${tempDegrees}°C at FL${tempFlightLevel}`,
});

} else {
decodeResult.raw.wind_data.push({
wind.push({
waypoint: waypoint,
flightLevel: flightLevel,
windDirection: windDirection,
windSpeed: windSpeed,
});
decodeResult.formatted.items.push({
type: 'wind_data',
code: 'WIND',
label: 'Wind Data',
value: `${waypoint} at FL${flightLevel}: ${windDirection}° at ${windSpeed}kt`,
});
}
});

ResultFormatter.windData(decodeResult, wind);
}
Loading