From 89091c85581d8b016394dbfc3254b595f1462970 Mon Sep 17 00:00:00 2001 From: Andre Paquette Date: Tue, 21 Apr 2026 20:34:06 -0400 Subject: [PATCH] Add Label 5Z decoder plugin New plugin registered in official.ts and MessageDecoder.ts. --- lib/MessageDecoder.ts | 1 + lib/plugins/Label_5Z_AA.ts | 181 +++++++++++++++++++++++++++++++++++++ lib/plugins/official.ts | 1 + 3 files changed, 183 insertions(+) create mode 100644 lib/plugins/Label_5Z_AA.ts diff --git a/lib/MessageDecoder.ts b/lib/MessageDecoder.ts index b40f43a..7512fe5 100644 --- a/lib/MessageDecoder.ts +++ b/lib/MessageDecoder.ts @@ -78,6 +78,7 @@ const pluginClasses = [ Plugins.Label_QQ, Plugins.Label_QR, Plugins.Label_QS, + Plugins.Label_5Z_AA, ]; export class MessageDecoder { diff --git a/lib/plugins/Label_5Z_AA.ts b/lib/plugins/Label_5Z_AA.ts new file mode 100644 index 0000000..6b002af --- /dev/null +++ b/lib/plugins/Label_5Z_AA.ts @@ -0,0 +1,181 @@ +import { DecoderPlugin } from '../DecoderPlugin'; +import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; +import { ResultFormatter } from '../utils/result_formatter'; +import { DateTimeUtils } from '../DateTimeUtils'; + +/** + * Label 5Z — Airline Designated Downlink, AA / US Airways "Variant 1" + * + * No leading `/` preamble (that variant is handled by Label_5Z_Slash). + * Used by American Airlines and US Airways for Operational Status (OS) + * downlinks reporting in-range / clearance / similar arrival advisories. + * + * Wire format: + * + * OS KMSN / IR KMSN 2209 + * | | | | | + * | | | | └── Optional 4-digit time HHMM (UTC) — typ. estimated arrival + * | | | └─────── Optional repeated airport ICAO + * | | └──────────── Sub-command (IR = In Range, CLR = Clear, ...) + * | └──────────────────── Reference / destination airport ICAO + * └──────────────────────── Command (OS = Out-Safe / Operational Status) + * + * Documented examples: + * OS KPHX /CLR + * OS KSFO /IR KSFO0312 + * OS KMSN /IR KMSN2209 + */ +export class Label_5Z_AA extends DecoderPlugin { + name = 'label-5z-aa'; + + private readonly commandDescriptions: Record = { + OS: 'Operational Status (Out-Safe)', + }; + + private readonly subCommandDescriptions: Record = { + IR: 'In Range', + CLR: 'Clear', + OUT: 'Out of Gate', + OFF: 'Off (departed)', + ON: 'On (touchdown)', + IN: 'In Gate', + FTM: 'Free-Text Message', + ALT: 'Altimeter / Arrival Data', + }; + + qualifiers() { + return { + labels: ['5Z'], + }; + } + + decode(message: Message, options: Options = {}): DecodeResult { + const decodeResult = this.initResult( + message, + 'Airline Designated Downlink (AA / US Airways Variant 1)', + ); + + const text = (message.text || '').trim(); + if (!text) { + this.setDecodeLevel(decodeResult, false); + return decodeResult; + } + + // Reject 5Z messages that begin with `/` — those are the United Airlines + // sub-format and are handled by Label_5Z_Slash. + if (text.startsWith('/')) { + this.setDecodeLevel(decodeResult, false); + return decodeResult; + } + + // Pattern: /[ ] followed by optional + // free-text body (newline-separated) — e.g. FTM messages carry crew + // requests / dispatcher notes after the subcommand. + const normalized = text.replace(/\r/g, ''); + const header = normalized.split('\n')[0].trim(); + const bodyLines = normalized + .split('\n') + .slice(1) + .map((l) => l.trim()) + .filter((l) => l.length > 0); + + // Accepted header trailer shapes after `/`: + // (a) (empty) — e.g. `OS KPHX /CLR` + // (b) — e.g. `OS KMSN /IR KMSN2209` + // (c) — e.g. `OS KDFW/ALT00001930` + // (ALT sub-code — altimeter + ETA) + const regex = + /^(?[A-Z]{1,3})\s+(?[A-Z]{3,4})\s*\/(?[A-Z]{1,4})(?:\s*(?:(?[A-Z]{3,4})(?\d{4})|(?\d{4})(?\d{4})))?\s*$/; + const m = header.match(regex); + if (!m?.groups) { + this.setDecodeLevel(decodeResult, false); + return decodeResult; + } + + const { cmd, icao, sub, altIcao, timeA, altimeter, timeB } = m.groups; + const time = timeA || timeB; + const freeText = bodyLines.join('\n'); + + decodeResult.raw.airline = 'American Airlines / US Airways'; + decodeResult.raw.command = cmd; + decodeResult.raw.subcommand = sub; + ResultFormatter.arrivalAirport(decodeResult, icao); + + if (altIcao) decodeResult.raw.alternate_icao = altIcao; + if (altimeter && altimeter !== '0000') { + // Documentation notes the altimeter 4-digit prefix may be obsolete / + // padding; only surface it when it carries a non-zero value. + decodeResult.raw.altimeter_raw = altimeter; + } + if (time) { + decodeResult.raw.estimated_arrival_time = `${time.substring(0, 2)}:${time.substring(2, 4)}`; + ResultFormatter.eta( + decodeResult, + DateTimeUtils.convertHHMMSSToTod(time + '00'), + ); + } + + // ── Formatted output (one row per field) ── + decodeResult.formatted.items.unshift( + { + type: 'airline', + code: 'AIRLINE', + label: 'Airline', + value: 'American Airlines / US Airways', + }, + { + type: 'message_type', + code: 'MSGTYP', + label: 'Message Type', + value: 'Airline Designated Downlink (AA Variant 1)', + }, + ); + const cmdLabel = this.commandDescriptions[cmd] + ? `${cmd} (${this.commandDescriptions[cmd]})` + : cmd; + decodeResult.formatted.items.push( + { + type: 'command', + code: 'CMD', + label: 'Command', + value: cmdLabel, + }, + { + type: 'subcommand', + code: 'SUBCMD', + label: 'Sub-command', + value: this.subCommandDescriptions[sub] + ? `${sub} (${this.subCommandDescriptions[sub]})` + : sub, + }, + ); + if (altIcao && altIcao !== icao) { + decodeResult.formatted.items.push({ + type: 'alt_icao', + code: 'ALTICAO', + label: 'Repeated ICAO', + value: altIcao, + }); + } + if (altimeter && altimeter !== '0000') { + decodeResult.formatted.items.push({ + type: 'altimeter', + code: 'ALTIM', + label: 'Altimeter (raw)', + value: altimeter, + }); + } + if (freeText) { + decodeResult.raw.free_text = freeText; + decodeResult.formatted.items.push({ + type: 'free_text', + code: 'FTEXT', + label: 'Free Text', + value: freeText, + }); + } + + this.setDecodeLevel(decodeResult, true, 'full'); + return decodeResult; + } +} diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index 0d3d6fa..c076e7a 100644 --- a/lib/plugins/official.ts +++ b/lib/plugins/official.ts @@ -64,3 +64,4 @@ export * from './Label_QR'; export * from './Label_QP'; export * from './Label_QS'; export * from './Label_QQ'; +export * from './Label_5Z_AA';