diff --git a/lib/MessageDecoder.ts b/lib/MessageDecoder.ts index 884f9be..8ff6dbb 100644 --- a/lib/MessageDecoder.ts +++ b/lib/MessageDecoder.ts @@ -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)); diff --git a/lib/plugins/Label_H2_02E.test.ts b/lib/plugins/Label_H2_02E.test.ts new file mode 100644 index 0000000..7ede384 --- /dev/null +++ b/lib/plugins/Label_H2_02E.test.ts @@ -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"); + }); +}); diff --git a/lib/plugins/Label_H2_02E.ts b/lib/plugins/Label_H2_02E.ts new file mode 100644 index 0000000..dfdee48 --- /dev/null +++ b/lib/plugins/Label_H2_02E.ts @@ -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 {}; diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index 3472459..cbd5e7a 100644 --- a/lib/plugins/official.ts +++ b/lib/plugins/official.ts @@ -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'; diff --git a/lib/types/wind.ts b/lib/types/wind.ts new file mode 100644 index 0000000..9beebcd --- /dev/null +++ b/lib/types/wind.ts @@ -0,0 +1,12 @@ +import { Waypoint } from "./waypoint"; + +export interface Wind { + waypoint: Waypoint; + flightLevel: number; + windDirection: number; + windSpeed: number; + temperature?: { + flightLevel: number; + degreesC: number; + }; +} \ No newline at end of file diff --git a/lib/utils/h1_helper.ts b/lib/utils/h1_helper.ts index 4ad1699..66251b6 100644 --- a/lib/utils/h1_helper.ts +++ b/lib/utils/h1_helper.ts @@ -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"; @@ -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)); @@ -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, @@ -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); } diff --git a/lib/utils/result_formatter.ts b/lib/utils/result_formatter.ts index c6bfc2c..8d02591 100644 --- a/lib/utils/result_formatter.ts +++ b/lib/utils/result_formatter.ts @@ -3,8 +3,8 @@ import { DecodeResult } from "../DecoderPluginInterface"; import { CoordinateUtils } from "./coordinate_utils"; import { DateTimeUtils } from "../DateTimeUtils"; import { RouteUtils } from "./route_utils"; -import { Waypoint } from "../types/waypoint"; import { Route } from "../types/route"; +import { Wind } from "../types/wind"; /** * Class to format the results of common fields @@ -393,6 +393,23 @@ export class ResultFormatter { }); } + static windData(decodeResult: DecodeResult, windData: Wind[]) { + decodeResult.raw.wind_data = windData; + for(const wind of windData) { + let text = `${RouteUtils.waypointToString(wind.waypoint)} at FL${wind.flightLevel}: ${wind.windDirection}° at ${wind.windSpeed}kt`; + if (wind.temperature) { + text += `, ${wind.temperature.degreesC}°C at FL${wind.temperature.flightLevel}`; + + } + decodeResult.formatted.items.push({ + type: 'wind_data', + code: 'WIND', + label: 'Wind Data', + value: text, + }); + } + } + static unknown(decodeResult: DecodeResult, value: string, sep: string = ',') { if (!decodeResult.remaining.text) decodeResult.remaining.text = value; @@ -403,4 +420,5 @@ export class ResultFormatter { static unknownArr(decodeResult: DecodeResult, value: string[], sep: string = ',') { this.unknown(decodeResult, value.join(sep), sep); }; + }