diff --git a/lib/MessageDecoder.ts b/lib/MessageDecoder.ts index b40f43a..56487f9 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_81_MVA, ]; export class MessageDecoder { diff --git a/lib/plugins/Label_81_MVA.ts b/lib/plugins/Label_81_MVA.ts new file mode 100644 index 0000000..68c18c1 --- /dev/null +++ b/lib/plugins/Label_81_MVA.ts @@ -0,0 +1,224 @@ +import { DecoderPlugin } from '../DecoderPlugin'; +import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; +import { ResultFormatter } from '../utils/result_formatter'; +import { DateTimeUtils } from '../DateTimeUtils'; + +/** + * Label 81 — MVA (Aircraft-Initiated Movement Message) + * + * An APAC / Jetstar-originated airline free-text ACARS label carrying an + * IATA AHM 780 **MVA** payload — a machine-generated arrival / departure / + * delay movement report transmitted automatically. + * + * Wire format (arrival variant observed): + * + * MVA + * JST0122/20.VHOYR.MEL + * AA0945/0950 + * SI FB 37 + * + * Lines: + * 1. Message type identifier (MVA / MVT) + * 2. /.. + * 3. Movement times: + * AA / (arrival) + * AD / (departure — analogous) + * 4. Optional supplementary information: + * SI e.g. `SI FB 37` = Fuel on Board 37 (units per op) + */ +export class Label_81_MVA extends DecoderPlugin { + name = 'label-81-mva'; + + qualifiers() { + return { + labels: ['81'], + preambles: ['MVA', 'MVT'], + }; + } + + decode(message: Message, options: Options = {}): DecodeResult { + const decodeResult = this.initResult( + message, + 'Aircraft-Initiated Movement Message (MVA)', + ); + + const text = (message.text || '').replace(/\r/g, '').trim(); + if (!text) { + this.setDecodeLevel(decodeResult, false); + return decodeResult; + } + + const lines = text.split(/\n+/).map((l) => l.trim()).filter((l) => l); + if (lines.length < 2) { + this.setDecodeLevel(decodeResult, false); + return decodeResult; + } + + // Line 1: message type — MVA or MVT + const msgType = lines[0]; + if (!/^(MVA|MVT)$/.test(msgType)) { + this.setDecodeLevel(decodeResult, false); + return decodeResult; + } + decodeResult.raw.mva_type = msgType; + + // Line 2: /.. + const idRe = + /^(?[A-Z]{2,3})(?\d{1,5}[A-Z]?)\/(?\d{1,2})\.(?[A-Z0-9-]{3,10})\.(?[A-Z]{3,4})$/; + const idm = lines[1].match(idRe); + if (!idm?.groups) { + this.setDecodeLevel(decodeResult, false); + return decodeResult; + } + const { carrier, flight, day, tail, airport } = idm.groups; + const fullFlight = `${carrier}${flight}`; + decodeResult.raw.carrier = carrier; + decodeResult.raw.flight_number = fullFlight; + decodeResult.raw.day = Number(day); + decodeResult.raw.tail = tail; + decodeResult.raw.airport = airport; + ResultFormatter.flightNumber(decodeResult, fullFlight); + ResultFormatter.tail(decodeResult, tail); + + // Line 3 (optional): AA / or AD / + let aaTouchdown = ''; + let aaOnBlocks = ''; + let adOffBlocks = ''; + let adTakeoff = ''; + if (lines[2]) { + const aa = lines[2].match(/^AA(?\d{4})(?:\/(?\d{4}))?$/); + const ad = !aa + ? lines[2].match(/^AD(?\d{4})(?:\/(?\d{4}))?$/) + : null; + if (aa?.groups) { + aaTouchdown = aa.groups.t1; + aaOnBlocks = aa.groups.t2 || ''; + decodeResult.raw.actual_touchdown = `${aaTouchdown.substring(0, 2)}:${aaTouchdown.substring(2, 4)}`; + if (aaOnBlocks) { + decodeResult.raw.actual_on_blocks = `${aaOnBlocks.substring(0, 2)}:${aaOnBlocks.substring(2, 4)}`; + } + ResultFormatter.arrivalAirport(decodeResult, airport); + ResultFormatter.timestamp( + decodeResult, + DateTimeUtils.convertHHMMSSToTod(aaTouchdown + '00'), + ); + } else if (ad?.groups) { + adOffBlocks = ad.groups.t1; + adTakeoff = ad.groups.t2 || ''; + decodeResult.raw.actual_off_blocks = `${adOffBlocks.substring(0, 2)}:${adOffBlocks.substring(2, 4)}`; + if (adTakeoff) { + decodeResult.raw.actual_takeoff = `${adTakeoff.substring(0, 2)}:${adTakeoff.substring(2, 4)}`; + } + ResultFormatter.departureAirport(decodeResult, airport); + ResultFormatter.timestamp( + decodeResult, + DateTimeUtils.convertHHMMSSToTod(adOffBlocks + '00'), + ); + } + } + + // Line 4 (optional): SI — e.g. "SI FB 37" + let siTag = ''; + let siValue = ''; + if (lines[3]) { + const si = lines[3].match(/^SI\s+([A-Z]{1,4})\s+([A-Z0-9.\-]+)\s*$/); + if (si) { + siTag = si[1]; + siValue = si[2]; + decodeResult.raw.supplementary_tag = siTag; + decodeResult.raw.supplementary_value = siValue; + } + } + + // ── formatted output ── + decodeResult.formatted.items.unshift( + { + type: 'message_type', + code: 'MSGTYP', + label: 'Message Type', + value: + msgType === 'MVA' + ? 'Aircraft-Initiated Movement Message (MVA)' + : 'Movement Message (MVT)', + }, + { + type: 'mva_type', + code: 'MVATYPE', + label: 'MVA/MVT Indicator', + value: msgType, + }, + ); + + decodeResult.formatted.items.push( + { + type: 'carrier', + code: 'CARRIER', + label: 'Carrier Code', + value: carrier, + }, + { + type: 'day', + code: 'DAY', + label: 'Day of Month', + value: day, + }, + { + type: 'airport', + code: 'APT', + label: 'Airport of Movement', + value: airport, + }, + ); + + if (aaTouchdown) { + decodeResult.formatted.items.push({ + type: 'actual_touchdown', + code: 'AAT', + label: 'Actual Touchdown (UTC)', + value: `${aaTouchdown.substring(0, 2)}:${aaTouchdown.substring(2, 4)}`, + }); + } + if (aaOnBlocks) { + decodeResult.formatted.items.push({ + type: 'actual_on_blocks', + code: 'AAB', + label: 'Actual On Blocks (UTC)', + value: `${aaOnBlocks.substring(0, 2)}:${aaOnBlocks.substring(2, 4)}`, + }); + } + if (adOffBlocks) { + decodeResult.formatted.items.push({ + type: 'actual_off_blocks', + code: 'ADB', + label: 'Actual Off Blocks (UTC)', + value: `${adOffBlocks.substring(0, 2)}:${adOffBlocks.substring(2, 4)}`, + }); + } + if (adTakeoff) { + decodeResult.formatted.items.push({ + type: 'actual_takeoff', + code: 'ADT', + label: 'Actual Takeoff (UTC)', + value: `${adTakeoff.substring(0, 2)}:${adTakeoff.substring(2, 4)}`, + }); + } + + if (siTag) { + const siLabelMap: Record = { + FB: 'Fuel on Board', + }; + const siLabel = siLabelMap[siTag] + ? `${siTag} (${siLabelMap[siTag]})` + : siTag; + decodeResult.formatted.items.push({ + type: 'supplementary', + code: 'SI', + label: 'Supplementary Info', + value: `${siLabel}: ${siValue}`, + }); + } + + this.setDecodeLevel(decodeResult, true, 'full'); + return decodeResult; + } +} diff --git a/lib/plugins/official.ts b/lib/plugins/official.ts index 0d3d6fa..ff83f11 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_81_MVA';