-
-
Notifications
You must be signed in to change notification settings - Fork 15
Adding Label H2 header 02E parser #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
makrsmark
merged 10 commits into
airframesio:master
from
makrsmark:feature/label-h1-weather
Dec 23, 2025
+320
−19
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
116c288
Adding Label H1 header 02E20 parser
makrsmark 30dc18b
Update lib/utils/h1_helper.ts
makrsmark 0648bd2
PR feedback
makrsmark b8f2be4
fix waypoint name
makrsmark 2cc4fe3
Add Day of Month
makrsmark b7b0837
missing files
makrsmark c66a6a2
rename
makrsmark 735359f
fix label
makrsmark 0ad0f48
specify radix
makrsmark 68250e9
header validation
makrsmark File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { Waypoint } from "./waypoint"; | ||
|
|
||
| export interface Wind { | ||
| waypoint: Waypoint; | ||
| flightLevel: number; | ||
| windDirection: number; | ||
| windSpeed: number; | ||
| temperature?: { | ||
| flightLevel: number; | ||
| degreesC: number; | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
Weatherand have an altitude with units (flight level or feet)There was a problem hiding this comment.
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!