diff --git a/lib/plugins/Label_16_AUTPOS.test.ts b/lib/plugins/Label_16_AUTPOS.test.ts new file mode 100644 index 0000000..64f5af3 --- /dev/null +++ b/lib/plugins/Label_16_AUTPOS.test.ts @@ -0,0 +1,67 @@ +import { MessageDecoder } from '../MessageDecoder'; +import { Label_16_AUTPOS } from './Label_16_AUTPOS'; + +describe('Label 16 AUTPOS', () => { + let plugin: Label_16_AUTPOS; + const message = { label: '16', text: '' }; + + beforeEach(() => { + const decoder = new MessageDecoder(); + plugin = new Label_16_AUTPOS(decoder); + }); + + test('matches qualifiers', () => { + expect(plugin.decode).toBeDefined(); + expect(plugin.name).toBe('label-16-autpos'); + expect(plugin.qualifiers).toBeDefined(); + expect(plugin.qualifiers()).toEqual({ + labels: ['16'], + preambles: [''], + }); + }); + + test('matches redacted', () => { + message.text = + '283806/AUTPOS/LLD N400547 W0774954\r\n/ALT 12932/SAT ****\r\n/WND ******/TAT ****/TAS ****/CRZ ***\r\n/FOB 065120\r\n/DAT 260228/TIM 150742'; + const decodeResult = plugin.decode(message); + + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('full'); + expect(decodeResult.raw.day).toBe(28); + expect(decodeResult.raw.flight_number).toBe('3806'); + expect(decodeResult.raw.position.latitude).toBeCloseTo(40.096, 3); + expect(decodeResult.raw.position.longitude).toBeCloseTo(-77.832, 3); + expect(decodeResult.raw.altitude).toBe(12932); + expect(decodeResult.raw.fuel_on_board).toBe(65120); + expect(decodeResult.raw.message_timestamp).toBe(1772291262); + expect(decodeResult.formatted.description).toBe('Position Report'); + expect(decodeResult.formatted.items.length).toBe(6); + }); + + test('matches all values', () => { + message.text = + '289142/AUTPOS/LLD N395538 W0753341 \r\n/ALT 35000/SAT -057\r\n/WND 239065/TAT -027/TAS 476/CRZ 836\r\n/FOB 107600\r\n/DAT 260228/TIM 132714'; + const decodeResult = plugin.decode(message); + expect(decodeResult.decoded).toBe(true); + expect(decodeResult.decoder.decodeLevel).toBe('full'); + expect(decodeResult.raw.day).toBe(28); + expect(decodeResult.raw.flight_number).toBe('9142'); + expect(decodeResult.raw.position.latitude).toBeCloseTo(39.927, 3); + expect(decodeResult.raw.position.longitude).toBeCloseTo(-75.561, 3); + expect(decodeResult.raw.altitude).toBe(35000); + expect(decodeResult.raw.wind_data[0].windSpeed).toBe(65); + expect(decodeResult.raw.wind_data[0].windDirection).toBe(239); + expect(decodeResult.raw.total_air_temperature).toBe(-27); + expect(decodeResult.raw.airspeed).toBe(476); + expect(decodeResult.raw.mach).toBe(0.836); + expect(decodeResult.raw.fuel_on_board).toBe(107600); + expect(decodeResult.raw.message_timestamp).toBe(1772285234); + expect(decodeResult.formatted.items.length).toBe(11); + }); + + test('does not match if wrong format', () => { + message.text = 'invalid AUTPOS message'; + const decodeResult = plugin.decode(message); + expect(decodeResult.decoded).toBe(false); + }); +}); diff --git a/lib/plugins/Label_16_AUTPOS.ts b/lib/plugins/Label_16_AUTPOS.ts new file mode 100644 index 0000000..7b8c343 --- /dev/null +++ b/lib/plugins/Label_16_AUTPOS.ts @@ -0,0 +1,122 @@ +import { DateTimeUtils } from '../DateTimeUtils'; +import { DecoderPlugin } from '../DecoderPlugin'; +import { DecodeResult, Message } from '../DecoderPluginInterface'; +import { CoordinateUtils } from '../utils/coordinate_utils'; +import { ResultFormatter } from '../utils/result_formatter'; + +export class Label_16_AUTPOS extends DecoderPlugin { + name = 'label-16-autpos'; + qualifiers() { + return { + labels: ['16'], + preambles: [''], + }; + } + + decode(message: Message): DecodeResult { + let decodeResult = this.defaultResult(); + decodeResult.decoder.name = this.name; + decodeResult.message = message; + + // Regex to match the AUTPOS message + const regex = + /^(\d{6})\/AUTPOS\/LLD (N|S)(\d{2})(\d{2})(\d{2}) (E|W)(\d{3})(\d{2})(\d{2})\s*\r?\n\/ALT (\d+)\/SAT ([*\-\d]{4})\r?\n\/WND ([*\d]{3})([\*\d]{3})\/TAT ([*\-\d]{4})\/TAS ([*\d]{3,4})\/CRZ ([*\d]{3,4})\r?\n\/FOB (\d{6})\r?\n\/DAT (\d{6})\/TIM (\d{6})/; + const match = regex.exec(message.text); + if (!match) { + decodeResult.decoded = false; + return decodeResult; + } + + decodeResult.decoded = true; + decodeResult.decoder.decodeLevel = 'full'; + decodeResult.formatted.description = 'Position Report'; + + // Extract fields + const [ + , + flight, + latHem, + latDeg, + latMin, + latSec, + lonHem, + lonDeg, + lonMin, + lonSec, + altitude, + sat, + windDir, + windSpd, + tat, + tas, + crz, + fob, + dat, + tim, + ] = match; + + ResultFormatter.day(decodeResult, parseInt(flight.slice(0, 2), 10)); + ResultFormatter.flightNumber(decodeResult, flight.slice(2)); + let latitude = CoordinateUtils.dmsToDecimalDegrees( + parseInt(latDeg, 10), + parseInt(latMin, 10), + parseInt(latSec, 10), + ); + if (latHem === 'S') latitude = -latitude; + let longitude = CoordinateUtils.dmsToDecimalDegrees( + parseInt(lonDeg, 10), + parseInt(lonMin, 10), + parseInt(lonSec, 10), + ); + if (lonHem === 'W') longitude = -longitude; + ResultFormatter.position(decodeResult, { + latitude, + longitude, + }); + const alt = parseInt(altitude, 10); + ResultFormatter.altitude(decodeResult, alt); + + // Parse wind + if (!windSpd.includes('*') && !windDir.includes('*')) { + ResultFormatter.windData(decodeResult, [ + { + waypoint: { + name: 'POSITION', + }, + flightLevel: Math.round(alt / 100), + windDirection: parseInt(windDir, 10), + windSpeed: parseInt(windSpd, 10), + }, + ]); + } + + if (!tat.includes('*')) { + ResultFormatter.totalAirTemp(decodeResult, tat); + } + if (!tas.includes('*')) { + ResultFormatter.airspeed(decodeResult, parseInt(tas, 10)); + } + if (!sat.includes('*')) { + ResultFormatter.temperature(decodeResult, sat); + } + if (!crz.includes('*')) { + ResultFormatter.mach(decodeResult, parseInt(crz, 10) / 1000); + } + + if (!fob.includes('*')) { + ResultFormatter.currentFuel(decodeResult, parseInt(fob, 10)); + } + + if (!dat.includes('*') && !tim.includes('*')) { + const yy = dat.slice(0, 2); + const mm = dat.slice(2, 4); + const dd = dat.slice(4, 6); + const mmddyy = `${mm}${dd}${yy}`; + ResultFormatter.timestamp( + decodeResult, + DateTimeUtils.convertDateTimeToEpoch(tim, mmddyy), + ); + } + return decodeResult; + } +} diff --git a/lib/utils/result_formatter.ts b/lib/utils/result_formatter.ts index 33ecb23..bf208f2 100644 --- a/lib/utils/result_formatter.ts +++ b/lib/utils/result_formatter.ts @@ -317,6 +317,21 @@ export class ResultFormatter { }); } + static totalAirTemp(decodeResult: DecodeResult, value: string) { + if (value.length === 0) { + return; + } + decodeResult.raw.total_air_temperature = Number( + value.replace('M', '-').replace('P', '+'), + ); + decodeResult.formatted.items.push({ + type: 'temperature', + code: 'TATEMP', + label: 'Total Air Temperature (C)', + value: `${decodeResult.raw.total_air_temperature} degrees`, + }); + } + static heading(decodeResult: DecodeResult, value: number) { decodeResult.raw.heading = value; decodeResult.formatted.items.push({ @@ -613,6 +628,16 @@ export class ResultFormatter { }); } + static timestamp(decodeResult: DecodeResult, value: number) { + decodeResult.raw.message_timestamp = value; + decodeResult.formatted.items.push({ + type: 'epoch', + code: 'TIMESTAMP', + label: 'Message Timestamp', + value: DateTimeUtils.timestampToString(value, 'epoch'), + }); + } + static unknown(decodeResult: DecodeResult, value: string, sep: string = ',') { if (!decodeResult.remaining.text) decodeResult.remaining.text = value; else decodeResult.remaining.text += sep + value;