├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── publish.yml │ └── yarn-test.yml ├── .gitignore ├── .npmignore ├── .yarnrc.yml ├── README.md ├── SECURITY.md ├── babel.config.js ├── index.ts ├── jest.config.ts ├── lib ├── DateTimeUtils.ts ├── DecoderPlugin.ts ├── DecoderPluginInterface.ts ├── IcaoDecoder.ts ├── MessageDecoder.test.ts ├── MessageDecoder.ts ├── bin │ ├── acars-decoder-test.ts │ └── acars-decoder.ts ├── plugins │ ├── Label_10_LDR.test.ts │ ├── Label_10_LDR.ts │ ├── Label_10_POS.test.ts │ ├── Label_10_POS.ts │ ├── Label_10_Slash.test.ts │ ├── Label_10_Slash.ts │ ├── Label_12_N_Space.test.ts │ ├── Label_12_N_Space.ts │ ├── Label_12_POS.test.ts │ ├── Label_12_POS.ts │ ├── Label_13Through18_Slash.test.ts │ ├── Label_13Through18_Slash.ts │ ├── Label_15.test.ts │ ├── Label_15.ts │ ├── Label_15_FST.test.ts │ ├── Label_15_FST.ts │ ├── Label_16_N_Space.test.ts │ ├── Label_16_N_Space.ts │ ├── Label_16_POSA1.test.ts │ ├── Label_16_POSA1.ts │ ├── Label_16_TOD.test.ts │ ├── Label_16_TOD.ts │ ├── Label_1J_2J_FTX.test.ts │ ├── Label_1J_2J_FTX.ts │ ├── Label_1L_070.test.ts │ ├── Label_1L_070.ts │ ├── Label_1L_3-line.test.ts │ ├── Label_1L_3-line.ts │ ├── Label_1L_660.test.ts │ ├── Label_1L_660.ts │ ├── Label_1L_Slash.test.ts │ ├── Label_1L_Slash.ts │ ├── Label_1M_Slash.test.ts │ ├── Label_1M_Slash.ts │ ├── Label_20_CFB.01.ts │ ├── Label_20_POS.ts │ ├── Label_21_POS.test.ts │ ├── Label_21_POS.ts │ ├── Label_22_OFF.test.ts │ ├── Label_22_OFF.ts │ ├── Label_22_POS.test.ts │ ├── Label_22_POS.ts │ ├── Label_24_Slash.test.ts │ ├── Label_24_Slash.ts │ ├── Label_2P_FM3.test.ts │ ├── Label_2P_FM3.ts │ ├── Label_2P_FM4.test.ts │ ├── Label_2P_FM4.ts │ ├── Label_2P_FM5.test.ts │ ├── Label_2P_FM5.ts │ ├── Label_2P_POS.test.ts │ ├── Label_2P_POS.ts │ ├── Label_30_Slash_EA.test.ts │ ├── Label_30_Slash_EA.ts │ ├── Label_44_ETA.test.ts │ ├── Label_44_ETA.ts │ ├── Label_44_IN.test.ts │ ├── Label_44_IN.ts │ ├── Label_44_OFF.test.ts │ ├── Label_44_OFF.ts │ ├── Label_44_ON.test.ts │ ├── Label_44_ON.ts │ ├── Label_44_POS.test.ts │ ├── Label_44_POS.ts │ ├── Label_4A.test.ts │ ├── Label_4A.ts │ ├── Label_4A_01.test.ts │ ├── Label_4A_01.ts │ ├── Label_4A_DIS.test.ts │ ├── Label_4A_DIS.ts │ ├── Label_4A_DOOR.test.ts │ ├── Label_4A_DOOR.ts │ ├── Label_4A_Slash_01.test.ts │ ├── Label_4A_Slash_01.ts │ ├── Label_4J_POS.test.ts │ ├── Label_4J_POS.ts │ ├── Label_4N.test.ts │ ├── Label_4N.ts │ ├── Label_4T_AGFSR.test.ts │ ├── Label_4T_AGFSR.ts │ ├── Label_4T_ETA.test.ts │ ├── Label_4T_ETA.ts │ ├── Label_58.test.ts │ ├── Label_58.ts │ ├── Label_5Z_Slash.test.ts │ ├── Label_5Z_Slash.ts │ ├── Label_80.test.ts │ ├── Label_80.ts │ ├── Label_83.test.ts │ ├── Label_83.ts │ ├── Label_8E.test.ts │ ├── Label_8E.ts │ ├── Label_B6.ts │ ├── Label_ColonComma.ts │ ├── Label_H1.test.ts │ ├── Label_H1.ts │ ├── Label_H1_FLR.test.ts │ ├── Label_H1_FLR.ts │ ├── Label_H1_FPN.test.ts │ ├── Label_H1_FTX.test.ts │ ├── Label_H1_INI.test.ts │ ├── Label_H1_OHMA.test.ts │ ├── Label_H1_OHMA.ts │ ├── Label_H1_POS.test.ts │ ├── Label_H1_PRG.test.ts │ ├── Label_H1_PWI.test.ts │ ├── Label_H1_Slash.test.ts │ ├── Label_H1_Slash.ts │ ├── Label_H1_StarPOS.test.ts │ ├── Label_H1_StarPOS.ts │ ├── Label_H1_WRN.test.ts │ ├── Label_H1_WRN.ts │ ├── Label_HX.test.ts │ ├── Label_HX.ts │ ├── Label_QP.ts │ ├── Label_QQ.test.ts │ ├── Label_QQ.ts │ ├── Label_QR.ts │ ├── Label_QS.ts │ ├── Label_SQ.ts │ └── official.ts ├── types │ ├── route.ts │ └── waypoint.ts └── utils │ ├── coordinate_utils.ts │ ├── flight_plan_utils.ts │ ├── h1_helper.ts │ ├── miam.test.ts │ ├── miam.ts │ ├── result_formatter.ts │ └── route_utils.ts ├── package.json ├── tsconfig.json ├── tsup.config.ts └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kevinelliott] 4 | patreon: Airframes 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: airframesio/data 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Grype CI 2 | on: 3 | workflow_dispatch: 4 | push: 5 | 6 | permissions: 7 | # required for all workflows 8 | security-events: write 9 | # only required for workflows in private repositories 10 | actions: read 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 18 | - name: Scan current project 19 | id: scan 20 | uses: anchore/scan-action@v6 21 | with: 22 | path: "." 23 | - name: upload Anchore scan SARIF report 24 | uses: github/codeql-action/upload-sarif@v3 25 | with: 26 | sarif_file: ${{ steps.scan.outputs.sarif }} 27 | - name: Inspect action SARIF report 28 | run: cat ${{ steps.scan.outputs.sarif }} 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - run: corepack enable 15 | - uses: actions/setup-node@v4 16 | with: 17 | cache: yarn 18 | node-version: '20.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | scope: '@airframes' 21 | - run: yarn install --immutable 22 | - run: yarn build 23 | - run: yarn npm publish 24 | env: 25 | YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/yarn-test.yml: -------------------------------------------------------------------------------- 1 | name: Yarn Test CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | # build against all active LTS and latest node 21 | node-version: [18.x, 20.x, latest] 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - run: corepack enable 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: yarn 31 | - run: yarn install --immutable 32 | - run: yarn test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/ 2 | node_modules/ 3 | coverage/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # acars-decoder-typescript 2 | 3 | [![NPM Version](https://badge.fury.io/js/@airframes%2Facars-decoder.svg)](https://badge.fury.io/js/@airframes%2Facars-decoder) 4 | [![GitHub Actions Workflow Status](https://github.com/airframesio/acars-decoder-typescript/actions/workflows/yarn-test.yml/badge.svg) 5 | ](https://github.com/airframesio/acars-decoder-typescript/actions/workflows/yarn-test.yml) 6 | ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/airframesio/acars-decoder-typescript?utm_source=oss&utm_medium=github&utm_campaign=airframesio%2Facars-decoder-typescript&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) 7 | [![Contributors](https://img.shields.io/github/contributors/airframesio/acars-decoder-typescript)](https://github.com/airframesio/acars-decoder-typescript/graphs/contributors) 8 | [![Activity](https://img.shields.io/github/commit-activity/m/airframesio/acars-decoder-typescript)](https://github.com/airframesio/acars-decoder-typescript/pulse) 9 | [![Discord](https://img.shields.io/discord/1067697487927853077?logo=discord)](https://discord.gg/airframes) 10 | 11 | ACARS is an aircraft communications messaging protocol that has been in use worldwide for a few decades. This library exists to specifically decode the text portion of the ACARS message payload. 12 | 13 | The library is built around research and discoveries from the [ACARS Message Documentation](https://github.com/airframesio/acars-message-documentation), a community effort to document the details of the ACARS message label/text payload. 14 | 15 | It has been written in TypeScript (which compiles to Javascript) and is published as an NPM package. 16 | 17 | You are welcome to contribute (please see https://github.com/airframesio/acars-message-documentation where we collaborate to research and document the various types of messages), and while it was primarily developed to power [Airframes](https://app.airframes.io) and [AcarsHub](https://sdr-e.com/docker-acarshub), you may use this library in your own applications freely. 18 | 19 | # Installation 20 | 21 | Add the `@airframes/acars-decoder` library to your JavaScript or TypeScript project. 22 | 23 | With `yarn`: 24 | ``` 25 | yarn add @airframes/acars-decoder 26 | ``` 27 | 28 | With `npm`: 29 | ``` 30 | npm install @airframes/acars-decoder 31 | ``` 32 | 33 | # Usage 34 | 35 | Documentation coming soon. 36 | 37 | # Contributions 38 | 39 | Contributions are welcome! Please follow the [ACARS Message Documentation](https://github.com/airframesio/acars-message-documentation) when implementing. Most find that this makes things a lot easier. Submit a Pull Request and we will gratefully review and merge. 40 | 41 | # Contributors 42 | 43 | | Contributor | Description | 44 | | ----------- | ----------- | 45 | | [Kevin Elliott](https://github.com/kevinelliott) | Primary Airframes contributor | 46 | | [Michael Johnson](https://github.com/johnsom) | Decoder plugins, testing framework | 47 | | [Mark Bumiller](https://github.com/makrsmark) | Decoder plugins, tests, utilities | 48 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 1.0.x | :white_check_mark: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Use this section to tell people how to report a vulnerability. 15 | 16 | Tell them where to go, how often they can expect to get an update on a 17 | reported vulnerability, what to expect if the vulnerability is accepted or 18 | declined, etc. 19 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/IcaoDecoder'; 2 | export * from './lib/MessageDecoder'; 3 | -------------------------------------------------------------------------------- /lib/DateTimeUtils.ts: -------------------------------------------------------------------------------- 1 | export class DateTimeUtils { 2 | 3 | // Expects a four digit UTC time string (HHMM) 4 | public static UTCToString(UTCString: string) { 5 | let utcDate = new Date(); 6 | utcDate.setUTCHours(+UTCString.substr(0, 2), +UTCString.substr(2, 2), 0); 7 | return utcDate.toTimeString(); 8 | } 9 | 10 | // Expects a six digit date string and a four digit UTC time string 11 | // (DDMMYY) (HHMM) 12 | public static UTCDateTimeToString(dateString: string, timeString: string) { 13 | let utcDate = new Date(); 14 | utcDate.setUTCDate(+dateString.substr(0, 2)); 15 | utcDate.setUTCMonth(+dateString.substr(2, 2)); 16 | if (dateString.length === 6) { 17 | utcDate.setUTCFullYear(2000 + +dateString.substr(4, 2)); 18 | } 19 | if (timeString.length === 6) { 20 | utcDate.setUTCHours(+timeString.substr(0, 2), +timeString.substr(2, 2), +timeString.substr(4, 2)); 21 | } else { 22 | utcDate.setUTCHours(+timeString.substr(0, 2), +timeString.substr(2, 2), 0); 23 | } 24 | return utcDate.toUTCString(); 25 | } 26 | 27 | /** 28 | * 29 | * @param time HHMMSS or HHMM 30 | * @returns seconds since midnight 31 | */ 32 | public static convertHHMMSSToTod(time: string): number { 33 | if(time.length === 4) { 34 | time += '00'; 35 | } 36 | const h = Number(time.substring(0, 2)); 37 | const m = Number(time.substring(2, 4)); 38 | const s = Number(time.substring(4, 6)); 39 | const tod = (h * 3600) + (m * 60) + s; 40 | return tod; 41 | } 42 | 43 | /** 44 | * 45 | * @param time HHMMSS 46 | * @param date MMDDYY or MMDDYYYY 47 | * @returns seconds since epoch 48 | */ 49 | public static convertDateTimeToEpoch(time: string, date: string): number { 50 | //YYYY-MM-DDTHH:mm:ss.sssZ 51 | if (date.length === 6) { 52 | date = date.substring(0, 4) + `20${date.substring(4, 6)}`; 53 | } 54 | const timestamp = `${date.substring(4, 8)}-${date.substring(0, 2)}-${date.substring(2, 4)}T${time.substring(0, 2)}:${time.substring(2, 4)}:${time.substring(4, 6)}.000Z` 55 | const millis = Date.parse(timestamp); 56 | return millis / 1000; 57 | } 58 | 59 | /** 60 | * Converts a timestamp to a string 61 | * 62 | * ISO-8601 format for 'epoch' 63 | * HH:MM:SS for 'tod' 64 | * @param time 65 | * @param format 66 | * @returns 67 | */ 68 | public static timestampToString(time: number, format: 'tod' | 'epoch'): string { 69 | const date = new Date(time * 1000); if (format == 'tod') { 70 | return date.toISOString().slice(11, 19); 71 | } 72 | //strip off millis 73 | return date.toISOString().slice(0, -5) + "Z"; 74 | } 75 | } -------------------------------------------------------------------------------- /lib/DecoderPlugin.ts: -------------------------------------------------------------------------------- 1 | import { DecodeResult, DecoderPluginInterface, Message, Options } from './DecoderPluginInterface'; 2 | 3 | export abstract class DecoderPlugin implements DecoderPluginInterface { 4 | decoder!: any; 5 | 6 | name: string = 'unknown'; 7 | 8 | defaultResult(): DecodeResult { 9 | return { 10 | decoded: false, 11 | decoder: { 12 | name: 'unknown', 13 | type: 'pattern-match', 14 | decodeLevel: 'none', 15 | }, 16 | formatted: { 17 | description: 'Unknown', 18 | items: [], 19 | }, 20 | raw: {}, 21 | remaining: {}, 22 | }; 23 | }; 24 | 25 | options: Object; 26 | 27 | constructor(decoder : any, options : Options = {}) { 28 | this.decoder = decoder; 29 | this.options = options; 30 | } 31 | 32 | id() : string { // eslint-disable-line class-methods-use-this 33 | console.log('DecoderPlugin subclass has not overriden id() to provide a unique ID for this plugin!'); 34 | return 'abstract_decoder_plugin'; 35 | } 36 | 37 | meetsStateRequirements() : boolean { // eslint-disable-line class-methods-use-this 38 | return true; 39 | } 40 | 41 | // onRegister(store: Store) { 42 | // this.store = store; 43 | // } 44 | 45 | qualifiers() : any { // eslint-disable-line class-methods-use-this 46 | const labels : Array = []; 47 | 48 | return { 49 | labels, 50 | }; 51 | } 52 | 53 | decode(message: Message) : DecodeResult { // eslint-disable-line class-methods-use-this 54 | const decodeResult = this.defaultResult(); 55 | decodeResult.remaining.text = message.text; 56 | return decodeResult; 57 | } 58 | } 59 | 60 | export default {}; 61 | -------------------------------------------------------------------------------- /lib/DecoderPluginInterface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Representation of a Message 3 | */ 4 | export interface Message { 5 | label?: string, 6 | sublabel?: string, 7 | text: string, 8 | } 9 | 10 | /** 11 | * Decoder Options 12 | */ 13 | export interface Options { 14 | debug?:boolean, 15 | } 16 | 17 | /** 18 | * Results from decoding a message 19 | */ 20 | export interface DecodeResult { 21 | decoded: boolean; 22 | decoder: { 23 | name: string, 24 | type: 'pattern-match' | 'none', 25 | decodeLevel: 'none' | 'partial' | 'full', 26 | }, 27 | error?: string, 28 | formatted: { 29 | description: string, 30 | items: { 31 | type: string, 32 | code: string, 33 | label: string, 34 | value: string, 35 | }[] 36 | }, 37 | message?: any, 38 | raw: any, 39 | remaining: { 40 | text?: string 41 | } 42 | } 43 | 44 | export interface DecoderPluginInterface { 45 | decode(message: Message) : DecodeResult; 46 | meetsStateRequirements() : boolean; 47 | // onRegister(store: Store) : void; 48 | qualifiers() : any; 49 | } 50 | 51 | export default { 52 | } 53 | -------------------------------------------------------------------------------- /lib/MessageDecoder.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from "./MessageDecoder" 2 | 3 | test('MIAM core seamless decode', () => { 4 | const message = { 5 | label: 'MA', 6 | text: 'T02!<<,:/k.E`;FOV@!\'s.16q6R+p(RK,|D2ujNJhRah?_qrNftWiI-V,@*RQs,tn,FYN$/V1!gNIc6CO;$D,1:.4?dF952;>XP$"B"Ok-Fr\'0^k?rP]3&UGoPX;\\ { 18 | const message = { 19 | label: 'H1', 20 | text: 'POSN43312W123174,EASON,215754,370,EBINY,220601,ELENN,M48,02216,185/TS215754,0921227A40' 21 | }; 22 | 23 | const decoder = new MessageDecoder(); 24 | decoder.decode(message); 25 | const decodeResult = decoder.decode(message); 26 | 27 | expect(decodeResult.message.label).toBe('H1'); 28 | expect(decodeResult.formatted.items.length).toBe(5); 29 | }) -------------------------------------------------------------------------------- /lib/bin/acars-decoder.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { MessageDecoder } from '../MessageDecoder'; 4 | 5 | const decoder = new MessageDecoder(); 6 | const message = { 7 | label: process.argv[2], 8 | text: process.argv[3] 9 | }; 10 | 11 | console.log("Original Message:"); 12 | console.log(message.text); 13 | console.log(); 14 | 15 | const result = decoder.decode(message, { debug: true }); 16 | 17 | console.log("Decoded Message:"); 18 | console.log(result.formatted.description); 19 | if (result.formatted.items && result.formatted.items.length > 0) { 20 | result.formatted.items.forEach((item: any) => { 21 | console.log(`${item.label} - ${item.value}`); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /lib/plugins/Label_10_LDR.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { ResultFormatter } from '../utils/result_formatter'; 4 | 5 | export class Label_10_LDR extends DecoderPlugin { // eslint-disable-line camelcase 6 | name = 'label-10-ldr'; 7 | 8 | qualifiers() { // eslint-disable-line class-methods-use-this 9 | return { 10 | labels: ['10'], 11 | preambles: ['LDR'], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}): DecodeResult { 16 | const decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.formatted.description = 'Position Report'; 19 | decodeResult.message = message; 20 | 21 | const parts = message.text.split(','); 22 | if (parts.length < 17) { 23 | if (options.debug) { 24 | console.log(`Decoder: Unknown 10 message: ${message.text}`); 25 | } 26 | ResultFormatter.unknown(decodeResult, message.text); 27 | decodeResult.decoded = false; 28 | decodeResult.decoder.decodeLevel = 'none'; 29 | return decodeResult; 30 | } 31 | 32 | const lat = parts[5]; 33 | const lon = parts[6]; 34 | const position = { 35 | latitude: (lat[0] === 'N' ? 1 : -1) * Number(lat.substring(1).trim()), 36 | longitude: (lon[0] === 'E' ? 1 : -1) * Number(lon.substring(1).trim()), 37 | } 38 | ResultFormatter.position(decodeResult, position); 39 | ResultFormatter.altitude(decodeResult, Number(parts[7])); 40 | ResultFormatter.departureAirport(decodeResult, parts[9]); 41 | ResultFormatter.arrivalAirport(decodeResult, parts[10]); 42 | ResultFormatter.alternateAirport(decodeResult, parts[11]); 43 | ResultFormatter.arrivalRunway(decodeResult, parts[12].split('/')[0]); // TODO: find out if anything comes after `/` sometimes 44 | const altRwy = [parts[13].split('/')[0], parts[14].split('/')[0]].filter((r) => r != "").join(","); 45 | if (altRwy != "") { 46 | ResultFormatter.alternateRunway(decodeResult, altRwy); // TODO: find out if anything comes after `/` sometimes 47 | } 48 | ResultFormatter.unknownArr(decodeResult, [...parts.slice(0,5), ...parts.slice(15)]); 49 | 50 | decodeResult.decoded = true; 51 | decodeResult.decoder.decodeLevel = 'partial'; 52 | return decodeResult; 53 | } 54 | } 55 | 56 | export default {}; 57 | -------------------------------------------------------------------------------- /lib/plugins/Label_10_POS.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_10_POS } from './Label_10_POS'; 3 | 4 | describe('Label_10_POS', () => { 5 | let plugin: Label_10_POS; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_10_POS(decoder); 10 | }); 11 | 12 | test('matches Label 10 Preamble pos qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-10-pos'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['10'], 18 | preambles: ['POS'], 19 | }); 20 | }); 21 | 22 | test('decodes Label 10 Preamble POS variant 1', () => { 23 | const text = 'POS082150, N 3885,W 7841,---,308,26922, 51,22290, 529, 19,-225,6' 24 | const decodeResult = plugin.decode({ text: text }); 25 | 26 | expect(decodeResult.decoded).toBe(true); 27 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 28 | expect(decodeResult.formatted.items.length).toBe(2); 29 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 30 | expect(decodeResult.formatted.items[0].value).toBe('38.850 N, 78.410 W'); 31 | expect(decodeResult.formatted.items[1].label).toBe('Altitude'); 32 | expect(decodeResult.formatted.items[1].value).toBe('22290 feet'); 33 | expect(decodeResult.remaining.text).toBe('POS082150,---,308,26922, 51, 529, 19,-225,6'); 34 | }); 35 | 36 | test('decodes Label 10 Preamble POS ', () => { 37 | 38 | const text = 'POS Bogus Message'; 39 | const decodeResult = plugin.decode({ text: text }); 40 | 41 | expect(decodeResult.decoded).toBe(false); 42 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 43 | expect(decodeResult.decoder.name).toBe('label-10-pos'); 44 | expect(decodeResult.formatted.description).toBe('Position Report'); 45 | expect(decodeResult.message.text).toBe(text); 46 | }); 47 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_10_POS.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { ResultFormatter } from '../utils/result_formatter'; 4 | 5 | export class Label_10_POS extends DecoderPlugin { // eslint-disable-line camelcase 6 | name = 'label-10-pos'; 7 | 8 | qualifiers() { // eslint-disable-line class-methods-use-this 9 | return { 10 | labels: ['10'], 11 | preambles: ['POS'], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}): DecodeResult { 16 | const decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.formatted.description = 'Position Report'; 19 | decodeResult.message = message; 20 | 21 | const parts = message.text.split(','); 22 | if (parts.length !== 12) { 23 | if (options.debug) { 24 | console.log(`Decoder: Unknown 10 message: ${message.text}`); 25 | } 26 | ResultFormatter.unknown(decodeResult, message.text); 27 | decodeResult.decoded = false; 28 | decodeResult.decoder.decodeLevel = 'none'; 29 | return decodeResult; 30 | } 31 | 32 | //const time = parts[0].substring(3); //DDHHMM 33 | const lat = parts[1].trim(); 34 | const lon = parts[2].trim(); 35 | const position = { 36 | latitude: (lat[0] === 'N' ? 1 : -1) * Number(lat.substring(1).trim())/100, 37 | longitude: (lon[0] === 'E' ? 1 : -1) * Number(lon.substring(1).trim())/100, 38 | } 39 | ResultFormatter.position(decodeResult, position); 40 | ResultFormatter.altitude(decodeResult, Number(parts[7])); 41 | ResultFormatter.unknownArr(decodeResult, [parts[0], ...parts.slice(3,7), ...parts.slice(8)]); 42 | 43 | decodeResult.decoded = true; 44 | decodeResult.decoder.decodeLevel = 'partial'; 45 | return decodeResult; 46 | } 47 | } 48 | 49 | export default {}; 50 | -------------------------------------------------------------------------------- /lib/plugins/Label_10_Slash.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { Waypoint } from '../types/waypoint'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | import { RouteUtils } from '../utils/route_utils'; 7 | 8 | export class Label_10_Slash extends DecoderPlugin { // eslint-disable-line camelcase 9 | name = 'label-10-slash'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['10'], 14 | preambles: ['/'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}): DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'Position Report'; 22 | decodeResult.message = message; 23 | 24 | const parts = message.text.split('/'); 25 | if (parts.length < 17) { 26 | if (options.debug) { 27 | console.log(`Decoder: Unknown 10 message: ${message.text}`); 28 | } 29 | ResultFormatter.unknown(decodeResult, message.text); 30 | decodeResult.decoded = false; 31 | decodeResult.decoder.decodeLevel = 'none'; 32 | return decodeResult; 33 | } 34 | 35 | const lat = parts[1]; 36 | const lon = parts[2]; 37 | const position = { 38 | latitude: (lat[0] === 'N' ? 1 : -1) * Number(lat.substring(1)), 39 | longitude: (lon[0] === 'E' ? 1 : -1) * Number(lon.substring(1)), 40 | } 41 | ResultFormatter.position(decodeResult, position); 42 | ResultFormatter.heading(decodeResult, Number(parts[5])); 43 | ResultFormatter.altitude(decodeResult, 100*Number(parts[6])); 44 | ResultFormatter.arrivalAirport(decodeResult, parts[7]); 45 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[8])); 46 | const waypoints: Waypoint[] = [{ 47 | name: parts[11], 48 | },{ 49 | name: parts[12], 50 | time: DateTimeUtils.convertHHMMSSToTod(parts[13]), 51 | timeFormat: 'tod', 52 | },{ 53 | name: parts[14], 54 | time: DateTimeUtils.convertHHMMSSToTod(parts[15]), 55 | timeFormat: 'tod', 56 | },]; 57 | ResultFormatter.route(decodeResult, { waypoints: waypoints }); 58 | 59 | if(parts[16]) { 60 | ResultFormatter.departureAirport(decodeResult, parts[16]); 61 | } 62 | 63 | ResultFormatter.unknownArr(decodeResult, [parts[3], parts[4], ...parts.slice(9,11), ...parts.slice(17)], '/'); 64 | 65 | decodeResult.decoded = true; 66 | decodeResult.decoder.decodeLevel = 'partial'; 67 | return decodeResult; 68 | } 69 | } 70 | 71 | export default {}; 72 | -------------------------------------------------------------------------------- /lib/plugins/Label_12_N_Space.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_12_N_Space extends DecoderPlugin { 7 | name = 'label-12-n-space'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["12"], 12 | preambles: ['N ', 'S '], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}): DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'Position Report'; 20 | decodeResult.message = message; 21 | 22 | const variant1Regex = /^(?[NS])\s(?.*),(?[EW])\s*(?.*),(?.*),(?.*),\s*(?.*),.(?.*),(?.*)$/; 23 | 24 | let results = message.text.match(variant1Regex) 25 | if (results?.groups) { 26 | if (options.debug) { 27 | console.log(`Label 12 N : results`); 28 | console.log(results); 29 | } 30 | 31 | ResultFormatter.position(decodeResult, { 32 | latitude: Number(results.groups.lat_coord) * (results.groups.lat == 'N' ? 1 : -1), 33 | longitude: Number(results.groups.long_coord) * (results.groups.long == 'E' ? 1 : -1) 34 | }); 35 | 36 | const altitude = results.groups.alt == 'GRD' || results.groups.alt == '***' ? 0 : Number(results.groups.alt); 37 | ResultFormatter.altitude(decodeResult, altitude); 38 | 39 | ResultFormatter.unknownArr(decodeResult, [results.groups.unkwn1, results.groups.unkwn2, results.groups.unkwn3]); 40 | decodeResult.decoded = true; 41 | decodeResult.decoder.decodeLevel = 'partial'; 42 | return decodeResult; 43 | } 44 | 45 | // Unknown 46 | if (options.debug) { 47 | console.log(`Decoder: Unknown 12 message: ${message.text}`); 48 | } 49 | ResultFormatter.unknown(decodeResult, message.text); 50 | decodeResult.decoded = false; 51 | decodeResult.decoder.decodeLevel = 'none'; 52 | 53 | return decodeResult; 54 | } 55 | } 56 | 57 | export default {}; 58 | -------------------------------------------------------------------------------- /lib/plugins/Label_12_POS.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_12_POS } from './Label_12_POS'; 3 | 4 | describe('Label 12 POS', () => { 5 | 6 | let plugin: Label_12_POS; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_12_POS(decoder); 11 | }); 12 | 13 | 14 | test('matches qualifiers', () => { 15 | expect(plugin.decode).toBeDefined(); 16 | expect(plugin.name).toBe('label-12-pos'); 17 | expect(plugin.qualifiers).toBeDefined(); 18 | expect(plugin.qualifiers()).toEqual({ 19 | labels: ['12'], 20 | preambles: ['POS'], 21 | }); 22 | }); 23 | 24 | 25 | test('decodes msg 1', () => { 26 | const text = 'POSN 390104W 754601,-------,1244,1446,,- 4,23249 12,FOB 73,ETA 1303,KATL,KPHL,'; 27 | 28 | const decodeResult = plugin.decode({ text: text }); 29 | 30 | expect(decodeResult.decoded).toBe(true); 31 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 32 | expect(decodeResult.formatted.description).toBe('Position Report'); 33 | expect(decodeResult.formatted.items.length).toBe(7); 34 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 35 | expect(decodeResult.formatted.items[0].value).toBe('39.018 N, 75.767 W'); 36 | expect(decodeResult.formatted.items[1].label).toBe('Message Timestamp'); 37 | expect(decodeResult.formatted.items[1].value).toBe('12:44:00'); 38 | expect(decodeResult.formatted.items[2].label).toBe('Altitude'); 39 | expect(decodeResult.formatted.items[2].value).toBe('14460 feet'); 40 | expect(decodeResult.formatted.items[3].label).toBe('Fuel On Board'); 41 | expect(decodeResult.formatted.items[3].value).toBe('73'); 42 | expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival'); 43 | expect(decodeResult.formatted.items[4].value).toBe('13:03:00'); 44 | expect(decodeResult.formatted.items[5].label).toBe('Origin'); 45 | expect(decodeResult.formatted.items[5].value).toBe('KATL'); 46 | expect(decodeResult.formatted.items[6].label).toBe('Destination'); 47 | expect(decodeResult.formatted.items[6].value).toBe('KPHL'); 48 | expect(decodeResult.remaining.text).toBe('-------,,- 4,23249 12,'); 49 | }); 50 | 51 | test('decodes ', () => { 52 | 53 | const text = 'POS Bogus message'; 54 | const decodeResult = plugin.decode({ text: text }); 55 | 56 | expect(decodeResult.decoded).toBe(false); 57 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 58 | expect(decodeResult.formatted.description).toBe('Position Report'); 59 | expect(decodeResult.formatted.items.length).toBe(0); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/plugins/Label_12_POS.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // General Aviation Position Report 8 | export class Label_12_POS extends DecoderPlugin { 9 | name = 'label-12-pos'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['12'], 14 | preambles: ['POS'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}): DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'Position Report'; 22 | decodeResult.message = message; 23 | 24 | const data = message.text.substring(3).split(','); 25 | if (!message.text.startsWith('POS') || data.length !== 12) { 26 | if (options.debug) { 27 | console.log(`Decoder: Unknown 12 message: ${message.text}`); 28 | } 29 | ResultFormatter.unknown(decodeResult, message.text); 30 | decodeResult.decoded = false; 31 | decodeResult.decoder.decodeLevel = 'none'; 32 | return decodeResult; 33 | } 34 | 35 | const lat = data[0].substring(0, 8); 36 | const lon = data[0].substring(8); 37 | ResultFormatter.position(decodeResult, { 38 | latitude: CoordinateUtils.getDirection(lat[0]) * CoordinateUtils.dmsToDecimalDegrees(Number(lat.substring(1, 4)), Number(lat.substring(4, 6)), Number(lat.substring(6, 8))), 39 | longitude: CoordinateUtils.getDirection(lon[0]) * CoordinateUtils.dmsToDecimalDegrees(Number(lon.substring(1, 4)), Number(lon.substring(4, 6)), Number(lon.substring(6, 8))), 40 | }); 41 | ResultFormatter.unknown(decodeResult, data[1]); 42 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[2])); 43 | ResultFormatter.altitude(decodeResult, 10 * Number(data[3])); 44 | ResultFormatter.unknownArr(decodeResult, data.slice(4, 7)); 45 | ResultFormatter.currentFuel(decodeResult, Number(data[7].substring(3).trim())); // strip FOB 46 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[8].substring(3).trim())); // strip ETA 47 | ResultFormatter.departureAirport(decodeResult, data[9]); 48 | ResultFormatter.arrivalAirport(decodeResult, data[10]); 49 | ResultFormatter.unknown(decodeResult, data[11]); 50 | 51 | decodeResult.decoded = true; 52 | decodeResult.decoder.decodeLevel = 'partial'; 53 | 54 | return decodeResult; 55 | } 56 | } 57 | 58 | export default {}; 59 | -------------------------------------------------------------------------------- /lib/plugins/Label_15.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // General Aviation Position Report 8 | export class Label_15 extends DecoderPlugin { 9 | name = 'label-15'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['15'], 14 | preambles: ['(2'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}): DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'Position Report'; 22 | decodeResult.message = message; 23 | 24 | if (message.text.startsWith('(2') && message.text.endsWith('(Z')) { 25 | const between = message.text.substring(2, message.text.length - 2); 26 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinatesDecimalMinutes(between.substring(0, 13))); 27 | if (between.length === 25) { // short variant 28 | ResultFormatter.unknown(decodeResult, between.substring(13, 19)); 29 | const alt = between.substring(19, 22); 30 | if (alt != '---') { 31 | ResultFormatter.altitude(decodeResult, 100 * Number(alt)); 32 | } 33 | ResultFormatter.temperature(decodeResult, between.substring(22).replaceAll(" ", "0")); 34 | } else if(between.substring(13,16) === 'OFF') { // off variant 35 | const ddmmyy = between.substring(16, 22); 36 | const hhmm = between.substring(22, 26); 37 | if(ddmmyy != '------') { 38 | const mmddyy = ddmmyy.substring(2, 4) + ddmmyy.substring(0, 2) + ddmmyy.substring(4); 39 | ResultFormatter.off(decodeResult, DateTimeUtils.convertDateTimeToEpoch(hhmm+'00', mmddyy), 'epoch'); 40 | } else { 41 | ResultFormatter.off(decodeResult, DateTimeUtils.convertHHMMSSToTod(hhmm), 'tod'); 42 | } 43 | ResultFormatter.unknown(decodeResult, between.substring(26)); 44 | } else { 45 | ResultFormatter.unknown(decodeResult, between.substring(26)); 46 | } 47 | } else { 48 | if (options.debug) { 49 | console.log(`Decoder: Unknown 15 message: ${message.text}`); 50 | } 51 | ResultFormatter.unknown(decodeResult, message.text); 52 | decodeResult.decoded = false; 53 | decodeResult.decoder.decodeLevel = 'none'; 54 | return decodeResult; 55 | } 56 | 57 | decodeResult.decoded = true; 58 | decodeResult.decoder.decodeLevel = 'partial'; 59 | return decodeResult; 60 | } 61 | } 62 | 63 | export default {}; 64 | -------------------------------------------------------------------------------- /lib/plugins/Label_15_FST.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_15_FST } from './Label_15_FST'; 3 | 4 | describe('Label_15_FST', () => { 5 | let plugin: Label_15_FST; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_15_FST(decoder); 10 | }); 11 | 12 | test('matches Label 15 Preamble FST qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-15-fst'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['15'], 18 | preambles: ['FST01'], 19 | }); 20 | }); 21 | 22 | 23 | test('decodes Label 15 Preamble FST variant 1', () => { 24 | const text = 'FST01EGKKKMCON373488W0756927380 156 495 M53C 4427422721045313002518521710' 25 | const decodeResult = plugin.decode({ text: text }); 26 | 27 | expect(decodeResult.decoded).toBe(true); 28 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 29 | expect(decodeResult.formatted.items.length).toBe(4); 30 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 31 | expect(decodeResult.formatted.items[0].value).toBe('37.349 N, 75.693 W'); 32 | expect(decodeResult.formatted.items[1].label).toBe('Altitude'); 33 | expect(decodeResult.formatted.items[1].value).toBe('38000 feet'); 34 | expect(decodeResult.formatted.items[2].label).toBe('Origin'); 35 | expect(decodeResult.formatted.items[2].value).toBe('EGKK'); 36 | expect(decodeResult.formatted.items[3].label).toBe('Destination'); 37 | expect(decodeResult.formatted.items[3].value).toBe('KMCO'); 38 | expect(decodeResult.remaining.text).toBe('156 495 M53C 4427422721045313002518521710'); 39 | }); 40 | 41 | test('decodes Label 15 Preamble FST ', () => { 42 | 43 | const text = 'INI Bogus message'; 44 | const decodeResult = plugin.decode({ text: text }); 45 | 46 | expect(decodeResult.decoded).toBe(false); 47 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 48 | expect(decodeResult.decoder.name).toBe('label-15-fst'); 49 | expect(decodeResult.formatted.description).toBe('Position Report'); 50 | expect(decodeResult.message.text).toBe(text); 51 | }); 52 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_15_FST.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | // Position Report 7 | export class Label_15_FST extends DecoderPlugin { 8 | name = 'label-15-fst'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ['15'], 13 | preambles: ['FST01'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}): DecodeResult { 18 | const decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'Position Report'; 21 | decodeResult.message = message; 22 | 23 | const parts = message.text.split(' '); 24 | // FST01KMCOEGKKN505552W00118021 25 | const header = parts[0]; 26 | 27 | const stringCoords = header.substring(13) 28 | // Don't use decodeStringCoordinates here, because of different pos format 29 | 30 | const firstChar = stringCoords.substring(0, 1); 31 | const middleChar= stringCoords.substring(7, 8); 32 | decodeResult.raw.position = {}; 33 | 34 | if ((firstChar === 'N' || firstChar === 'S') && (middleChar === 'W' || middleChar === 'E')) { 35 | const lat = (Number(stringCoords.substring(1, 7)) / 10000) * (firstChar === 'S' ? -1 : 1); 36 | const lon = (Number(stringCoords.substring(8, 15)) / 10000) * (middleChar === 'W' ? -1 : 1); 37 | ResultFormatter.position(decodeResult, {latitude: lat, longitude: lon}); 38 | ResultFormatter.altitude(decodeResult, Number(stringCoords.substring(15)) * 100); 39 | } else { 40 | decodeResult.decoded = false; 41 | decodeResult.decoder.decodeLevel = 'none'; 42 | return decodeResult; 43 | } 44 | 45 | ResultFormatter.departureAirport(decodeResult, header.substring(5,9)); 46 | ResultFormatter.arrivalAirport(decodeResult, header.substring(9,13)); 47 | 48 | ResultFormatter.unknownArr(decodeResult, parts.slice(1), ' '); 49 | 50 | decodeResult.decoded = true; 51 | decodeResult.decoder.decodeLevel = 'partial'; 52 | return decodeResult; 53 | } 54 | } 55 | 56 | export default {}; 57 | -------------------------------------------------------------------------------- /lib/plugins/Label_16_N_Space.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_16_N_Space extends DecoderPlugin { 7 | name = 'label-16-n-space'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["16"], 12 | preambles: ['N ', 'S '], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'Position Report'; 20 | decodeResult.message = message; 21 | 22 | // Style: N 44.203,W 86.546,31965,6, 290 23 | let variant1Regex = /^(?[NS])\s(?.*),(?[EW])\s*(?.*),(?.*),(?.*),\s*(?.*)$/; 24 | 25 | // Style: N 28.177/W 96.055 26 | let variant2Regex = /^(?[NS])\s(?.*)\/(?[EW])\s*(?.*)$/; 27 | 28 | let results = message.text.match(variant1Regex); 29 | if (results?.groups) { 30 | if (options.debug) { 31 | console.log(`Label 16 N : results`); 32 | console.log(results); 33 | } 34 | 35 | let pos = { 36 | latitude: Number(results.groups.lat_coord) * (results.groups.lat == 'N' ? 1 : -1), 37 | longitude: Number(results.groups.long_coord) * (results.groups.long == 'E' ? 1 : -1), 38 | }; 39 | const altitude = results.groups.alt == 'GRD' || results.groups.alt == '***' ? 0 : Number(results.groups.alt); 40 | 41 | ResultFormatter.position(decodeResult, pos); 42 | ResultFormatter.altitude(decodeResult, altitude) 43 | 44 | ResultFormatter.unknownArr(decodeResult, [results.groups.unkwn1, results.groups.unkwn2]); 45 | decodeResult.decoded = true; 46 | decodeResult.decoder.decodeLevel = 'partial'; 47 | 48 | return decodeResult 49 | } 50 | 51 | results = message.text.match(variant2Regex) 52 | if (results?.groups) { 53 | if (options.debug) { 54 | console.log(`Label 16 N : results`); 55 | console.log(results); 56 | } 57 | 58 | let pos = { 59 | latitude: Number(results.groups.lat_coord) * (results.groups.lat == 'N' ? 1 : -1), 60 | longitude: Number(results.groups.long_coord) * (results.groups.long == 'E' ? 1 : -1) 61 | }; 62 | 63 | ResultFormatter.position(decodeResult, pos); 64 | 65 | decodeResult.decoded = true; 66 | decodeResult.decoder.decodeLevel = 'full'; 67 | return decodeResult; 68 | } 69 | 70 | // Unknown 71 | if (options.debug) { 72 | console.log(`Decoder: Unknown 16 message: ${message.text}`); 73 | } 74 | ResultFormatter.unknown(decodeResult, message.text); 75 | decodeResult.decoded = false; 76 | decodeResult.decoder.decodeLevel = 'none'; 77 | 78 | return decodeResult; 79 | } 80 | } 81 | 82 | export default {}; 83 | -------------------------------------------------------------------------------- /lib/plugins/Label_16_POSA1.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_16_POSA1 } from './Label_16_POSA1'; 3 | 4 | describe('Label 16 POSA1', () => { 5 | 6 | let plugin: Label_16_POSA1; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_16_POSA1(decoder); 11 | }); 12 | 13 | test('matches qualifiers', () => { 14 | expect(plugin.decode).toBeDefined(); 15 | expect(plugin.name).toBe('label-16-posa1'); 16 | expect(plugin.qualifiers).toBeDefined(); 17 | expect(plugin.qualifiers()).toEqual({ 18 | labels: ['16'], 19 | preambles: ['POSA1'], 20 | }); 21 | }); 22 | test('decodes variant 1', () => { 23 | const text = 'POSA1N37358W 77279,GEARS ,221626,370,BBOBO ,222053,,-61,139,1174,829'; 24 | const decodeResult = plugin.decode({ text: text }); 25 | 26 | expect(decodeResult.decoded).toBe(true); 27 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 28 | expect(decodeResult.decoder.name).toBe('label-16-posa1'); 29 | expect(decodeResult.formatted.description).toBe('Position Report'); 30 | expect(decodeResult.message.text).toBe(text); 31 | expect(decodeResult.formatted.items.length).toBe(3); 32 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 33 | expect(decodeResult.formatted.items[0].value).toBe('37.358 N, 77.279 W'); 34 | expect(decodeResult.formatted.items[1].label).toBe('Altitude'); 35 | expect(decodeResult.formatted.items[1].value).toBe('37000 feet'); 36 | expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); 37 | expect(decodeResult.formatted.items[2].value).toBe('GEARS@22:16:26 > BBOBO@22:20:53'); 38 | expect(decodeResult.remaining.text).toBe(',-61,139,1174,829'); 39 | }); 40 | 41 | test('decodes redacted', () => { 42 | const text = 'POSA1N38843W 78790,RONZZ ,005159,390,RAMAY ,010055,,*****,*****, 744, 0'; 43 | const decodeResult = plugin.decode({ text: text }); 44 | 45 | expect(decodeResult.decoded).toBe(true); 46 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 47 | expect(decodeResult.decoder.name).toBe('label-16-posa1'); 48 | expect(decodeResult.formatted.description).toBe('Position Report'); 49 | expect(decodeResult.message.text).toBe(text); 50 | expect(decodeResult.formatted.items.length).toBe(3); 51 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 52 | expect(decodeResult.formatted.items[0].value).toBe('38.843 N, 78.790 W'); 53 | expect(decodeResult.formatted.items[1].label).toBe('Altitude'); 54 | expect(decodeResult.formatted.items[1].value).toBe('39000 feet'); 55 | expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route'); 56 | expect(decodeResult.formatted.items[2].value).toBe('RONZZ@00:51:59 > RAMAY@01:00:55'); 57 | expect(decodeResult.remaining.text).toBe(',*****,*****, 744, 0'); 58 | }); 59 | 60 | test('decodes Label 16 variant ', () => { 61 | const text = 'N Bogus message'; 62 | const decodeResult = plugin.decode({ text: text }); 63 | 64 | expect(decodeResult.decoded).toBe(false); 65 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 66 | expect(decodeResult.decoder.name).toBe('label-16-posa1'); 67 | expect(decodeResult.formatted.description).toBe('Position Report'); 68 | expect(decodeResult.message.text).toBe(text); 69 | }); 70 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_16_POSA1.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | export class Label_16_POSA1 extends DecoderPlugin { 8 | name = 'label-16-posa1'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ["16"], 13 | preambles: ['POSA1'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}) : DecodeResult { 18 | const decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'Position Report'; 21 | decodeResult.message = message; 22 | 23 | const fields = message.text.split(','); 24 | if (fields.length !== 11 || !fields[0].startsWith('POSA1')) { 25 | if (options.debug) { 26 | console.log(`Decoder: Unknown 16 message: ${message.text}`); 27 | } 28 | decodeResult.remaining.text = message.text; 29 | decodeResult.decoded = false; 30 | decodeResult.decoder.decodeLevel = 'none'; 31 | return decodeResult; 32 | } 33 | 34 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinates(fields[0].substring(5))); // strip 'POSA1' 35 | const waypoint = fields[1].trim(); 36 | const time = DateTimeUtils.convertHHMMSSToTod(fields[2]); 37 | ResultFormatter.altitude(decodeResult, Number(fields[3])*100); 38 | const nextWaypoint = fields[4].trim(); 39 | const nextTime = DateTimeUtils.convertHHMMSSToTod(fields[5]); 40 | ResultFormatter.unknownArr(decodeResult, fields.slice(6), ','); 41 | ResultFormatter.route(decodeResult, {waypoints: [ 42 | {name: waypoint, time: time, timeFormat: 'tod'}, 43 | {name: nextWaypoint, time: nextTime, timeFormat: 'tod'} 44 | ]}); 45 | decodeResult.decoded = true; 46 | decodeResult.decoder.decodeLevel = 'partial'; 47 | 48 | 49 | return decodeResult; 50 | } 51 | } 52 | 53 | export default {}; 54 | -------------------------------------------------------------------------------- /lib/plugins/Label_16_TOD.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | export class Label_16_TOD extends DecoderPlugin { 8 | name = 'label-16-tod'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ["16"], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'Position Report'; 20 | decodeResult.message = message; 21 | 22 | const fields = message.text.split(','); 23 | const time = DateTimeUtils.convertHHMMSSToTod(fields[0]) 24 | if (fields.length !== 5 || Number.isNaN(time)) { 25 | if (options.debug) { 26 | console.log(`Decoder: Unknown 16 message: ${message.text}`); 27 | } 28 | decodeResult.remaining.text = message.text; 29 | decodeResult.decoded = false; 30 | decodeResult.decoder.decodeLevel = 'none'; 31 | return decodeResult; 32 | } 33 | 34 | ResultFormatter.time_of_day(decodeResult, time); 35 | if(fields[1] !== '') { 36 | ResultFormatter.altitude(decodeResult, Number(fields[1])); 37 | } 38 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[2])); 39 | ResultFormatter.unknown(decodeResult, fields[3]); 40 | const temp = fields[4].split('/'); 41 | const posFields = temp[0].split(' '); 42 | ResultFormatter.position(decodeResult, { 43 | latitude: CoordinateUtils.getDirection(posFields[0]) * Number(posFields[1]), 44 | longitude: CoordinateUtils.getDirection(posFields[2]) * Number(posFields[3]), 45 | }); 46 | 47 | if(temp.length > 1) { 48 | ResultFormatter.flightNumber(decodeResult, temp[1]); 49 | } 50 | decodeResult.decoded = true; 51 | decodeResult.decoder.decodeLevel = 'partial'; 52 | 53 | 54 | return decodeResult; 55 | } 56 | } 57 | 58 | export default {}; 59 | -------------------------------------------------------------------------------- /lib/plugins/Label_1J_2J_FTX.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_1J_2J_FTX } from './Label_1J_2J_FTX'; 3 | 4 | describe('Label 1J/2J FTX', () => { 5 | 6 | let plugin: Label_1J_2J_FTX; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_1J_2J_FTX(decoder); 11 | }); 12 | 13 | 14 | test('decodes Label 1J', () => { 15 | // https://app.airframes.io/messages/4178692503 16 | const text = 'FTX/ID50007B,RCH4086,ABB02R70E037/MR6,/FX4 QTR PHILLY UP 37-6307A' 17 | const decodeResult = plugin.decode({ text: text }); 18 | 19 | expect(decodeResult.decoded).toBe(true); 20 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 21 | expect(decodeResult.raw.mission_number).toBe('ABB02R70E037'); 22 | expect(decodeResult.formatted.items.length).toBe(4); 23 | expect(decodeResult.formatted.items[0].label).toBe('Tail'); 24 | expect(decodeResult.formatted.items[0].value).toBe('50007B'); 25 | expect(decodeResult.formatted.items[1].label).toBe('Flight Number'); 26 | expect(decodeResult.formatted.items[1].value).toBe('RCH4086'); 27 | expect(decodeResult.formatted.items[2].label).toBe('Free Text'); 28 | expect(decodeResult.formatted.items[2].value).toBe('4 QTR PHILLY UP 37-6'); 29 | expect(decodeResult.formatted.items[3].label).toBe('Message Checksum'); 30 | expect(decodeResult.formatted.items[3].value).toBe('0x307a'); 31 | expect(decodeResult.remaining.text).toBe('MR6,'); 32 | }); 33 | 34 | test('decodes Label 2J', () => { 35 | // https://app.airframes.io/messages/4178362466 36 | const text = 'M74AMC4086FTX/ID50007B,RCH4086,ABB02R70E037/DC10022025,011728/MR049,/FXGOOD EVENING PLEASE PASS US THE SUPER BOWL SCORE WHEN ABLE. THANK YOU/FB1791/VR0328D70' 37 | const decodeResult = plugin.decode({ text: text }); 38 | 39 | expect(decodeResult.decoded).toBe(true); 40 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 41 | expect(decodeResult.raw.mission_number).toBe('ABB02R70E037'); 42 | expect(decodeResult.formatted.items.length).toBe(5); 43 | expect(decodeResult.formatted.items[0].label).toBe('Flight Number'); 44 | expect(decodeResult.formatted.items[0].value).toBe('MC4086'); 45 | expect(decodeResult.formatted.items[1].label).toBe('Tail'); 46 | expect(decodeResult.formatted.items[1].value).toBe('50007B'); 47 | expect(decodeResult.formatted.items[2].label).toBe('Flight Number'); 48 | expect(decodeResult.formatted.items[2].value).toBe('RCH4086'); 49 | expect(decodeResult.formatted.items[3].label).toBe('Free Text'); 50 | expect(decodeResult.formatted.items[3].value).toBe('GOOD EVENING PLEASE PASS US THE SUPER BOWL SCORE WHEN ABLE. THANK YOU'); 51 | expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); 52 | expect(decodeResult.formatted.items[4].value).toBe('0x8d70'); 53 | expect(decodeResult.remaining.text).toBe('M74A/MR049,/FB1791/VR032'); 54 | }); 55 | 56 | test('decodes ', () => { 57 | 58 | const text = 'FTX Bogus message'; 59 | const decodeResult = plugin.decode({ text: text }); 60 | 61 | expect(decodeResult.decoded).toBe(false); 62 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 63 | expect(decodeResult.formatted.description).toBe('Free Text'); 64 | expect(decodeResult.message.text).toBe(text); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /lib/plugins/Label_1J_2J_FTX.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { H1Helper } from '../utils/h1_helper'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_1J_2J_FTX extends DecoderPlugin { 7 | name = 'label-1j-2j-ftx'; 8 | qualifiers() { // eslint-disable-line class-methods-use-this 9 | return { 10 | labels: ['1J', '2J'], 11 | }; 12 | } 13 | 14 | decode(message: Message, options: Options = {}) : DecodeResult { 15 | let decodeResult = this.defaultResult(); 16 | decodeResult.decoder.name = this.name; 17 | decodeResult.message = message; 18 | 19 | const msg = message.text.replace(/\n|\r/g, ""); 20 | const decoded = H1Helper.decodeH1Message(decodeResult, msg); 21 | decodeResult.decoded = decoded; 22 | 23 | decodeResult.decoder.decodeLevel = !decodeResult.remaining.text ? 'full' : 'partial'; 24 | if (decodeResult.formatted.items.length === 0) { 25 | if (options.debug) { 26 | console.log(`Decoder: Unknown 1J/2J message: ${message.text}`); 27 | } 28 | ResultFormatter.unknown(decodeResult, message.text); 29 | decodeResult.decoded = false; 30 | decodeResult.decoder.decodeLevel = 'none'; 31 | } 32 | return decodeResult; 33 | } 34 | } 35 | 36 | export default {}; 37 | -------------------------------------------------------------------------------- /lib/plugins/Label_1L_070.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_1L_070 } from './Label_1L_070'; 3 | 4 | describe('Label_1L 070', () => { 5 | let plugin: Label_1L_070; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_1L_070(decoder); 10 | }); 11 | 12 | test('matches qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-1l-070'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['1L'], 18 | preambles: ['000000070'], 19 | }); 20 | }); 21 | 22 | test('decodes variant 1', () => { 23 | // https://app.airframes.io/messages/3492019143 24 | const text = '000000070LOWW,KEWR,0932,1744,N 49.223,E 12.038,0659' 25 | const decodeResult = plugin.decode({ text: text }); 26 | 27 | expect(decodeResult.decoded).toBe(true); 28 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 29 | expect(decodeResult.raw.departure_icao).toBe('LOWW'); 30 | expect(decodeResult.raw.arrival_icao).toBe('KEWR'); 31 | expect(decodeResult.raw.time_of_day).toBe(34320); 32 | expect(decodeResult.raw.eta_time).toBe(63840); 33 | expect(decodeResult.raw.position.latitude).toBe(49.223); 34 | expect(decodeResult.raw.position.longitude).toBe(12.038); 35 | expect(decodeResult.formatted.items.length).toBe(5); 36 | expect(decodeResult.formatted.items[0].label).toBe('Origin'); 37 | expect(decodeResult.formatted.items[0].value).toBe('LOWW'); 38 | expect(decodeResult.formatted.items[1].label).toBe('Destination'); 39 | expect(decodeResult.formatted.items[1].value).toBe('KEWR'); 40 | expect(decodeResult.formatted.items[2].label).toBe('Message Timestamp'); 41 | expect(decodeResult.formatted.items[2].value).toBe('09:32:00'); 42 | expect(decodeResult.formatted.items[3].label).toBe('Estimated Time of Arrival'); 43 | expect(decodeResult.formatted.items[3].value).toBe('17:44:00'); 44 | expect(decodeResult.formatted.items[4].label).toBe('Aircraft Position'); 45 | expect(decodeResult.formatted.items[4].value).toBe('49.223 N, 12.038 E'); 46 | expect(decodeResult.remaining.text).toBe('0659'); 47 | }); 48 | 49 | test('does not decode ', () => { 50 | 51 | const text = 'POS Bogus Message'; 52 | const decodeResult = plugin.decode({ text: text }); 53 | 54 | expect(decodeResult.decoded).toBe(false); 55 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 56 | expect(decodeResult.decoder.name).toBe('label-1l-070'); 57 | expect(decodeResult.formatted.description).toBe('Position Report'); 58 | expect(decodeResult.message.text).toBe(text); 59 | }); 60 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_1L_070.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | export class Label_1L_070 extends DecoderPlugin { // eslint-disable-line camelcase 8 | name = 'label-1l-070'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ['1L'], 13 | preambles: ['000000070'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}): DecodeResult { 18 | const decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'Position Report'; 21 | decodeResult.message = message; 22 | 23 | if (!message.text.startsWith('000000070')) { 24 | if (options.debug) { 25 | console.log(`Decoder: Unknown 1L message: ${message.text}`); 26 | } 27 | decodeResult.remaining.text = message.text; 28 | decodeResult.decoded = false; 29 | decodeResult.decoder.decodeLevel = 'none'; 30 | return decodeResult; 31 | } 32 | 33 | const parts = message.text.substring(9).split(','); 34 | 35 | if (parts.length !== 7) { 36 | if (options.debug) { 37 | console.log(`Decoder: Unknown 1L message: ${message.text}`); 38 | } 39 | decodeResult.remaining.text = message.text; 40 | decodeResult.decoded = false; 41 | decodeResult.decoder.decodeLevel = 'none'; 42 | return decodeResult; 43 | } 44 | 45 | ResultFormatter.departureAirport(decodeResult, parts[0]); 46 | ResultFormatter.arrivalAirport(decodeResult, parts[1]); 47 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[2])); 48 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[3])); 49 | ResultFormatter.position(decodeResult, { 50 | latitude: CoordinateUtils.getDirection(parts[4][0]) * Number(parts[4].substring(1)), 51 | longitude: CoordinateUtils.getDirection(parts[5][0]) * Number(parts[5].substring(1)), 52 | }); 53 | 54 | 55 | decodeResult.remaining.text = parts[6]; 56 | 57 | decodeResult.decoded = true; 58 | decodeResult.decoder.decodeLevel = 'partial'; 59 | return decodeResult; 60 | } 61 | } 62 | 63 | export default {}; -------------------------------------------------------------------------------- /lib/plugins/Label_1L_660.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_1L_660 } from './Label_1L_660'; 3 | 4 | describe('Label_1L 660', () => { 5 | let plugin: Label_1L_660; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_1L_660(decoder); 10 | }); 11 | 12 | test('matches qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-1l-660'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['1L'], 18 | preambles: ['000000660'], 19 | }); 20 | }); 21 | 22 | test('decodes variant 1', () => { 23 | // https://app.airframes.io/messages/3492135103 24 | const text = '000000660N50442E005566,100444359SOG-06 ,,--- 21-,83617441' 25 | const decodeResult = plugin.decode({ text: text }); 26 | 27 | expect(decodeResult.decoded).toBe(true); 28 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 29 | expect(decodeResult.raw.position.latitude).toBe(50.736666666666665); 30 | expect(decodeResult.raw.position.longitude).toBe(5.943333333333333); 31 | expect(decodeResult.raw.time_of_day).toBe(36284); 32 | expect(decodeResult.raw.altitude).toBe(35900); 33 | expect(decodeResult.formatted.items.length).toBe(4); 34 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 35 | expect(decodeResult.formatted.items[0].value).toBe('50.737 N, 5.943 E'); 36 | expect(decodeResult.formatted.items[1].label).toBe('Message Timestamp'); 37 | expect(decodeResult.formatted.items[1].value).toBe('10:04:44'); 38 | expect(decodeResult.formatted.items[2].label).toBe('Altitude'); 39 | expect(decodeResult.formatted.items[2].value).toBe('35900 feet'); 40 | expect(decodeResult.formatted.items[3].label).toBe('Aircraft Route'); 41 | expect(decodeResult.formatted.items[3].value).toBe('SOG-06'); 42 | expect(decodeResult.remaining.text).toBe(',--- 21-,83617441'); 43 | }); 44 | 45 | test('does not decode ', () => { 46 | 47 | const text = 'POS Bogus Message'; 48 | const decodeResult = plugin.decode({ text: text }); 49 | 50 | expect(decodeResult.decoded).toBe(false); 51 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 52 | expect(decodeResult.decoder.name).toBe('label-1l-660'); 53 | expect(decodeResult.formatted.description).toBe('Position Report'); 54 | expect(decodeResult.message.text).toBe(text); 55 | }); 56 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_1L_660.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | export class Label_1L_660 extends DecoderPlugin { // eslint-disable-line camelcase 8 | name = 'label-1l-660'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ['1L'], 13 | preambles: ['000000660'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}): DecodeResult { 18 | const decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'Position Report'; 21 | decodeResult.message = message; 22 | 23 | if (!message.text.startsWith('000000660')) { 24 | if (options.debug) { 25 | console.log(`Decoder: Unknown 1L message: ${message.text}`); 26 | } 27 | decodeResult.remaining.text = message.text; 28 | decodeResult.decoded = false; 29 | decodeResult.decoder.decodeLevel = 'none'; 30 | return decodeResult; 31 | } 32 | 33 | 34 | const parts = message.text.substring(9).split(','); 35 | 36 | if (parts.length !== 5) { 37 | if (options.debug) { 38 | console.log(`Decoder: Unknown 1L message: ${message.text}`); 39 | } 40 | decodeResult.remaining.text = message.text; 41 | decodeResult.decoded = false; 42 | decodeResult.decoder.decodeLevel = 'none'; 43 | return decodeResult; 44 | } 45 | 46 | const position = CoordinateUtils.decodeStringCoordinatesDecimalMinutes(parts[0]); 47 | if (position) { 48 | ResultFormatter.position(decodeResult, position); 49 | } 50 | const hhmmss = parts[1].substring(0, 6); 51 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(hhmmss)); 52 | const fl = parts[1].substring(6, 9); 53 | ResultFormatter.altitude(decodeResult, Number(fl) * 100); 54 | const next = parts[1].substring(9); 55 | ResultFormatter.route(decodeResult, { waypoints: [{ name: next.trim() }] }); 56 | 57 | decodeResult.remaining.text = parts.slice(2).join(','); 58 | 59 | decodeResult.decoded = true; 60 | decodeResult.decoder.decodeLevel = 'partial'; 61 | return decodeResult; 62 | } 63 | } 64 | 65 | export default {}; -------------------------------------------------------------------------------- /lib/plugins/Label_1L_Slash.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_1L_Slash } from './Label_1L_Slash'; 3 | 4 | describe('Label_1L Slash', () => { 5 | let plugin: Label_1L_Slash; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_1L_Slash(decoder); 10 | }); 11 | 12 | test('matches qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-1l-1-line'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['1L'], 18 | preambles: ['+', '-'], 19 | }); 20 | }); 21 | 22 | test('decodes variant 1', () => { 23 | const text = '+ 39.126/- 77.358/UTC 085208/FOB 8.2/ALT 3997/CAS 239/ETA 0903' 24 | const decodeResult = plugin.decode({ text: text }); 25 | 26 | expect(decodeResult.decoded).toBe(true); 27 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 28 | expect(decodeResult.raw.position.latitude).toBe(39.126); 29 | expect(decodeResult.raw.position.longitude).toBe(-77.358); 30 | expect(decodeResult.raw.time_of_day).toBe(31928); 31 | expect(decodeResult.raw.fuel_on_board).toBe(8.2); 32 | expect(decodeResult.raw.altitude).toBe(3997); 33 | expect(decodeResult.raw.eta_time).toBe(32580); 34 | expect(decodeResult.formatted.items.length).toBe(5); 35 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 36 | expect(decodeResult.formatted.items[0].value).toBe('39.126 N, 77.358 W'); 37 | expect(decodeResult.formatted.items[1].label).toBe('Message Timestamp'); 38 | expect(decodeResult.formatted.items[1].value).toBe('08:52:08'); 39 | expect(decodeResult.formatted.items[2].label).toBe('Altitude'); 40 | expect(decodeResult.formatted.items[2].value).toBe('3997 feet'); 41 | expect(decodeResult.formatted.items[3].label).toBe('Fuel On Board'); 42 | expect(decodeResult.formatted.items[3].value).toBe('8.2'); // tons? 43 | expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival'); 44 | expect(decodeResult.formatted.items[4].value).toBe('09:03:00'); 45 | expect(decodeResult.remaining.text).toBe('/CAS 239'); 46 | }); 47 | 48 | test('does not decode ', () => { 49 | 50 | const text = 'POS Bogus Message'; 51 | const decodeResult = plugin.decode({ text: text }); 52 | 53 | expect(decodeResult.decoded).toBe(false); 54 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 55 | expect(decodeResult.decoder.name).toBe('label-1l-1-line'); 56 | expect(decodeResult.formatted.description).toBe('Position Report'); 57 | expect(decodeResult.message.text).toBe(text); 58 | }); 59 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_1L_Slash.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_1L_Slash extends DecoderPlugin { // eslint-disable-line camelcase 7 | name = 'label-1l-1-line'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['1L'], 12 | preambles: ['+', '-'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}): DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'Position Report'; 20 | decodeResult.message = message; 21 | 22 | const parts = message.text.split('/'); 23 | 24 | if (parts.length !== 7) { 25 | if (options.debug) { 26 | console.log(`Decoder: Unknown 1L message: ${message.text}`); 27 | } 28 | decodeResult.remaining.text = message.text; 29 | decodeResult.decoded = false; 30 | decodeResult.decoder.decodeLevel = 'none'; 31 | return decodeResult; 32 | } 33 | 34 | const data = new Map(); 35 | data.set('LAT', parts[0].replaceAll(' ', '')); 36 | data.set('LON', parts[1].replaceAll(' ', '')); 37 | for (let i = 2; i < parts.length; i++) { 38 | const part = parts[i].split(' '); 39 | data.set(part[0], part.slice(1).join(' ')); 40 | } 41 | 42 | const position = { 43 | latitude: Number(data.get('LAT')), 44 | longitude: Number(data.get('LON')), 45 | } 46 | data.delete('LAT'); 47 | data.delete('LON'); 48 | 49 | ResultFormatter.position(decodeResult, position); 50 | const utc = data.get('UTC'); 51 | if (utc) { 52 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(utc)); 53 | data.delete('UTC'); 54 | } 55 | const alt = data.get('ALT'); 56 | if (alt) { 57 | ResultFormatter.altitude(decodeResult, Number(alt)); 58 | data.delete('ALT'); 59 | } 60 | const fob = data.get('FOB'); 61 | if (fob) { 62 | ResultFormatter.currentFuel(decodeResult, Number(fob)); 63 | data.delete('FOB'); 64 | } 65 | const eta = data.get('ETA'); 66 | if (eta) { 67 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(eta)); 68 | data.delete('ETA'); 69 | } 70 | 71 | let remaining = ''; 72 | for (const [key, value] of data.entries()) { 73 | remaining += `/${key} ${value}`; 74 | } 75 | decodeResult.remaining.text = remaining; 76 | 77 | decodeResult.decoded = true; 78 | decodeResult.decoder.decodeLevel = 'partial'; 79 | return decodeResult; 80 | } 81 | } 82 | 83 | export default {}; -------------------------------------------------------------------------------- /lib/plugins/Label_1M_Slash.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_1M_Slash } from './Label_1M_Slash'; 3 | 4 | test('decodes Label 8E sample 1', () => { 5 | const decoder = new MessageDecoder(); 6 | const decoderPlugin = new Label_1M_Slash(decoder); 7 | 8 | expect(decoderPlugin.decode).toBeDefined(); 9 | expect(decoderPlugin.name).toBe('label-1m-slash'); 10 | expect(decoderPlugin.qualifiers).toBeDefined(); 11 | expect(decoderPlugin.qualifiers()).toEqual({ 12 | labels: ['1M'], 13 | preambles: ['/'], 14 | }); 15 | 16 | const text = '/BA0843/ETA01/230822/LDSP/EGLL/EGSS/2JK0\n1940/EGLL27L/10'; 17 | const decodeResult = decoderPlugin.decode({ text: text }); 18 | 19 | expect(decodeResult.decoded).toBe(true); 20 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 21 | expect(decodeResult.decoder.name).toBe('label-1m-slash'); 22 | expect(decodeResult.formatted.description).toBe('ETA Report'); 23 | expect(decodeResult.message.text).toBe(text); 24 | expect(decodeResult.raw.alternate_icao).toBe('EGSS'); 25 | expect(decodeResult.raw.arrival_icao).toBe('EGLL'); 26 | expect(decodeResult.raw.arrival_runway).toBe('27L'); 27 | expect(decodeResult.raw.departure_icao).toBe('LDSP'); 28 | expect(decodeResult.raw.flight_number).toBe('BA0843'); 29 | expect(decodeResult.formatted.items.length).toBe(5); 30 | expect(decodeResult.formatted.items[0].type).toBe('icao'); 31 | expect(decodeResult.formatted.items[0].code).toBe('ORG'); 32 | expect(decodeResult.formatted.items[0].label).toBe('Origin'); 33 | expect(decodeResult.formatted.items[0].value).toBe('LDSP'); 34 | expect(decodeResult.formatted.items[1].type).toBe('icao'); 35 | expect(decodeResult.formatted.items[1].code).toBe('DST'); 36 | expect(decodeResult.formatted.items[1].label).toBe('Destination'); 37 | expect(decodeResult.formatted.items[1].value).toBe('EGLL'); 38 | expect(decodeResult.formatted.items[2].type).toBe('icao'); 39 | expect(decodeResult.formatted.items[2].code).toBe('ALT_DST'); 40 | expect(decodeResult.formatted.items[2].label).toBe('Alternate Destination'); 41 | expect(decodeResult.formatted.items[2].value).toBe('EGSS'); 42 | expect(decodeResult.formatted.items[3].type).toBe('runway'); 43 | expect(decodeResult.formatted.items[3].code).toBe('ARWY'); 44 | expect(decodeResult.formatted.items[3].label).toBe('Arrival Runway'); 45 | expect(decodeResult.formatted.items[3].value).toBe('27L'); 46 | expect(decodeResult.formatted.items[4].type).toBe('epoch'); 47 | expect(decodeResult.formatted.items[4].code).toBe('ETA'); 48 | expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival'); 49 | expect(decodeResult.formatted.items[4].value).toBe('2023-08-22T19:40:00Z'); 50 | }); 51 | -------------------------------------------------------------------------------- /lib/plugins/Label_1M_Slash.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_1M_Slash extends DecoderPlugin { 7 | name = 'label-1m-slash'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["1M"], 12 | preambles: ['/'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'ETA Report'; 20 | decodeResult.message = message; 21 | 22 | // Style: /BA0843/ETA01/230822/LDSP/EGLL/EGSS/2JK0(NEW LINE)1940/EGLL27L/10 23 | const results = message.text.split(/\n|\//).slice(1); // Split by / and new line 24 | 25 | if (results) { 26 | if (options.debug) { 27 | console.log(`Label 1M ETA: results`); 28 | console.log(results); 29 | } 30 | 31 | decodeResult.raw.flight_number = results[0]; 32 | // results[1]: ETA01 (???) 33 | // results[2]: 230822 - UTC date of eta 34 | ResultFormatter.departureAirport(decodeResult, results[3]); 35 | ResultFormatter.arrivalAirport(decodeResult, results[4]); 36 | ResultFormatter.alternateAirport(decodeResult, results[5]); 37 | // results[6]: 2JK0 (???) 38 | // results[7] 1940 - UTC eta 39 | ResultFormatter.arrivalRunway(decodeResult, results[8].replace(results[4], "")); // results[8] EGLL27L 40 | // results[9]: 10(space) (???) 41 | 42 | const yymmdd = results[2]; 43 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertDateTimeToEpoch(results[7]+'00', yymmdd.substring(2,4)+yymmdd.substring(4,6)+yymmdd.substring(0,2)), 'epoch') 44 | 45 | } 46 | 47 | decodeResult.decoded = true; 48 | decodeResult.decoder.decodeLevel = 'partial'; 49 | 50 | return decodeResult; 51 | } 52 | } 53 | 54 | export default {}; 55 | -------------------------------------------------------------------------------- /lib/plugins/Label_20_CFB.01.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | // In Air Report 7 | export class Label_20_CFB01 extends DecoderPlugin { 8 | name = 'label-20-cfb01'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ['20'], 13 | preambles: ['#CFB.01'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}): DecodeResult { 18 | const decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'Crew Flight Bag Message'; 21 | decodeResult.message = message; 22 | 23 | // Style: IN02,N38338W121179,KMHR,KPDX,0806,2355,005.1 24 | // Match: IN02,coords,departure_icao,arrival_icao,current_date,current_time,fuel_in_tons 25 | const regex = /^IN02,(?.*),(?.*),(?.*),(?.*),(?.*),(?.*)$/; 26 | const results = message.text.match(regex); 27 | if (results?.groups) { 28 | if (options.debug) { 29 | console.log(`Label 44 ETA Report: groups`); 30 | console.log(results.groups); 31 | } 32 | 33 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinates(results.groups.unsplit_coords)); 34 | ResultFormatter.departureAirport(decodeResult, results.groups.departure_icao); 35 | ResultFormatter.arrivalAirport(decodeResult, results.groups.arrival_icao); 36 | 37 | decodeResult.raw.current_time = Date.parse( 38 | new Date().getFullYear() + "-" + 39 | results.groups.current_date.substr(0, 2) + "-" + 40 | results.groups.current_date.substr(2, 2) + "T" + 41 | results.groups.current_time.substr(0, 2) + ":" + 42 | results.groups.current_time.substr(2, 2) + ":00Z" 43 | ); 44 | 45 | if (results.groups.fuel_in_tons != '***' && results.groups.fuel_in_tons != '****') { 46 | decodeResult.raw.fuel_in_tons = Number(results.groups.fuel_in_tons); 47 | } 48 | 49 | } 50 | 51 | decodeResult.decoded = true; 52 | decodeResult.decoder.decodeLevel = 'full'; 53 | 54 | return decodeResult; 55 | } 56 | } 57 | 58 | export default {}; 59 | -------------------------------------------------------------------------------- /lib/plugins/Label_20_POS.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | // Position Report 7 | export class Label_20_POS extends DecoderPlugin { 8 | name = 'label-20-pos'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ['20'], 13 | preambles: ['POS'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}): DecodeResult { 18 | const decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'Position Report'; 21 | decodeResult.message = message; 22 | 23 | decodeResult.raw.preamble = message.text.substring(0, 3); 24 | 25 | const content = message.text.substring(3); 26 | const fields = content.split(','); 27 | 28 | if (fields.length == 11) { 29 | // N38160W077075,,211733,360,OTT,212041,,N42,19689,40,544 30 | if (options.debug) { 31 | console.log(`DEBUG: ${this.name}: Variation 1 detected`); 32 | } 33 | // Field 1: Coordinates 34 | const rawCoords = fields[0]; 35 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinates(rawCoords)); 36 | 37 | decodeResult.decoded = true; 38 | decodeResult.decoder.decodeLevel = 'full'; 39 | } else if (fields.length == 5) { 40 | // N38160W077075,,211733,360,OTT 41 | if (options.debug) { 42 | console.log(`DEBUG: ${this.name}: Variation 2 detected`); 43 | } 44 | // Field 1: Coordinates 45 | const position = CoordinateUtils.decodeStringCoordinates(fields[0]); 46 | if (position) { 47 | ResultFormatter.position(decodeResult, position); 48 | } 49 | decodeResult.decoded = true; 50 | decodeResult.decoder.decodeLevel = 'full'; 51 | } else { 52 | // Unknown! 53 | if (options.debug) { 54 | console.log(`DEBUG: ${this.name}: Unknown variation. Field count: ${fields.length}, content: ${content}`); 55 | } 56 | decodeResult.decoded = false; 57 | decodeResult.decoder.decodeLevel = 'none'; 58 | } 59 | return decodeResult; 60 | } 61 | } 62 | 63 | export default {}; 64 | -------------------------------------------------------------------------------- /lib/plugins/Label_21_POS.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_21_POS } from './Label_21_POS'; 3 | 4 | describe('Label_21_POS', () => { 5 | let plugin: Label_21_POS; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_21_POS(decoder); 10 | }); 11 | 12 | test('matches qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-21-pos'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['21'], 18 | preambles: ['POS'], 19 | }); 20 | }); 21 | 22 | 23 | test('decodes valid', () => { 24 | const text = 'POSN 39.841W 75.790, 220,184218,17222,22051, 34,- 4,204748,KTPA' 25 | const decodeResult = plugin.decode({ text: text }); 26 | expect(decodeResult.decoded).toBe(true); 27 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 28 | expect(decodeResult.formatted.items.length).toBe(6); 29 | expect(decodeResult.formatted.items[0].type).toBe('aircraft_position'); 30 | expect(decodeResult.formatted.items[0].code).toBe('POS'); 31 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 32 | expect(decodeResult.formatted.items[0].value).toBe('39.840 N, 75.790 W'); 33 | expect(decodeResult.formatted.items[1].type).toBe('time_of_day'); 34 | expect(decodeResult.formatted.items[1].code).toBe('MSG_TOD'); 35 | expect(decodeResult.formatted.items[1].label).toBe('Message Timestamp'); 36 | expect(decodeResult.formatted.items[1].value).toBe('18:42:18'); 37 | expect(decodeResult.formatted.items[2].type).toBe('altitude'); 38 | expect(decodeResult.formatted.items[2].code).toBe('ALT'); 39 | expect(decodeResult.formatted.items[2].label).toBe('Altitude'); 40 | expect(decodeResult.formatted.items[2].value).toBe('17222 feet'); 41 | expect(decodeResult.formatted.items[3].type).toBe('outside_air_temperature'); 42 | expect(decodeResult.formatted.items[3].code).toBe('OATEMP'); 43 | expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)'); 44 | expect(decodeResult.formatted.items[3].value).toBe('-4 degrees'); 45 | expect(decodeResult.formatted.items[4].type).toBe('time_of_day'); 46 | expect(decodeResult.formatted.items[4].code).toBe('ETA'); 47 | expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival'); 48 | expect(decodeResult.formatted.items[4].value).toBe('20:47:48'); 49 | expect(decodeResult.formatted.items[5].type).toBe('icao'); 50 | expect(decodeResult.formatted.items[5].code).toBe('DST'); 51 | expect(decodeResult.formatted.items[5].label).toBe('Destination'); 52 | expect(decodeResult.formatted.items[5].value).toBe('KTPA'); 53 | expect(decodeResult.remaining.text).toBe(' 220,22051, 34'); 54 | }); 55 | 56 | test('does not decode invalid', () => { 57 | 58 | const text = 'POS Bogus message'; 59 | const decodeResult = plugin.decode({ text: text }); 60 | 61 | expect(decodeResult.decoded).toBe(false); 62 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 63 | expect(decodeResult.decoder.name).toBe('label-21-pos'); 64 | expect(decodeResult.formatted.description).toBe('Position Report'); 65 | expect(decodeResult.message.text).toBe(text); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /lib/plugins/Label_21_POS.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // Position Report 8 | export class Label_21_POS extends DecoderPlugin { 9 | name = 'label-21-pos'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['21'], 14 | preambles: ['POS'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}) : DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'Position Report'; 22 | decodeResult.message = message; 23 | 24 | decodeResult.raw.preamble = message.text.substring(0, 3); 25 | 26 | const content = message.text.substring(3); 27 | const fields = content.split(','); 28 | 29 | if (fields.length == 9) { 30 | // POSN 37.550W 76.436, 98,110800,23961,25820, 65,-23,114212,KRDU 31 | processPosition(decodeResult, fields[0].trim()); 32 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[2])); 33 | ResultFormatter.altitude( decodeResult, Number(fields[3])); 34 | ResultFormatter.temperature(decodeResult, fields[6].replace(/ /g, "")); 35 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[7])); 36 | ResultFormatter.arrivalAirport(decodeResult, fields[8]); 37 | 38 | ResultFormatter.unknownArr(decodeResult, [fields[1], fields[4], fields[5]]); 39 | 40 | decodeResult.decoded = true; 41 | decodeResult.decoder.decodeLevel = 'partial'; 42 | } else { 43 | // Unknown! 44 | if(options.debug) { 45 | console.log(`DEBUG: ${this.name}: Unknown variation. Field count: ${fields.length}, content: ${content}`); 46 | } 47 | decodeResult.decoded = false; 48 | decodeResult.decoder.decodeLevel = 'none'; 49 | } 50 | return decodeResult; 51 | } 52 | } 53 | function processPosition(decodeResult: DecodeResult, value: string) { 54 | // N 39.841W 75.790 55 | if(value.length !== 16 && value[0] !== 'N' && value[0] !== 'S' && value[8] !== 'W' && value[8] !== 'E') { 56 | return; 57 | } 58 | const latDir = value[0] === 'N' ? 1 : -1; 59 | const lonDir = value[8] === 'E' ? 1 : -1; 60 | const position ={ 61 | latitude: latDir * Number(value.substring(1, 7)), 62 | longitude: lonDir * Number(value.substring(9, 15)), 63 | }; 64 | 65 | ResultFormatter.position(decodeResult, position); 66 | } 67 | 68 | export default {}; 69 | 70 | -------------------------------------------------------------------------------- /lib/plugins/Label_22_POS.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_22_POS } from './Label_22_POS'; 3 | 4 | describe('Label 22', () => { 5 | let plugin: Label_22_POS; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_22_POS(decoder); 10 | }); 11 | 12 | test('matches qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-22-pos'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['22'], 18 | preambles: ['N', 'S'], 19 | }); 20 | }); 21 | 22 | test('decodes valid', () => { 23 | const text = 'N 370824W 760010,-------,194936,30418, , , ,M 42,27335 42, 107,' 24 | const decodeResult = plugin.decode({ text: text }); 25 | expect(decodeResult.decoded).toBe(true); 26 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 27 | expect(decodeResult.raw.position.latitude).toBe(37.0824); 28 | expect(decodeResult.raw.position.longitude).toBe(-76.001); 29 | expect(decodeResult.raw.time_of_day).toBe(71376); 30 | expect(decodeResult.raw.altitude).toBe(30418); 31 | expect(decodeResult.formatted.items.length).toBe(3); 32 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 33 | expect(decodeResult.formatted.items[0].value).toBe('37.082 N, 76.001 W'); 34 | expect(decodeResult.formatted.items[1].label).toBe('Message Timestamp'); 35 | expect(decodeResult.formatted.items[1].value).toBe('19:49:36'); 36 | expect(decodeResult.formatted.items[2].label).toBe('Altitude'); 37 | expect(decodeResult.formatted.items[2].value).toBe('30418 feet'); 38 | expect(decodeResult.remaining.text).toBe('-------, , , ,M 42,27335 42, 107,'); 39 | }); 40 | 41 | test('does not decode invalid', () => { 42 | 43 | const text = 'POS Bogus message'; 44 | const decodeResult = plugin.decode({ text: text }); 45 | 46 | expect(decodeResult.decoded).toBe(false); 47 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 48 | expect(decodeResult.message.text).toBe(text); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /lib/plugins/Label_22_POS.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // Position Report 8 | export class Label_22_POS extends DecoderPlugin { 9 | name = 'label-22-pos'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['22'], 14 | preambles: ['N', 'S'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}): DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'Position Report'; 22 | decodeResult.message = message; 23 | 24 | const fields = message.text.split(','); 25 | 26 | if (fields.length !== 11) { 27 | if (options.debug) { 28 | console.log(`DEBUG: ${this.name}: Unknown variation. Field count: ${fields.length}, content: ${fields.join(',')}`); 29 | } 30 | decodeResult.decoded = false; 31 | decodeResult.decoder.decodeLevel = 'none'; 32 | return decodeResult; 33 | } 34 | 35 | const latStr = fields[0].substring(1, 8); 36 | const lonStr = fields[0].substring(9); 37 | const lat = Number(latStr) / 10000; 38 | const lon = Number(lonStr) / 10000; 39 | ResultFormatter.position(decodeResult, { 40 | latitude: CoordinateUtils.getDirection(fields[0][0]) * lat, 41 | longitude: CoordinateUtils.getDirection(fields[0][8]) * lon, 42 | }); 43 | 44 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[2])); 45 | ResultFormatter.altitude(decodeResult, Number(fields[3])); 46 | 47 | ResultFormatter.unknownArr(decodeResult, [fields[1], ...fields.slice(4)]); 48 | 49 | decodeResult.decoded = true; 50 | decodeResult.decoder.decodeLevel = 'partial'; 51 | return decodeResult; 52 | } 53 | } 54 | 55 | export default {}; 56 | -------------------------------------------------------------------------------- /lib/plugins/Label_24_Slash.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_24_Slash } from './Label_24_Slash'; 3 | 4 | describe('Label_24_Slash', () => { 5 | let plugin: Label_24_Slash; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_24_Slash(decoder); 10 | }); 11 | 12 | test('matches qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-24-slash'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['24'], 18 | preambles: ['/'], 19 | }); 20 | }); 21 | 22 | 23 | test('valid', () => { 24 | // https://app.airframes.io/messages/3439806391 25 | const text = '/241710/1021/04WM/34962/N53.13/E001.33/3374/1056/'; 26 | const decodeResult = plugin.decode({ text: text }); 27 | expect(decodeResult.decoded).toBe(true); 28 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 29 | expect(decodeResult.raw.message_timestamp).toBe(1729160460); 30 | expect(decodeResult.raw.flight_number).toBe('04WM'); 31 | expect(decodeResult.raw.altitude).toBe(34962); 32 | expect(decodeResult.raw.position.latitude).toBe(53.13); 33 | expect(decodeResult.raw.position.longitude).toBe(1.33); 34 | expect(decodeResult.raw.eta_time).toBe(39360); 35 | expect(decodeResult.formatted.items.length).toBe(4); 36 | expect(decodeResult.formatted.items[0].label).toBe('Flight Number'); 37 | expect(decodeResult.formatted.items[0].value).toBe('04WM'); 38 | expect(decodeResult.formatted.items[1].label).toBe('Altitude'); 39 | expect(decodeResult.formatted.items[1].value).toBe('34962 feet'); 40 | expect(decodeResult.formatted.items[2].label).toBe('Aircraft Position'); 41 | expect(decodeResult.formatted.items[2].value).toBe('53.130 N, 1.330 E'); 42 | expect(decodeResult.formatted.items[3].label).toBe('Estimated Time of Arrival'); 43 | expect(decodeResult.formatted.items[3].value).toBe('10:56:00'); 44 | 45 | expect(decodeResult.remaining.text).toBe('3374'); 46 | }); 47 | 48 | test('does not decode invalid', () => { 49 | 50 | const text = '/ Bogus message'; 51 | const decodeResult = plugin.decode({ text: text }); 52 | 53 | expect(decodeResult.decoded).toBe(false); 54 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 55 | expect(decodeResult.message.text).toBe(text); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /lib/plugins/Label_24_Slash.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | // Position Report 7 | export class Label_24_Slash extends DecoderPlugin { 8 | name = 'label-24-slash'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ['24'], 13 | preambles: ['/'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}) : DecodeResult { 18 | const decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'Position Report'; 21 | decodeResult.message = message; 22 | 23 | const fields = message.text.split('/'); 24 | 25 | if (fields.length == 10 && fields[0] == '' && fields[9] == '') { // begin and ends with `/` 26 | const mmddyy = fields[1].substring(4,6) + fields[1].substring(2,4) + fields[1].substring(0,2); // YYDDMM 27 | const hhmmss = fields[2] + '00'; 28 | decodeResult.raw.message_timestamp = DateTimeUtils.convertDateTimeToEpoch(hhmmss,mmddyy); 29 | ResultFormatter.flightNumber(decodeResult, fields[3]); 30 | ResultFormatter.altitude(decodeResult, Number(fields[4])); 31 | const lat = fields[5]; 32 | const lon = fields[6]; 33 | const position = { 34 | latitude: (lat[0] === 'N' ? 1 : -1) * Number(lat.substring(1)), 35 | longitude: (lon[0] === 'E' ? 1 : -1) * Number(lon.substring(1)), 36 | }; 37 | ResultFormatter.position(decodeResult, position); 38 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[8])); 39 | ResultFormatter.unknown(decodeResult, fields[7]); 40 | 41 | decodeResult.decoded = true; 42 | decodeResult.decoder.decodeLevel = 'partial'; 43 | } else { 44 | // Unknown! 45 | if(options.debug) { 46 | console.log(`DEBUG: ${this.name}: Unknown variation. Field count: ${fields.length}. Message: ${message.text}`); 47 | } 48 | decodeResult.decoded = false; 49 | decodeResult.decoder.decodeLevel = 'none'; 50 | } 51 | return decodeResult; 52 | } 53 | } 54 | 55 | export default {}; 56 | 57 | -------------------------------------------------------------------------------- /lib/plugins/Label_2P_FM3.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | export class Label_2P_FM3 extends DecoderPlugin { 8 | name = 'label-2p-fm3'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ["2P"], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | let decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'Flight Report'; 20 | decodeResult.message = message; 21 | 22 | const parts = message.text.split(','); 23 | 24 | if(parts.length === 7){ 25 | 26 | const header = parts[0].split('FM3 '); 27 | if(header.length == 0) { 28 | // can't use preambles, as there can be info before `FM4` 29 | // so let's check if we want to decode it here 30 | ResultFormatter.unknown(decodeResult, message.text); 31 | decodeResult.decoded = false; 32 | decodeResult.decoder.decodeLevel = 'none'; 33 | return decodeResult; 34 | } 35 | 36 | if(header[0].length > 0) { 37 | ResultFormatter.unknown(decodeResult, header[0].substring(0,4)); 38 | ResultFormatter.flightNumber(decodeResult, header[0].substring(4)); 39 | } 40 | console.log(header[1]); 41 | if(header[1].length === 4) { 42 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(header[1])); 43 | } else { 44 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(header[1])); 45 | } 46 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[1])); 47 | const lat = parts[2].replaceAll(' ',''); 48 | const lon = parts[3].replaceAll(' ',''); 49 | if(lat[0] === 'N' || lat[0] === 'S') { 50 | ResultFormatter.position(decodeResult, { 51 | latitude: CoordinateUtils.getDirection(lat[0]) * Number(lat.substring(1)), 52 | longitude: CoordinateUtils.getDirection(lon[0]) * Number(lon.substring(1)), 53 | }); 54 | } else { 55 | ResultFormatter.position(decodeResult, {latitude: Number(lat), longitude: Number(lon)}); 56 | } 57 | ResultFormatter.altitude(decodeResult, Number(parts[4])); 58 | // TODO: decode further 59 | ResultFormatter.unknown(decodeResult, parts[5]); 60 | ResultFormatter.unknown(decodeResult, parts[6]); 61 | 62 | decodeResult.decoded = true; 63 | decodeResult.decoder.decodeLevel = 'partial'; 64 | } else { 65 | // Unknown 66 | if (options.debug) { 67 | console.log(`Decoder: Unknown H1 message: ${message.text}`); 68 | } 69 | ResultFormatter.unknown(decodeResult, message.text); 70 | decodeResult.decoded = false; 71 | decodeResult.decoder.decodeLevel = 'none'; 72 | } 73 | 74 | return decodeResult; 75 | } 76 | } 77 | 78 | export default {}; 79 | -------------------------------------------------------------------------------- /lib/plugins/Label_2P_FM4.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_2P_FM4 extends DecoderPlugin { 7 | name = 'label-2p-fm4'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["2P"], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}) : DecodeResult { 16 | let decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.formatted.description = 'Flight Report'; 19 | decodeResult.message = message; 20 | 21 | const parts = message.text.split(','); 22 | 23 | if(parts.length === 10){ 24 | const header = parts[0].split('FM4'); 25 | if(header.length == 0) { 26 | // can't use preambles, as there can be info before `FM4` 27 | // so let's check if we want to decode it here 28 | ResultFormatter.unknown(decodeResult, message.text); 29 | decodeResult.decoded = false; 30 | decodeResult.decoder.decodeLevel = 'none'; 31 | return decodeResult; 32 | } 33 | if(header[0].length > 0) { 34 | ResultFormatter.unknown(decodeResult, header[0].substring(0,4)); 35 | ResultFormatter.flightNumber(decodeResult, header[0].substring(4)); 36 | } 37 | ResultFormatter.departureAirport(decodeResult, header[1]); 38 | ResultFormatter.arrivalAirport(decodeResult, parts[1]); 39 | ResultFormatter.day(decodeResult, Number(parts[2].substring(0,2))); 40 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[2].substring(2))); 41 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[3])); 42 | ResultFormatter.position(decodeResult, {latitude: Number(parts[4].replaceAll(' ','')), longitude: Number(parts[5].replaceAll(' ',''))}); 43 | ResultFormatter.altitude(decodeResult, Number(parts[6])); 44 | ResultFormatter.heading(decodeResult, Number(parts[7])); 45 | // TODO: decode further 46 | ResultFormatter.unknown(decodeResult, parts[8]); 47 | ResultFormatter.unknown(decodeResult, parts[9]); 48 | 49 | 50 | decodeResult.decoded = true; 51 | decodeResult.decoder.decodeLevel = 'partial'; 52 | } else { 53 | // Unknown 54 | if (options.debug) { 55 | console.log(`Decoder: Unknown H1 message: ${message.text}`); 56 | } 57 | ResultFormatter.unknown(decodeResult, message.text); 58 | decodeResult.decoded = false; 59 | decodeResult.decoder.decodeLevel = 'none'; 60 | } 61 | 62 | return decodeResult; 63 | } 64 | } 65 | 66 | export default {}; 67 | -------------------------------------------------------------------------------- /lib/plugins/Label_2P_FM5.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_2P_FM5 } from './Label_2P_FM5'; 3 | 4 | describe('Label_2P Preamble FM5', () => { 5 | 6 | let plugin: Label_2P_FM5; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_2P_FM5(decoder); 11 | }); 12 | 13 | test('variant 1', () => { 14 | // https://app.airframes.io/messages/4208768180 15 | const text = 'FM5 EIDW,OMAA,113522,1540,+45.147, +23.384,35002,116.24,502 ,36900,ETD23N ,'; 16 | const decodeResult = plugin.decode({ text: text }); 17 | 18 | expect(decodeResult.decoded).toBe(true); 19 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 20 | expect(decodeResult.formatted.description).toBe('Flight Report'); 21 | expect(decodeResult.message.text).toBe(text); 22 | expect(decodeResult.formatted.items.length).toBe(7); 23 | expect(decodeResult.formatted.items[0].label).toBe('Origin'); 24 | expect(decodeResult.formatted.items[0].value).toBe('EIDW'); 25 | expect(decodeResult.formatted.items[1].label).toBe('Destination'); 26 | expect(decodeResult.formatted.items[1].value).toBe('OMAA'); 27 | expect(decodeResult.formatted.items[2].label).toBe('Message Timestamp'); 28 | expect(decodeResult.formatted.items[2].value).toBe('11:35:22'); 29 | expect(decodeResult.formatted.items[3].label).toBe('Estimated Time of Arrival'); 30 | expect(decodeResult.formatted.items[3].value).toBe('15:40:00'); 31 | expect(decodeResult.formatted.items[4].label).toBe('Aircraft Position'); 32 | expect(decodeResult.formatted.items[4].value).toBe('45.147 N, 23.384 E'); 33 | expect(decodeResult.formatted.items[5].label).toBe('Altitude'); 34 | expect(decodeResult.formatted.items[5].value).toBe('35002 feet'); 35 | expect(decodeResult.formatted.items[6].label).toBe('Flight Number'); 36 | expect(decodeResult.formatted.items[6].value).toBe('ETD23N'); 37 | expect(decodeResult.remaining.text).toBe('116.24,502 ,36900,'); 38 | }); 39 | 40 | test('', () => { 41 | 42 | const text = 'FM4 Bogus message'; 43 | const decodeResult = plugin.decode({ text: text }); 44 | 45 | expect(decodeResult.decoded).toBe(false); 46 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 47 | expect(decodeResult.formatted.description).toBe('Flight Report'); 48 | expect(decodeResult.formatted.items.length).toBe(0); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /lib/plugins/Label_2P_FM5.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_2P_FM5 extends DecoderPlugin { 7 | name = 'label-2p-fm5'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["2P"], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}) : DecodeResult { 16 | let decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.formatted.description = 'Flight Report'; 19 | decodeResult.message = message; 20 | 21 | const parts = message.text.split(','); 22 | 23 | if(parts.length === 12){ 24 | 25 | const header = parts[0].split('FM5 '); 26 | if(header.length == 0) { 27 | // can't use preambles, as there can be info before `FM4` 28 | // so let's check if we want to decode it here 29 | ResultFormatter.unknown(decodeResult, message.text); 30 | decodeResult.decoded = false; 31 | decodeResult.decoder.decodeLevel = 'none'; 32 | return decodeResult; 33 | } 34 | 35 | ResultFormatter.departureAirport(decodeResult, header[1]); 36 | ResultFormatter.arrivalAirport(decodeResult, parts[1]); 37 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[2])); 38 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(parts[3])); 39 | ResultFormatter.position(decodeResult, {latitude: Number(parts[4].replaceAll(' ','')), longitude: Number(parts[5].replaceAll(' ',''))}); 40 | ResultFormatter.altitude(decodeResult, Number(parts[6])); 41 | // TODO: decode further 42 | ResultFormatter.unknown(decodeResult, parts[7]); 43 | ResultFormatter.unknown(decodeResult, parts[8]); 44 | ResultFormatter.unknown(decodeResult, parts[9]); 45 | ResultFormatter.flightNumber(decodeResult, parts[10].trim()); 46 | ResultFormatter.unknown(decodeResult, parts[11]); 47 | 48 | decodeResult.decoded = true; 49 | decodeResult.decoder.decodeLevel = 'partial'; 50 | } else { 51 | // Unknown 52 | if (options.debug) { 53 | console.log(`Decoder: Unknown H1 message: ${message.text}`); 54 | } 55 | ResultFormatter.unknown(decodeResult, message.text); 56 | decodeResult.decoded = false; 57 | decodeResult.decoder.decodeLevel = 'none'; 58 | } 59 | 60 | return decodeResult; 61 | } 62 | } 63 | 64 | export default {}; 65 | -------------------------------------------------------------------------------- /lib/plugins/Label_2P_POS.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_2P_POS } from './Label_2P_POS'; 3 | 4 | describe('Label_2P Preamble POS', () => { 5 | 6 | let plugin: Label_2P_POS; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_2P_POS(decoder); 11 | }); 12 | 13 | test('variant 1', () => { 14 | // https://app.airframes.io/messages/4179262958 15 | const text = 'M80AMC4086POS/ID50007B,RCH4086,ABB02R70E037/DC10022025,051804/MR103,/ET090738/PSN56012W013273,051804,350,,,,,084081,/CG,,/FB0857/VR0322B89'; 16 | const decodeResult = plugin.decode({ text: text }); 17 | 18 | expect(decodeResult.decoded).toBe(true); 19 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 20 | expect(decodeResult.formatted.description).toBe('Position Report'); 21 | expect(decodeResult.message.text).toBe(text); 22 | expect(decodeResult.formatted.items.length).toBe(9); 23 | expect(decodeResult.formatted.items[0].label).toBe('Flight Number'); 24 | expect(decodeResult.formatted.items[0].value).toBe('MC4086'); 25 | expect(decodeResult.formatted.items[1].label).toBe('Tail'); 26 | expect(decodeResult.formatted.items[1].value).toBe('50007B'); 27 | expect(decodeResult.formatted.items[2].label).toBe('Flight Number'); 28 | expect(decodeResult.formatted.items[2].value).toBe('RCH4086'); 29 | expect(decodeResult.formatted.items[3].label).toBe('Day of Month'); 30 | expect(decodeResult.formatted.items[3].value).toBe('9'); 31 | expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival'); 32 | expect(decodeResult.formatted.items[4].value).toBe('07:38:00'); 33 | expect(decodeResult.formatted.items[5].label).toBe('Aircraft Position'); 34 | expect(decodeResult.formatted.items[5].value).toBe('56.020 N, 13.455 W'); 35 | expect(decodeResult.formatted.items[6].label).toBe('Aircraft Route'); 36 | expect(decodeResult.formatted.items[6].value).toBe('@05:18:04 >> ?'); // Yuck - maybe fix? 37 | expect(decodeResult.formatted.items[7].label).toBe('Altitude'); 38 | expect(decodeResult.formatted.items[7].value).toBe('35000 feet'); 39 | expect(decodeResult.formatted.items[8].label).toBe('Message Checksum'); 40 | expect(decodeResult.formatted.items[8].value).toBe('0x2b89'); 41 | expect(decodeResult.remaining.text).toBe('M80A/MR103,,084081,/CG,,/FB0857/VR032'); 42 | }); 43 | 44 | test('', () => { 45 | 46 | const text = 'M01AFN1234POS Bogus message'; 47 | const decodeResult = plugin.decode({ text: text }); 48 | 49 | expect(decodeResult.decoded).toBe(false); 50 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 51 | expect(decodeResult.formatted.description).toBe('Unknown H1 Message'); 52 | expect(decodeResult.formatted.items.length).toBe(0); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /lib/plugins/Label_2P_POS.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { H1Helper } from '../utils/h1_helper'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_2P_POS extends DecoderPlugin { 7 | name = 'label-2p-pos'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["2P"], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}) : DecodeResult { 16 | let decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.message = message; 19 | 20 | const msg = message.text.replace(/\n|\r/g, ""); 21 | const decoded = H1Helper.decodeH1Message(decodeResult, msg); 22 | decodeResult.decoded = decoded; 23 | 24 | decodeResult.decoder.decodeLevel = !decodeResult.remaining.text ? 'full' : 'partial'; 25 | if (decodeResult.formatted.items.length === 0) { 26 | if (options.debug) { 27 | console.log(`Decoder: Unknown H1 message: ${message.text}`); 28 | } 29 | ResultFormatter.unknown(decodeResult, message.text); 30 | decodeResult.decoded = false; 31 | decodeResult.decoder.decodeLevel = 'none'; 32 | } 33 | return decodeResult; 34 | } 35 | } 36 | 37 | export default {}; 38 | -------------------------------------------------------------------------------- /lib/plugins/Label_30_Slash_EA.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_30_Slash_EA } from './Label_30_Slash_EA'; 3 | 4 | test('decodes Label 30 sample 1', () => { 5 | const decoder = new MessageDecoder(); 6 | const decoderPlugin = new Label_30_Slash_EA(decoder); 7 | 8 | expect(decoderPlugin.decode).toBeDefined(); 9 | expect(decoderPlugin.name).toBe('label-30-slash-ea'); 10 | expect(decoderPlugin.qualifiers).toBeDefined(); 11 | expect(decoderPlugin.qualifiers()).toEqual({ 12 | labels: ['30'], 13 | preambles: ['/EA'], 14 | }); 15 | 16 | const text = '/EA1719/DSKSFO/SK23'; 17 | const decodeResult = decoderPlugin.decode({ text: text }); 18 | 19 | expect(decodeResult.decoded).toBe(true); 20 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 21 | expect(decodeResult.decoder.name).toBe('label-30-slash-ea'); 22 | expect(decodeResult.formatted.description).toBe('ETA Report'); 23 | expect(decodeResult.message.text).toBe(text); 24 | expect(decodeResult.raw.arrival_icao).toBe('KSFO'); 25 | expect(decodeResult.formatted.items.length).toBe(2); 26 | expect(decodeResult.formatted.items[0].type).toBe('time_of_day'); 27 | expect(decodeResult.formatted.items[0].code).toBe('ETA'); 28 | expect(decodeResult.formatted.items[0].label).toBe('Estimated Time of Arrival'); 29 | expect(decodeResult.formatted.items[0].value).toBe('17:19:00'); 30 | expect(decodeResult.formatted.items[1].type).toBe('icao'); 31 | expect(decodeResult.formatted.items[1].code).toBe('DST'); 32 | expect(decodeResult.formatted.items[1].label).toBe('Destination'); 33 | expect(decodeResult.formatted.items[1].value).toBe('KSFO'); 34 | }); 35 | -------------------------------------------------------------------------------- /lib/plugins/Label_30_Slash_EA.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_30_Slash_EA extends DecoderPlugin { 7 | name = 'label-30-slash-ea'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["30"], 12 | preambles: ['/EA'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'ETA Report'; 20 | decodeResult.message = message; 21 | 22 | // Style: /EA1830/DSKSFO/SK24 23 | const results = message.text.split(/\n|\//).slice(1); // Split by / and new line 24 | 25 | if (results) { 26 | if (options.debug) { 27 | console.log(`Label 30 EA: results`); 28 | console.log(results); 29 | } 30 | } 31 | 32 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(results[0].substr(2, 4))); 33 | 34 | if (results[1].substring(0,2) === "DS") { 35 | ResultFormatter.arrivalAirport(decodeResult, results[1].substring(2, 6)); 36 | ResultFormatter.unknown(decodeResult, "/".concat(results[2])); 37 | } else { 38 | ResultFormatter.unknown(decodeResult, "/".concat(results[1], "/", results[2])); 39 | } 40 | 41 | decodeResult.decoded = true; 42 | decodeResult.decoder.decodeLevel = 'partial'; 43 | 44 | return decodeResult; 45 | } 46 | } 47 | 48 | export default {}; 49 | -------------------------------------------------------------------------------- /lib/plugins/Label_44_ETA.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_44_ETA } from './Label_44_ETA'; 3 | 4 | describe('Label 44 IN', () => { 5 | let plugin: Label_44_ETA; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_44_ETA(decoder); 10 | }); 11 | 12 | test('matches qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-44-eta'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['44'], 18 | preambles: ['00ETA01', '00ETA02', '00ETA03', 'ETA01', 'ETA02', 'ETA03'], 19 | }); 20 | }); 21 | 22 | test('decodes variant 1', () => { 23 | // https://app.airframes.io/messages/3569460297 24 | const text = '00ETA03,N38241W081357,330,KBNA,KBWI,1107,0123,0208,008.1'; 25 | const decodeResult = plugin.decode({ text: text }); 26 | expect(decodeResult.decoded).toBe(true); 27 | expect(decodeResult.decoder.decodeLevel).toBe('full'); 28 | expect(decodeResult.raw.position.latitude).toBe(38.401666666666664); 29 | expect(decodeResult.raw.position.longitude).toBe(-81.595); 30 | expect(decodeResult.raw.altitude).toBe(33000); 31 | expect(decodeResult.raw.departure_icao).toBe('KBNA'); 32 | expect(decodeResult.raw.arrival_icao).toBe('KBWI'); 33 | expect(decodeResult.raw.month).toBe(11); 34 | expect(decodeResult.raw.day).toBe(7); 35 | expect(decodeResult.raw.time_of_day).toBe(4980); 36 | expect(decodeResult.raw.eta_time).toBe(7680); 37 | expect(decodeResult.formatted.items.length).toBe(9); 38 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 39 | expect(decodeResult.formatted.items[0].value).toBe('38.402 N, 81.595 W'); 40 | expect(decodeResult.formatted.items[1].label).toBe('Altitude'); 41 | expect(decodeResult.formatted.items[1].value).toBe('33000 feet'); 42 | expect(decodeResult.formatted.items[2].label).toBe('Origin'); 43 | expect(decodeResult.formatted.items[2].value).toBe('KBNA'); 44 | expect(decodeResult.formatted.items[3].label).toBe('Destination'); 45 | expect(decodeResult.formatted.items[3].value).toBe('KBWI'); 46 | expect(decodeResult.formatted.items[4].label).toBe('Month of Year'); 47 | expect(decodeResult.formatted.items[4].value).toBe('11'); 48 | expect(decodeResult.formatted.items[5].label).toBe('Day of Month'); 49 | expect(decodeResult.formatted.items[5].value).toBe('7'); 50 | expect(decodeResult.formatted.items[6].label).toBe('Message Timestamp'); 51 | expect(decodeResult.formatted.items[6].value).toBe('01:23:00'); 52 | expect(decodeResult.formatted.items[7].label).toBe('Estimated Time of Arrival'); 53 | expect(decodeResult.formatted.items[7].value).toBe('02:08:00'); 54 | expect(decodeResult.formatted.items[8].label).toBe('Fuel Remaining'); 55 | expect(decodeResult.formatted.items[8].value).toBe('8.1'); 56 | }); 57 | 58 | test('does not decode invalid', () => { 59 | 60 | const text = '00OFF01 Bogus message'; 61 | const decodeResult = plugin.decode({ text: text }); 62 | 63 | expect(decodeResult.decoded).toBe(false); 64 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 65 | expect(decodeResult.message.text).toBe(text); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /lib/plugins/Label_44_ETA.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // In Air Report 8 | export class Label_44_ETA extends DecoderPlugin { 9 | name = 'label-44-eta'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['44'], 14 | preambles: ['00ETA01', '00ETA02', '00ETA03', 'ETA01', 'ETA02', 'ETA03'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}): DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'ETA Report'; 22 | decodeResult.message = message; 23 | 24 | const data = message.text.split(','); 25 | if (data.length >= 9) { 26 | if (options.debug) { 27 | console.log(`Label 44 ETA Report: groups`); 28 | console.log(data); 29 | } 30 | 31 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinatesDecimalMinutes(data[1])); 32 | ResultFormatter.altitude(decodeResult, 100 * Number(data[2])); 33 | ResultFormatter.departureAirport(decodeResult, data[3]); 34 | ResultFormatter.arrivalAirport(decodeResult, data[4]); 35 | 36 | ResultFormatter.month(decodeResult, Number(data[5].substring(0, 2))); 37 | ResultFormatter.day(decodeResult, Number(data[5].substring(2, 4))); 38 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[6])); 39 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[7])); 40 | const fuel = Number(data[8]); 41 | if (!isNaN(fuel)) { 42 | ResultFormatter.remainingFuel(decodeResult, Number(fuel)); 43 | } 44 | 45 | if (data.length > 9) { 46 | ResultFormatter.unknownArr(decodeResult, data.slice(9)); 47 | } 48 | 49 | } else { 50 | if (options.debug) { 51 | console.log(`Decoder: Unknown 44 message: ${message.text}`); 52 | } 53 | ResultFormatter.unknown(decodeResult, message.text); 54 | decodeResult.decoded = false; 55 | decodeResult.decoder.decodeLevel = 'none'; 56 | return decodeResult; 57 | } 58 | 59 | decodeResult.decoded = true; 60 | decodeResult.decoder.decodeLevel = 'full'; 61 | 62 | return decodeResult; 63 | } 64 | } 65 | 66 | export default {}; 67 | -------------------------------------------------------------------------------- /lib/plugins/Label_44_IN.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // In Air Report 8 | export class Label_44_IN extends DecoderPlugin { 9 | name = 'label-44-in'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['44'], 14 | preambles: ['00IN01', '00IN02', '00IN03', 'IN01', 'IN02', 'IN03'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}): DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'In Air Report'; 22 | decodeResult.message = message; 23 | 24 | const data = message.text.split(','); 25 | if (data.length >= 7) { 26 | if (options.debug) { 27 | console.log(`Label 44 In Air Report: groups`); 28 | console.log(data); 29 | } 30 | 31 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinatesDecimalMinutes(data[1])); 32 | ResultFormatter.departureAirport(decodeResult, data[2]); 33 | ResultFormatter.arrivalAirport(decodeResult, data[3]); 34 | ResultFormatter.month(decodeResult, Number(data[4].substring(0, 2))); 35 | ResultFormatter.day(decodeResult, Number(data[4].substring(2, 4))); 36 | ResultFormatter.in(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[5])); 37 | const fuel = Number(data[6]); 38 | if (!isNaN(fuel)) { 39 | ResultFormatter.remainingFuel(decodeResult, Number(fuel)); 40 | } 41 | 42 | if (data.length > 7) { 43 | ResultFormatter.unknownArr(decodeResult, data.slice(7)); 44 | } 45 | 46 | } else { 47 | if (options.debug) { 48 | console.log(`Decoder: Unknown 44 message: ${message.text}`); 49 | } 50 | ResultFormatter.unknown(decodeResult, message.text); 51 | decodeResult.decoded = false; 52 | decodeResult.decoder.decodeLevel = 'none'; 53 | return decodeResult; 54 | } 55 | 56 | decodeResult.decoded = true; 57 | decodeResult.decoder.decodeLevel = 'full'; 58 | 59 | return decodeResult; 60 | } 61 | } 62 | 63 | export default {}; 64 | -------------------------------------------------------------------------------- /lib/plugins/Label_44_OFF.test.ts: -------------------------------------------------------------------------------- 1 | import { decode } from 'punycode'; 2 | import { MessageDecoder } from '../MessageDecoder'; 3 | import { Label_44_OFF } from './Label_44_OFF'; 4 | 5 | describe('Label 44 OFF', () => { 6 | let plugin: Label_44_OFF; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_44_OFF(decoder); 11 | }); 12 | 13 | test('matches qualifiers', () => { 14 | expect(plugin.decode).toBeDefined(); 15 | expect(plugin.name).toBe('label-44-off'); 16 | expect(plugin.qualifiers).toBeDefined(); 17 | expect(plugin.qualifiers()).toEqual({ 18 | labels: ['44'], 19 | preambles: ['00OFF01', '00OFF02', '00OFF03', 'OFF01', 'OFF02', 'OFF03'], 20 | }); 21 | }); 22 | 23 | test('decodes variant 1', () => { 24 | const text = 'OFF02,N39247W077226,KFDK,KSNA,1106,2124,0248,011.1' 25 | const decodeResult = plugin.decode({ text: text }); 26 | expect(decodeResult.decoded).toBe(true); 27 | expect(decodeResult.decoder.decodeLevel).toBe('full'); 28 | expect(decodeResult.raw.position.latitude).toBe(39.41166666666667); 29 | expect(decodeResult.raw.position.longitude).toBe(-77.37666666666667); 30 | expect(decodeResult.raw.departure_icao).toBe('KFDK'); 31 | expect(decodeResult.raw.arrival_icao).toBe('KSNA'); 32 | expect(decodeResult.raw.month).toBe(11); 33 | expect(decodeResult.raw.day).toBe(6); 34 | expect(decodeResult.raw.off_time).toBe(77040); 35 | expect(decodeResult.raw.eta_time).toBe(10080); 36 | expect(decodeResult.raw.fuel_remaining).toBe(11.1); 37 | expect(decodeResult.formatted.items.length).toBe(8); 38 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 39 | expect(decodeResult.formatted.items[0].value).toBe('39.412 N, 77.377 W'); 40 | expect(decodeResult.formatted.items[1].label).toBe('Origin'); 41 | expect(decodeResult.formatted.items[1].value).toBe('KFDK'); 42 | expect(decodeResult.formatted.items[2].label).toBe('Destination'); 43 | expect(decodeResult.formatted.items[2].value).toBe('KSNA'); 44 | expect(decodeResult.formatted.items[3].label).toBe('Month of Year'); 45 | expect(decodeResult.formatted.items[3].value).toBe('11'); 46 | expect(decodeResult.formatted.items[4].label).toBe('Day of Month'); 47 | expect(decodeResult.formatted.items[4].value).toBe('6'); 48 | expect(decodeResult.formatted.items[5].label).toBe('Takeoff Time'); 49 | expect(decodeResult.formatted.items[5].value).toBe('21:24:00'); 50 | expect(decodeResult.formatted.items[6].label).toBe('Estimated Time of Arrival'); 51 | expect(decodeResult.formatted.items[6].value).toBe('02:48:00'); 52 | expect(decodeResult.formatted.items[7].label).toBe('Fuel Remaining'); 53 | expect(decodeResult.formatted.items[7].value).toBe('11.1'); 54 | }); 55 | 56 | test('does not decode invalid', () => { 57 | 58 | const text = '00OFF01 Bogus message'; 59 | const decodeResult = plugin.decode({ text: text }); 60 | 61 | expect(decodeResult.decoded).toBe(false); 62 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 63 | expect(decodeResult.message.text).toBe(text); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /lib/plugins/Label_44_OFF.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // Off Runway Report 8 | export class Label_44_OFF extends DecoderPlugin { 9 | name = 'label-44-off'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['44'], 14 | preambles: ['00OFF01', '00OFF02', '00OFF03', 'OFF01', 'OFF02', 'OFF03'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}): DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'Off Runway Report'; 22 | decodeResult.message = message; 23 | 24 | 25 | const data = message.text.split(','); 26 | if (data.length >= 8) { 27 | if (options.debug) { 28 | console.log(`Label 44 Off Runway Report: groups`); 29 | console.log(data); 30 | } 31 | 32 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinatesDecimalMinutes(data[1])); 33 | ResultFormatter.departureAirport(decodeResult, data[2]); 34 | ResultFormatter.arrivalAirport(decodeResult, data[3]); 35 | ResultFormatter.month(decodeResult, Number(data[4].substring(0, 2))); 36 | ResultFormatter.day(decodeResult, Number(data[4].substring(2, 4))); 37 | ResultFormatter.off(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[5])); 38 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[6])); 39 | const fuel = Number(data[7]); 40 | if (!isNaN(fuel)) { 41 | ResultFormatter.remainingFuel(decodeResult, Number(fuel)); 42 | } 43 | 44 | if (data.length > 8) { 45 | ResultFormatter.unknownArr(decodeResult, data.slice(8)); 46 | } 47 | 48 | } else { 49 | if (options.debug) { 50 | console.log(`Decoder: Unknown 44 message: ${message.text}`); 51 | } 52 | ResultFormatter.unknown(decodeResult, message.text); 53 | decodeResult.decoded = false; 54 | decodeResult.decoder.decodeLevel = 'none'; 55 | return decodeResult; 56 | } 57 | 58 | decodeResult.decoded = true; 59 | decodeResult.decoder.decodeLevel = 'full'; 60 | 61 | return decodeResult; 62 | } 63 | } 64 | 65 | export default {}; 66 | -------------------------------------------------------------------------------- /lib/plugins/Label_44_ON.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // On Runway Report 8 | export class Label_44_ON extends DecoderPlugin { 9 | name = 'label-44-on'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['44'], 14 | preambles: ['00ON01', '00ON02', '00ON03', 'ON01', 'ON02', 'ON03'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}): DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'On Runway Report'; 22 | decodeResult.message = message; 23 | 24 | const data = message.text.split(','); 25 | if (data.length >= 7) { 26 | if (options.debug) { 27 | console.log(`Label 44 On Runway Report: groups`); 28 | console.log(data); 29 | } 30 | 31 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinatesDecimalMinutes(data[1])); 32 | ResultFormatter.departureAirport(decodeResult, data[2]); 33 | ResultFormatter.arrivalAirport(decodeResult, data[3]); 34 | ResultFormatter.month(decodeResult, Number(data[4].substring(0, 2))); 35 | ResultFormatter.day(decodeResult, Number(data[4].substring(2, 4))); 36 | ResultFormatter.on(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[5])); 37 | const fuel = Number(data[6]); 38 | if (!isNaN(fuel)) { 39 | ResultFormatter.remainingFuel(decodeResult, Number(fuel)); 40 | } 41 | 42 | if (data.length > 7) { 43 | ResultFormatter.unknownArr(decodeResult, data.slice(7)); 44 | } 45 | 46 | } else { 47 | if (options.debug) { 48 | console.log(`Decoder: Unknown 44 message: ${message.text}`); 49 | } 50 | ResultFormatter.unknown(decodeResult, message.text); 51 | decodeResult.decoded = false; 52 | decodeResult.decoder.decodeLevel = 'none'; 53 | return decodeResult; 54 | } 55 | 56 | decodeResult.decoded = true; 57 | decodeResult.decoder.decodeLevel = 'full'; 58 | 59 | return decodeResult; 60 | } 61 | } 62 | 63 | export default {}; 64 | -------------------------------------------------------------------------------- /lib/plugins/Label_44_POS.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // General Aviation Position Report 8 | export class Label_44_POS extends DecoderPlugin { 9 | name = 'label-44-pos'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['44'], 14 | preambles: ['00POS01', '00POS02', '00POS03', 'POS01', 'POS02', 'POS03'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}) : DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'Position Report'; 22 | decodeResult.message = message; 23 | 24 | // Style: POS02,N38338W121179,GRD,KMHR,KPDX,0807,0003,0112,005.1 25 | // Match: POS02,coords,flight_level_or_ground,departure_icao,arrival_icao,current_date,current_time,eta_time,unknown 26 | const regex = /^.*,(?.*),(?.*),(?.*),(?.*),(?.*),(?.*),(?.*),(?.*)$/; 27 | const results = message.text.match(regex); 28 | if (results?.groups) { 29 | if (options.debug) { 30 | console.log(`Label 44 Position Report: groups`); 31 | console.log(results.groups); 32 | } 33 | 34 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinatesDecimalMinutes(results.groups.unsplit_coords)); 35 | const flight_level = results.groups.flight_level_or_ground == 'GRD' || results.groups.flight_level_or_ground == '***' ? 0 : Number(results.groups.flight_level_or_ground); 36 | 37 | 38 | ResultFormatter.month(decodeResult, Number(results.groups.current_date.substring(0, 2))); 39 | ResultFormatter.day(decodeResult, Number(results.groups.current_date.substring(2, 4))); 40 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(results.groups.current_time + '00')); 41 | 42 | // TODO: ETA month and Day 43 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(results.groups.eta_time + '00')); 44 | 45 | if (results.groups.fuel_in_tons != '***' && results.groups.fuel_in_tons != '****') { 46 | decodeResult.raw.fuel_in_tons = Number(results.groups.fuel_in_tons); 47 | } 48 | 49 | ResultFormatter.departureAirport(decodeResult, results.groups.departure_icao); 50 | ResultFormatter.arrivalAirport(decodeResult, results.groups.arrival_icao); 51 | ResultFormatter.altitude(decodeResult, flight_level * 100); 52 | } 53 | 54 | decodeResult.decoded = true; 55 | decodeResult.decoder.decodeLevel = 'full'; 56 | 57 | return decodeResult; 58 | } 59 | } 60 | 61 | export default {}; 62 | -------------------------------------------------------------------------------- /lib/plugins/Label_4A_01.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_4A_01 } from './Label_4A_01'; 3 | 4 | test('matches Label 4A_01 qualifiers', () => { 5 | const decoder = new MessageDecoder(); 6 | const decoderPlugin = new Label_4A_01(decoder); 7 | 8 | expect(decoderPlugin.decode).toBeDefined(); 9 | expect(decoderPlugin.name).toBe('label-4a-01'); 10 | expect(decoderPlugin.qualifiers).toBeDefined(); 11 | expect(decoderPlugin.qualifiers()).toEqual({ 12 | labels: ['4A'], 13 | preambles: ['01'], 14 | }); 15 | }); 16 | 17 | test('decodes Label 4A_01', () => { 18 | const decoder = new MessageDecoder(); 19 | const decoderPlugin = new Label_4A_01(decoder); 20 | 21 | // https://app.airframes.io/messages/3450562911 22 | const text = '01DCAP VIR41R/190203EGLLKSFO\r\n+ 1418158.0+ 24.8'; 23 | const decodeResult = decoderPlugin.decode({ text: text }); 24 | 25 | expect(decodeResult.decoded).toBe(true); 26 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 27 | expect(decodeResult.decoder.name).toBe('label-4a-01'); 28 | expect(decodeResult.formatted.description).toBe('Latest New Format'); 29 | expect(decodeResult.message.text).toBe(text); 30 | expect(decodeResult.remaining.text).toBe('158.0'); 31 | expect(decodeResult.formatted.items.length).toBe(7); 32 | expect(decodeResult.formatted.items[0].code).toBe('STATE_CHANGE'); 33 | expect(decodeResult.formatted.items[0].value).toBe('Descent -> Approach'); 34 | expect(decodeResult.formatted.items[1].code).toBe('CALLSIGN'); 35 | expect(decodeResult.formatted.items[1].value).toBe('VIR41R'); 36 | expect(decodeResult.formatted.items[2].code).toBe('MSG_TOD'); 37 | expect(decodeResult.formatted.items[2].value).toBe('19:02:03'); 38 | expect(decodeResult.formatted.items[3].code).toBe('ORG'); 39 | expect(decodeResult.formatted.items[3].value).toBe('EGLL'); 40 | expect(decodeResult.formatted.items[4].code).toBe('DST'); 41 | expect(decodeResult.formatted.items[4].value).toBe('KSFO'); 42 | expect(decodeResult.formatted.items[5].code).toBe('ALT'); 43 | expect(decodeResult.formatted.items[5].value).toBe("1418 feet"); 44 | expect(decodeResult.formatted.items[6].code).toBe('OATEMP'); 45 | expect(decodeResult.formatted.items[6].value).toBe('24.8 degrees'); 46 | }); 47 | 48 | // disabled because all messages should decode 49 | xtest('decodes Label 4A_01 ', () => { 50 | const decoder = new MessageDecoder(); 51 | const decoderPlugin = new Label_4A_01(decoder); 52 | 53 | const text = '4A_01 Bogus message'; 54 | const decodeResult = decoderPlugin.decode({ text: text }); 55 | 56 | expect(decodeResult.decoded).toBe(false); 57 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 58 | expect(decodeResult.decoder.name).toBe('label-4a-01'); 59 | expect(decodeResult.formatted.description).toBe('Latest New Format'); 60 | expect(decodeResult.formatted.items.length).toBe(0); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/plugins/Label_4A_01.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { DateTimeUtils } from '../DateTimeUtils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_4A_01 extends DecoderPlugin { 7 | name = 'label-4a-01'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['4A'], 12 | preambles: ['01'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.message = message; 20 | decodeResult.formatted.description = 'Latest New Format'; 21 | 22 | decodeResult.decoded = true; 23 | let rgx = message.text.match(/^01([A-Z]{2})([A-Z]{2})\s*(\w+)\/(\d{6})([A-Z]{4})([A-Z]{4})\r\n([+-]\s*\d+)(\d{3}\.\d)([+-]\s*\d+\.\d)/); 24 | if (rgx) { 25 | ResultFormatter.state_change(decodeResult, rgx[1], rgx[2]); 26 | ResultFormatter.callsign(decodeResult, rgx[3]); 27 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(rgx[4] + "00")); 28 | ResultFormatter.departureAirport(decodeResult, rgx[5]); 29 | ResultFormatter.arrivalAirport(decodeResult, rgx[6]); 30 | ResultFormatter.altitude(decodeResult, Number(rgx[7].replace(/ /g, ""))); 31 | ResultFormatter.unknown(decodeResult, rgx[8]); 32 | ResultFormatter.temperature(decodeResult, rgx[9].replace(/ /g, "")); 33 | } else { 34 | decodeResult.decoded = false; 35 | ResultFormatter.unknown(decodeResult, message.text); 36 | } 37 | 38 | if (decodeResult.decoded) { 39 | if(!decodeResult.remaining.text) 40 | decodeResult.decoder.decodeLevel = 'full'; 41 | else 42 | decodeResult.decoder.decodeLevel = 'partial'; 43 | } else { 44 | decodeResult.decoder.decodeLevel = 'none'; 45 | } 46 | 47 | return decodeResult; 48 | } 49 | } 50 | 51 | export default {}; 52 | -------------------------------------------------------------------------------- /lib/plugins/Label_4A_DIS.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_4A_DIS } from './Label_4A_DIS'; 3 | 4 | test('matches Label 4A_DIS qualifiers', () => { 5 | const decoder = new MessageDecoder(); 6 | const decoderPlugin = new Label_4A_DIS(decoder); 7 | 8 | expect(decoderPlugin.decode).toBeDefined(); 9 | expect(decoderPlugin.name).toBe('label-4a-dis'); 10 | expect(decoderPlugin.qualifiers).toBeDefined(); 11 | expect(decoderPlugin.qualifiers()).toEqual({ 12 | labels: ['4A'], 13 | preambles: ['DIS'], 14 | }); 15 | }); 16 | 17 | test('decodes Label 4A_DIS', () => { 18 | const decoder = new MessageDecoder(); 19 | const decoderPlugin = new Label_4A_DIS(decoder); 20 | 21 | // https://app.airframes.io/messages/3450166197 22 | const text = 'DIS01,190009,WEN3140,@HOLD CNX'; 23 | const decodeResult = decoderPlugin.decode({ text: text }); 24 | 25 | expect(decodeResult.decoded).toBe(true); 26 | expect(decodeResult.decoder.decodeLevel).toBe('full'); 27 | expect(decodeResult.decoder.name).toBe('label-4a-dis'); 28 | expect(decodeResult.formatted.description).toBe('Latest New Format'); 29 | expect(decodeResult.message.text).toBe(text); 30 | expect(decodeResult.remaining.text).toBe(undefined); 31 | expect(decodeResult.formatted.items.length).toBe(3); 32 | expect(decodeResult.formatted.items[0].code).toBe('MSG_TOD'); 33 | expect(decodeResult.formatted.items[0].value).toBe('00:09:00'); 34 | expect(decodeResult.formatted.items[1].code).toBe('CALLSIGN'); 35 | expect(decodeResult.formatted.items[1].value).toBe('WEN3140'); 36 | expect(decodeResult.formatted.items[2].code).toBe('FREE_TEXT'); 37 | expect(decodeResult.formatted.items[2].value).toBe('@HOLD CNX'); 38 | }); 39 | 40 | // disabled because all messages should decode 41 | xtest('decodes Label 4A_DIS ', () => { 42 | const decoder = new MessageDecoder(); 43 | const decoderPlugin = new Label_4A_DIS(decoder); 44 | 45 | const text = '4A_DIS Bogus message'; 46 | const decodeResult = decoderPlugin.decode({ text: text }); 47 | 48 | expect(decodeResult.decoded).toBe(false); 49 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 50 | expect(decodeResult.decoder.name).toBe('label-4a-dis'); 51 | expect(decodeResult.formatted.description).toBe('Latest New Format'); 52 | expect(decodeResult.formatted.items.length).toBe(0); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/plugins/Label_4A_DIS.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { DateTimeUtils } from '../DateTimeUtils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_4A_DIS extends DecoderPlugin { 7 | name = 'label-4a-dis'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['4A'], 12 | preambles: ['DIS'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.message = message; 20 | decodeResult.formatted.description = 'Latest New Format'; 21 | 22 | decodeResult.decoded = true; 23 | const fields = message.text.split(","); 24 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[1].substring(2) + "00")); 25 | ResultFormatter.callsign(decodeResult, fields[2]); 26 | ResultFormatter.freetext(decodeResult, fields.slice(3).join("")); 27 | 28 | if (decodeResult.decoded) { 29 | if(!decodeResult.remaining.text) 30 | decodeResult.decoder.decodeLevel = 'full'; 31 | else 32 | decodeResult.decoder.decodeLevel = 'partial'; 33 | } else { 34 | decodeResult.decoder.decodeLevel = 'none'; 35 | } 36 | 37 | return decodeResult; 38 | } 39 | } 40 | 41 | export default {}; 42 | -------------------------------------------------------------------------------- /lib/plugins/Label_4A_DOOR.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_4A_DOOR } from './Label_4A_DOOR'; 3 | 4 | test('matches Label 4A_DOOR qualifiers', () => { 5 | const decoder = new MessageDecoder(); 6 | const decoderPlugin = new Label_4A_DOOR(decoder); 7 | 8 | expect(decoderPlugin.decode).toBeDefined(); 9 | expect(decoderPlugin.name).toBe('label-4a-door'); 10 | expect(decoderPlugin.qualifiers).toBeDefined(); 11 | expect(decoderPlugin.qualifiers()).toEqual({ 12 | labels: ['4A'], 13 | preambles: ['DOOR'], 14 | }); 15 | }); 16 | 17 | test('decodes Label 4A_DOOR', () => { 18 | const decoder = new MessageDecoder(); 19 | const decoderPlugin = new Label_4A_DOOR(decoder); 20 | 21 | // https://app.airframes.io/messages/3453841057 22 | const text = 'DOOR/FWDENTRY CLSD 1440'; 23 | const decodeResult = decoderPlugin.decode({ text: text }); 24 | 25 | expect(decodeResult.decoded).toBe(true); 26 | expect(decodeResult.decoder.decodeLevel).toBe('full'); 27 | expect(decodeResult.decoder.name).toBe('label-4a-door'); 28 | expect(decodeResult.formatted.description).toBe('Latest New Format'); 29 | expect(decodeResult.message.text).toBe(text); 30 | expect(decodeResult.remaining.text).toBe(undefined); 31 | expect(decodeResult.formatted.items.length).toBe(2); 32 | expect(decodeResult.formatted.items[0].code).toBe('DOOR'); 33 | expect(decodeResult.formatted.items[0].value).toBe('FWDENTRY CLSD'); 34 | expect(decodeResult.formatted.items[1].code).toBe('MSG_TOD'); 35 | expect(decodeResult.formatted.items[1].value).toBe('14:40:00'); 36 | }); 37 | 38 | // disabled because all messages should decode 39 | xtest('decodes Label 4A_DOOR ', () => { 40 | const decoder = new MessageDecoder(); 41 | const decoderPlugin = new Label_4A_DOOR(decoder); 42 | 43 | const text = '4A_DOOR Bogus message'; 44 | const decodeResult = decoderPlugin.decode({ text: text }); 45 | 46 | expect(decodeResult.decoded).toBe(false); 47 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 48 | expect(decodeResult.decoder.name).toBe('label-4a-door'); 49 | expect(decodeResult.formatted.description).toBe('Latest New Format'); 50 | expect(decodeResult.formatted.items.length).toBe(0); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/plugins/Label_4A_DOOR.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { DateTimeUtils } from '../DateTimeUtils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_4A_DOOR extends DecoderPlugin { 7 | name = 'label-4a-door'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['4A'], 12 | preambles: ['DOOR'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.message = message; 20 | decodeResult.formatted.description = 'Latest New Format'; 21 | 22 | decodeResult.decoded = true; 23 | const fields = message.text.split(" "); 24 | if (fields.length === 3) { 25 | ResultFormatter.door_event(decodeResult, fields[0].split("/")[1], fields[1]); 26 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(fields[2] + "00")); 27 | } else { 28 | decodeResult.decoded = false; 29 | ResultFormatter.unknown(decodeResult, message.text); 30 | } 31 | if (decodeResult.decoded) { 32 | if(!decodeResult.remaining.text) 33 | decodeResult.decoder.decodeLevel = 'full'; 34 | else 35 | decodeResult.decoder.decodeLevel = 'partial'; 36 | } else { 37 | decodeResult.decoder.decodeLevel = 'none'; 38 | } 39 | 40 | return decodeResult; 41 | } 42 | } 43 | 44 | export default {}; 45 | -------------------------------------------------------------------------------- /lib/plugins/Label_4A_Slash_01.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_4A_Slash_01 } from './Label_4A_Slash_01'; 3 | 4 | test('matches Label 4A_Slash_01 qualifiers', () => { 5 | const decoder = new MessageDecoder(); 6 | const decoderPlugin = new Label_4A_Slash_01(decoder); 7 | 8 | expect(decoderPlugin.decode).toBeDefined(); 9 | expect(decoderPlugin.name).toBe('label-4a-slash-01'); 10 | expect(decoderPlugin.qualifiers).toBeDefined(); 11 | expect(decoderPlugin.qualifiers()).toEqual({ 12 | labels: ['4A'], 13 | preambles: ['/01'], 14 | }); 15 | }); 16 | 17 | test('decodes Label 4A_Slash_01', () => { 18 | const decoder = new MessageDecoder(); 19 | const decoderPlugin = new Label_4A_Slash_01(decoder); 20 | 21 | // https://app.airframes.io/messages/3460403039 22 | const text = '/01-C'; 23 | const decodeResult = decoderPlugin.decode({ text: text }); 24 | 25 | expect(decodeResult.decoded).toBe(true); 26 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 27 | expect(decodeResult.decoder.name).toBe('label-4a-slash-01'); 28 | expect(decodeResult.formatted.description).toBe('Latest New Format'); 29 | expect(decodeResult.message.text).toBe(text); 30 | expect(decodeResult.remaining.text).toBe('C'); 31 | expect(decodeResult.formatted.items.length).toBe(0); 32 | }); 33 | 34 | // disabled because all messages should decode 35 | xtest('decodes Label 4A_Slash_01 ', () => { 36 | const decoder = new MessageDecoder(); 37 | const decoderPlugin = new Label_4A_Slash_01(decoder); 38 | 39 | const text = '4A_Slash_01 Bogus message'; 40 | const decodeResult = decoderPlugin.decode({ text: text }); 41 | 42 | expect(decodeResult.decoded).toBe(false); 43 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 44 | expect(decodeResult.decoder.name).toBe('label-4a-slash-01'); 45 | expect(decodeResult.formatted.description).toBe('Latest New Format'); 46 | expect(decodeResult.formatted.items.length).toBe(0); 47 | }); 48 | -------------------------------------------------------------------------------- /lib/plugins/Label_4A_Slash_01.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_4A_Slash_01 extends DecoderPlugin { 7 | name = 'label-4a-slash-01'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['4A'], 12 | preambles: ['/01'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.message = message; 20 | decodeResult.formatted.description = 'Latest New Format'; 21 | 22 | decodeResult.decoded = true; 23 | if (message.text.length === 5 && message.text.substring(0,4) === "/01-") { 24 | ResultFormatter.unknown(decodeResult, message.text.substring(4)); 25 | } else { 26 | decodeResult.decoded = false; 27 | ResultFormatter.unknown(decodeResult, message.text); 28 | } 29 | 30 | if (decodeResult.decoded) { 31 | if(!decodeResult.remaining.text) 32 | decodeResult.decoder.decodeLevel = 'full'; 33 | else 34 | decodeResult.decoder.decodeLevel = 'partial'; 35 | } else { 36 | decodeResult.decoder.decodeLevel = 'none'; 37 | } 38 | 39 | return decodeResult; 40 | } 41 | } 42 | 43 | export default {}; 44 | -------------------------------------------------------------------------------- /lib/plugins/Label_4J_POS.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { H1Helper } from '../utils/h1_helper'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_4J_POS extends DecoderPlugin { 7 | name = 'label-4j-pos'; 8 | qualifiers() { // eslint-disable-line class-methods-use-this 9 | return { 10 | labels: ['4J'], 11 | preambles: ['POS/'], 12 | }; 13 | } 14 | 15 | // copied from Label_H1.ts since i don't really want to have to have 16 | // something named like that decode more than 1 type 17 | // if we figure out a good name, i'll combine them 18 | decode(message: Message, options: Options = {}) : DecodeResult { 19 | let decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.message = message; 22 | 23 | const msg = message.text.replace(/\n|\r/g, ""); 24 | const decoded = H1Helper.decodeH1Message(decodeResult, msg); 25 | decodeResult.decoded = decoded; 26 | 27 | decodeResult.decoder.decodeLevel = !decodeResult.remaining.text ? 'full' : 'partial'; 28 | if (decodeResult.formatted.items.length === 0) { 29 | if (options.debug) { 30 | console.log(`Decoder: Unknown H1 message: ${message.text}`); 31 | } 32 | ResultFormatter.unknown(decodeResult, message.text); 33 | decodeResult.decoded = false; 34 | decodeResult.decoder.decodeLevel = 'none'; 35 | } 36 | return decodeResult; 37 | } 38 | } 39 | 40 | export default {}; 41 | -------------------------------------------------------------------------------- /lib/plugins/Label_4N.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_4N extends DecoderPlugin { 7 | name = 'label-4n'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['4N'], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}) : DecodeResult { 16 | const decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.message = message; 19 | decodeResult.formatted.description = 'Airline Defined'; 20 | 21 | // Inmarsat C-band seems to prefix normal messages with a message number and flight number 22 | let text = message.text; 23 | if (text.match(/^M\d{2}A\w{6}/)) { 24 | ResultFormatter.flightNumber(decodeResult, message.text.substring(4, 10).replace(/^([A-Z]+)0*/g, "$1")); 25 | text = text.substring(10); 26 | } 27 | 28 | decodeResult.decoded = true; 29 | const fields = text.split(","); 30 | if (text.length === 51) { 31 | // variant 1 32 | decodeResult.raw.day = text.substring(0, 2); 33 | ResultFormatter.departureAirport(decodeResult, text.substring(8, 11)); 34 | ResultFormatter.arrivalAirport(decodeResult, text.substring(13, 16)); 35 | ResultFormatter.position(decodeResult, CoordinateUtils.decodeStringCoordinatesDecimalMinutes(text.substring(30, 45).replace(/^(.)0/, "$1"))); 36 | ResultFormatter.altitude(decodeResult, Number(text.substring(48, 51)) * 100); 37 | ResultFormatter.unknownArr(decodeResult, [text.substring(2, 4), text.substring(19, 29)], " "); 38 | } else if (fields.length === 33) { 39 | // variant 2 40 | decodeResult.raw.date = fields[3]; 41 | if (fields[1] === "B") { 42 | ResultFormatter.position(decodeResult, {latitude: Number(fields[4]), longitude: Number(fields[5])}); 43 | ResultFormatter.altitude(decodeResult, Number(fields[6])); 44 | } 45 | ResultFormatter.departureAirport(decodeResult, fields[8]); 46 | ResultFormatter.arrivalAirport(decodeResult, fields[9]); 47 | ResultFormatter.alternateAirport(decodeResult, fields[10]); 48 | ResultFormatter.arrivalRunway(decodeResult, fields[11].split("/")[0]); 49 | if (fields[12].length > 1) { 50 | ResultFormatter.alternateRunway(decodeResult, fields[12].split("/")[0]); 51 | } 52 | ResultFormatter.checksum(decodeResult, fields[32]); 53 | ResultFormatter.unknownArr(decodeResult, [...fields.slice(1,3), fields[7], ...fields.slice(13, 32)].filter((f) => f != "")); 54 | } else { 55 | decodeResult.decoded = false; 56 | ResultFormatter.unknown(decodeResult, text); 57 | } 58 | 59 | if (decodeResult.decoded) { 60 | if(!decodeResult.remaining.text) 61 | decodeResult.decoder.decodeLevel = 'full'; 62 | else 63 | decodeResult.decoder.decodeLevel = 'partial'; 64 | } else { 65 | decodeResult.decoder.decodeLevel = 'none'; 66 | } 67 | 68 | return decodeResult; 69 | } 70 | } 71 | 72 | export default {}; 73 | -------------------------------------------------------------------------------- /lib/plugins/Label_4T_AGFSR.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // General Aviation Position Report 8 | export class Label_4T_AGFSR extends DecoderPlugin { 9 | name = 'label-4t-agfsr'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['4T'], 14 | preambles: ['AGFSR'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}) : DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'Position Report'; 22 | decodeResult.message = message; 23 | 24 | const data = message.text.substring(5).split('/'); 25 | 26 | if(!message.text.startsWith('AGFSR') || data.length !== 20) { 27 | if (options.debug) { 28 | console.log(`Decoder: Unknown 4T message: ${message.text}`); 29 | } 30 | ResultFormatter.unknown(decodeResult, message.text); 31 | decodeResult.decoded = false; 32 | decodeResult.decoder.decodeLevel = 'none'; 33 | return decodeResult; 34 | } 35 | 36 | ResultFormatter.flightNumber(decodeResult, data[0].trim()); 37 | ResultFormatter.departureDay(decodeResult, Number(data[1])); 38 | ResultFormatter.arrivalDay(decodeResult, Number(data[2])); 39 | ResultFormatter.departureAirport(decodeResult, data[3].substring(0,3), 'IATA'); 40 | ResultFormatter.arrivalAirport(decodeResult, data[3].substring(3), 'IATA'); 41 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[4].substring(0,4))); // HHMMZ 42 | ResultFormatter.unknown(decodeResult, data[5], '/'); 43 | const lat = data[6].substring(0,7); 44 | const lon = data[6].substring(7,15); 45 | ResultFormatter.position(decodeResult, { 46 | latitude: CoordinateUtils.getDirection(lat[6]) * Number(lat.substring(0,2)) + Number(lat.substring(2,6))/60, 47 | longitude: CoordinateUtils.getDirection(lon[7]) * Number(lon.substring(0,3)) + Number(lon.substring(3, 7))/60 48 | }); 49 | ResultFormatter.altitude(decodeResult, 100* Number(data[7])); 50 | // 8 - phase of flight? 51 | // 11 - temperature? 52 | ResultFormatter.unknownArr(decodeResult, data.slice(8), '/'); 53 | 54 | decodeResult.decoded = true; 55 | decodeResult.decoder.decodeLevel = 'partial'; 56 | 57 | return decodeResult; 58 | } 59 | } 60 | 61 | export default {}; 62 | -------------------------------------------------------------------------------- /lib/plugins/Label_4T_ETA.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_4T_ETA } from './Label_4T_ETA'; 3 | 4 | describe('Label 4T ETA', () => { 5 | 6 | let plugin: Label_4T_ETA; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_4T_ETA(decoder); 11 | }); 12 | 13 | 14 | test('matches qualifiers', () => { 15 | expect(plugin.decode).toBeDefined(); 16 | expect(plugin.name).toBe('label-4t-eta'); 17 | expect(plugin.qualifiers).toBeDefined(); 18 | expect(plugin.qualifiers()).toEqual({ 19 | labels: ['4T'], 20 | preambles: ['ETA'], 21 | }); 22 | }); 23 | 24 | 25 | test('decodes msg 1', () => { 26 | const text = 'ETA AC7221/13/14 YYZ 0902Z'; 27 | 28 | const decodeResult = plugin.decode({ text: text }); 29 | 30 | expect(decodeResult.decoded).toBe(true); 31 | expect(decodeResult.decoder.decodeLevel).toBe('full'); 32 | expect(decodeResult.formatted.description).toBe('ETA Report'); 33 | expect(decodeResult.formatted.items.length).toBe(5); 34 | expect(decodeResult.formatted.items[0].label).toBe('Flight Number'); 35 | expect(decodeResult.formatted.items[0].value).toBe('AC7221'); 36 | expect(decodeResult.formatted.items[1].label).toBe('Departure Day'); 37 | expect(decodeResult.formatted.items[1].value).toBe('13'); 38 | expect(decodeResult.formatted.items[2].label).toBe('Arrival Day'); 39 | expect(decodeResult.formatted.items[2].value).toBe('14'); 40 | expect(decodeResult.formatted.items[3].label).toBe('Destination'); 41 | expect(decodeResult.formatted.items[3].value).toBe('YYZ'); 42 | expect(decodeResult.formatted.items[4].label).toBe('Estimated Time of Arrival'); 43 | expect(decodeResult.formatted.items[4].value).toBe('09:02:00'); 44 | }); 45 | 46 | 47 | test('decodes ', () => { 48 | 49 | const text = 'ETA Bogus message'; 50 | const decodeResult = plugin.decode({ text: text }); 51 | 52 | expect(decodeResult.decoded).toBe(false); 53 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 54 | expect(decodeResult.formatted.description).toBe('ETA Report'); 55 | expect(decodeResult.formatted.items.length).toBe(0); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /lib/plugins/Label_4T_ETA.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // General Aviation Position Report 8 | export class Label_4T_ETA extends DecoderPlugin { 9 | name = 'label-4t-eta'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['4T'], 14 | preambles: ['ETA'], 15 | }; 16 | } 17 | 18 | decode(message: Message, options: Options = {}) : DecodeResult { 19 | const decodeResult = this.defaultResult(); 20 | decodeResult.decoder.name = this.name; 21 | decodeResult.formatted.description = 'ETA Report'; 22 | decodeResult.message = message; 23 | 24 | const data = message.text.substring(3).split('/'); 25 | 26 | if(!message.text.startsWith('ETA') || data.length !== 3) { 27 | if (options.debug) { 28 | console.log(`Decoder: Unknown 4T message: ${message.text}`); 29 | } 30 | ResultFormatter.unknown(decodeResult, message.text); 31 | decodeResult.decoded = false; 32 | decodeResult.decoder.decodeLevel = 'none'; 33 | return decodeResult; 34 | } 35 | 36 | ResultFormatter.flightNumber(decodeResult, data[0].trim()); 37 | ResultFormatter.departureDay(decodeResult, Number(data[1])); 38 | const etaData = data[2].split(' '); 39 | ResultFormatter.arrivalDay(decodeResult, Number(etaData[0])); 40 | ResultFormatter.arrivalAirport(decodeResult, etaData[1], 'IATA'); 41 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(etaData[2].substring(0,4))); 42 | 43 | decodeResult.decoded = true; 44 | decodeResult.decoder.decodeLevel = 'full'; 45 | 46 | return decodeResult; 47 | } 48 | } 49 | 50 | export default {}; 51 | -------------------------------------------------------------------------------- /lib/plugins/Label_58.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_58 } from './Label_58'; 3 | 4 | describe('Label_58', () => { 5 | let plugin: Label_58; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_58(decoder); 10 | }); 11 | 12 | test('matches qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-58'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['58'], 18 | }); 19 | }); 20 | 21 | test('decodes variant 1', () => { 22 | const text = 'OG0704/06/230942/N39.214/W76.106/22683/N/' 23 | const decodeResult = plugin.decode({ text: text }); 24 | 25 | expect(decodeResult.decoded).toBe(true); 26 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 27 | expect(decodeResult.raw.flight_number).toBe('OG0704'); 28 | expect(decodeResult.raw.day).toBe(6); 29 | expect(decodeResult.raw.time_of_day).toBe(83382); 30 | expect(decodeResult.raw.position.latitude).toBe(39.214); 31 | expect(decodeResult.raw.position.longitude).toBe(-76.106); 32 | expect(decodeResult.raw.altitude).toBe(22683); 33 | expect(decodeResult.formatted.items.length).toBe(5); 34 | expect(decodeResult.formatted.items[0].label).toBe('Flight Number'); 35 | expect(decodeResult.formatted.items[0].value).toBe('OG0704'); 36 | expect(decodeResult.formatted.items[1].label).toBe('Day of Month'); 37 | expect(decodeResult.formatted.items[1].value).toBe('6'); 38 | expect(decodeResult.formatted.items[2].label).toBe('Message Timestamp'); 39 | expect(decodeResult.formatted.items[2].value).toBe('23:09:42'); 40 | expect(decodeResult.formatted.items[3].label).toBe('Aircraft Position'); 41 | expect(decodeResult.formatted.items[3].value).toBe('39.214 N, 76.106 W'); 42 | expect(decodeResult.formatted.items[4].label).toBe('Altitude'); 43 | expect(decodeResult.formatted.items[4].value).toBe('22683 feet'); 44 | expect(decodeResult.remaining.text).toBe('N/'); 45 | }); 46 | 47 | test('does not decode ', () => { 48 | 49 | const text = 'Bogus/message'; 50 | const decodeResult = plugin.decode({ text: text }); 51 | 52 | expect(decodeResult.decoded).toBe(false); 53 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 54 | expect(decodeResult.decoder.name).toBe('label-58'); 55 | expect(decodeResult.formatted.description).toBe('Position Report'); 56 | expect(decodeResult.message.text).toBe(text); 57 | }); 58 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_58.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | // General Aviation Position Report 8 | export class Label_58 extends DecoderPlugin { 9 | name = 'label-58'; 10 | 11 | qualifiers() { // eslint-disable-line class-methods-use-this 12 | return { 13 | labels: ['58'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}): DecodeResult { 18 | const decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'Position Report'; 21 | decodeResult.message = message; 22 | 23 | const data = message.text.split('/'); 24 | if (data.length === 8) { 25 | ResultFormatter.flightNumber(decodeResult, data[0]); 26 | ResultFormatter.day(decodeResult, Number(data[1])); 27 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(data[2])); 28 | const lat = data[3]; 29 | const lon = data[4]; 30 | ResultFormatter.position(decodeResult, { 31 | latitude: CoordinateUtils.getDirection(lat[0]) * Number(lat.substring(1)), 32 | longitude:CoordinateUtils.getDirection(lon[0]) * Number(lon.substring(1)) 33 | }); 34 | ResultFormatter.altitude(decodeResult, Number(data[5])); 35 | ResultFormatter.unknown(decodeResult, data[6], '/'); 36 | ResultFormatter.unknown(decodeResult, data[7], '/'); 37 | } else { 38 | if (options.debug) { 39 | console.log(`Decoder: Unknown 58 message: ${message.text}`); 40 | } 41 | ResultFormatter.unknown(decodeResult, message.text); 42 | decodeResult.decoded = false; 43 | decodeResult.decoder.decodeLevel = 'none'; 44 | return decodeResult; 45 | } 46 | 47 | decodeResult.decoded = true; 48 | decodeResult.decoder.decodeLevel = 'partial'; 49 | return decodeResult; 50 | } 51 | } 52 | 53 | export default {}; 54 | -------------------------------------------------------------------------------- /lib/plugins/Label_8E.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_8E } from './Label_8E'; 3 | 4 | test('decodes Label 8E sample 1', () => { 5 | const decoder = new MessageDecoder(); 6 | const decoderPlugin = new Label_8E(decoder); 7 | 8 | expect(decoderPlugin.decode).toBeDefined(); 9 | expect(decoderPlugin.name).toBe('label-8e'); 10 | expect(decoderPlugin.qualifiers).toBeDefined(); 11 | expect(decoderPlugin.qualifiers()).toEqual({ 12 | labels: ['8E'], 13 | }); 14 | 15 | const decodeResult = decoderPlugin.decode({ text: 'EGSS,1618' }); 16 | 17 | expect(decodeResult.decoded).toBe(true); 18 | expect(decodeResult.decoder.name).toBe('label-8e'); 19 | expect(decodeResult.formatted.description).toBe('ETA Report'); 20 | expect(decodeResult.message.text).toBe('EGSS,1618'); 21 | expect(decodeResult.raw.arrival_icao).toBe('EGSS'); 22 | expect(decodeResult.formatted.items.length).toBe(2); 23 | expect(decodeResult.formatted.items[0].type).toBe('time_of_day'); 24 | expect(decodeResult.formatted.items[0].code).toBe('ETA'); 25 | expect(decodeResult.formatted.items[0].label).toBe('Estimated Time of Arrival'); 26 | expect(decodeResult.formatted.items[0].value).toBe('16:18:00'); 27 | expect(decodeResult.formatted.items[1].type).toBe('icao'); 28 | expect(decodeResult.formatted.items[1].code).toBe('DST'); 29 | expect(decodeResult.formatted.items[1].label).toBe('Destination'); 30 | expect(decodeResult.formatted.items[1].value).toBe('EGSS'); 31 | }); 32 | -------------------------------------------------------------------------------- /lib/plugins/Label_8E.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | // ETA 7 | export class Label_8E extends DecoderPlugin { 8 | name = 'label-8e'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ["8E"], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'ETA Report'; 20 | decodeResult.message = message; 21 | 22 | // Style: EGSS,1618 23 | // Match: arrival_icao,arrival_eta 24 | const regex = /^(?\w{4}),(?\d{4})$/; 25 | const results = message.text.match(regex); 26 | if (results?.groups) { 27 | if (options.debug) { 28 | console.log(`Label 8E ETA: groups`); 29 | console.log(results.groups); 30 | } 31 | 32 | ResultFormatter.eta(decodeResult, DateTimeUtils.convertHHMMSSToTod(results.groups.arrival_eta)); 33 | ResultFormatter.arrivalAirport(decodeResult, results.groups.arrival_icao); 34 | } 35 | 36 | decodeResult.decoded = true; 37 | decodeResult.decoder.decodeLevel = 'full'; 38 | 39 | return decodeResult; 40 | } 41 | } 42 | 43 | export default {}; 44 | -------------------------------------------------------------------------------- /lib/plugins/Label_B6.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | 4 | // CPDLC 5 | export class Label_B6_Forwardslash extends DecoderPlugin { 6 | name = 'label-b6-forwardslash'; 7 | 8 | qualifiers() { // eslint-disable-line class-methods-use-this 9 | return { 10 | labels: ['B6'], 11 | preambles: ['/'], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}) : DecodeResult { 16 | const decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.formatted.description = 'CPDLC Message'; 19 | decodeResult.message = message; 20 | 21 | if (options.debug) { 22 | console.log("CPDLC: " + message); 23 | } 24 | 25 | return decodeResult; 26 | } 27 | } 28 | 29 | export default {}; 30 | -------------------------------------------------------------------------------- /lib/plugins/Label_ColonComma.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | 4 | export class Label_ColonComma extends DecoderPlugin { 5 | name = 'label-colon-comma'; 6 | 7 | qualifiers() { // eslint-disable-line class-methods-use-this 8 | return { 9 | labels: [':;'], 10 | }; 11 | } 12 | 13 | decode(message: Message, options: Options = {}) : DecodeResult { 14 | const decodeResult = this.defaultResult(); 15 | decodeResult.decoder.name = this.name; 16 | 17 | decodeResult.raw.frequency = Number(message.text) / 1000; 18 | 19 | decodeResult.formatted.description = 'Aircraft Transceiver Frequency Change'; 20 | decodeResult.formatted.items.push({ 21 | type: 'frequency', 22 | label: 'Frequency', 23 | value: `${decodeResult.raw.frequency} MHz`, 24 | code: 'FREQ' 25 | }); 26 | 27 | decodeResult.decoded = true; 28 | decodeResult.decoder.decodeLevel = 'full'; 29 | 30 | return decodeResult; 31 | } 32 | } 33 | 34 | export default {}; 35 | -------------------------------------------------------------------------------- /lib/plugins/Label_H1.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_H1 } from './Label_H1'; 3 | 4 | describe('Label_H1 INI', () => { 5 | 6 | let plugin: Label_H1; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_H1(decoder); 11 | }); 12 | 13 | 14 | test('matches Label H1 Preamble FLR qualifiers', () => { 15 | expect(plugin.decode).toBeDefined(); 16 | expect(plugin.name).toBe('label-h1'); 17 | expect(plugin.qualifiers).toBeDefined(); 18 | expect(plugin.qualifiers()).toEqual({ 19 | labels: ['H1'], 20 | }); 21 | }); 22 | 23 | test('INI ', () => { 24 | 25 | const text = 'Bogus message'; 26 | const decodeResult = plugin.decode({ text: text }); 27 | 28 | expect(decodeResult.decoded).toBe(false); 29 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 30 | expect(decodeResult.formatted.description).toBe('Unknown H1 Message'); 31 | expect(decodeResult.message.text).toBe(text); 32 | }); 33 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_H1.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { H1Helper } from '../utils/h1_helper'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_H1 extends DecoderPlugin { 7 | name = 'label-h1'; 8 | qualifiers() { // eslint-disable-line class-methods-use-this 9 | return { 10 | labels: ['H1'], 11 | }; 12 | } 13 | 14 | decode(message: Message, options: Options = {}) : DecodeResult { 15 | let decodeResult = this.defaultResult(); 16 | decodeResult.decoder.name = this.name; 17 | decodeResult.message = message; 18 | 19 | const msg = message.text.replace(/\n|\r/g, ""); 20 | const decoded = H1Helper.decodeH1Message(decodeResult, msg); 21 | decodeResult.decoded = decoded; 22 | 23 | decodeResult.decoder.decodeLevel = !decodeResult.remaining.text ? 'full' : 'partial'; 24 | if (decodeResult.formatted.items.length === 0) { 25 | if (options.debug) { 26 | console.log(`Decoder: Unknown H1 message: ${message.text}`); 27 | } 28 | ResultFormatter.unknown(decodeResult, message.text); 29 | decodeResult.decoded = false; 30 | decodeResult.decoder.decodeLevel = 'none'; 31 | } 32 | return decodeResult; 33 | } 34 | } 35 | 36 | export default {}; 37 | -------------------------------------------------------------------------------- /lib/plugins/Label_H1_FLR.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_H1_FLR extends DecoderPlugin { 7 | name = 'label-h1-flr'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["H1"], 12 | preambles: ['FLR', '#CFBFLR'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | let decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'Fault Log Report'; 20 | decodeResult.message = message; 21 | 22 | const parts = message.text.split('/FR'); 23 | 24 | if(parts.length > 1){ 25 | //decode header 26 | // can go away, here because i want to decode it 27 | const fields = parts[0].split('/'); 28 | // 0 is the msg type 29 | for(let i=1; i { 5 | 6 | let plugin: Label_H1; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_H1(decoder); 11 | }); 12 | 13 | 14 | test('decodes Label H1 Preamble FTX valid', () => { 15 | // https://app.airframes.io/messages/3402014738 16 | const text = 'FTX/ID23544S,HIFI21,7VZ007B1S276/MR2,/FXFYI .. TAF KSUX 021720Z 0218 0318 20017G28KT P6SM SKC FM022200 22012G18KT P6SM SKC .. PUTS YOUR CXWIND AT 26KT ON RWY 13 .. REDUCES TO 18KT AT 22Z4FEF' 17 | const decodeResult = plugin.decode({ text: text }); 18 | 19 | expect(decodeResult.decoded).toBe(true); 20 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 21 | expect(decodeResult.raw.mission_number).toBe('7VZ007B1S276'); 22 | expect(decodeResult.formatted.items.length).toBe(4); 23 | expect(decodeResult.formatted.items[0].label).toBe('Tail'); 24 | expect(decodeResult.formatted.items[0].value).toBe('23544S'); 25 | expect(decodeResult.formatted.items[1].label).toBe('Flight Number'); 26 | expect(decodeResult.formatted.items[1].value).toBe('HIFI21'); 27 | expect(decodeResult.formatted.items[2].label).toBe('Free Text'); 28 | expect(decodeResult.formatted.items[2].value).toBe('FYI .. TAF KSUX 021720Z 0218 0318 20017G28KT P6SM SKC FM022200 22012G18KT P6SM SKC .. PUTS YOUR CXWIND AT 26KT ON RWY 13 .. REDUCES TO 18KT AT 22Z'); 29 | expect(decodeResult.formatted.items[3].label).toBe('Message Checksum'); 30 | expect(decodeResult.formatted.items[3].value).toBe('0x4fef'); 31 | expect(decodeResult.remaining.text).toBe('MR2,'); 32 | }); 33 | 34 | test('decodes Label H1 Preamble - #MDFTX valid', () => { 35 | // https://app.airframes.io/messages/3400555283 36 | const text = '- #MDFTX/ID77170A,RCH836,ABZ01G6XH273/MR2,/FXIRAN IS LAUNCHING MISSILES TOWARDS ISRAEL. YOUR FLIGHT PATH IS CURRENTLY NORTH OF PROJECTED MISSILE TRACKS. EXERCIZE EXTREME CAUTION.4A99' 37 | const decodeResult = plugin.decode({ text: text }); 38 | 39 | expect(decodeResult.decoded).toBe(true); 40 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 41 | expect(decodeResult.raw.mission_number).toBe('ABZ01G6XH273'); 42 | expect(decodeResult.formatted.items.length).toBe(4); 43 | expect(decodeResult.formatted.items[0].label).toBe('Tail'); 44 | expect(decodeResult.formatted.items[0].value).toBe('77170A'); 45 | expect(decodeResult.formatted.items[1].label).toBe('Flight Number'); 46 | expect(decodeResult.formatted.items[1].value).toBe('RCH836'); 47 | expect(decodeResult.formatted.items[2].label).toBe('Free Text'); 48 | expect(decodeResult.formatted.items[2].value).toBe('IRAN IS LAUNCHING MISSILES TOWARDS ISRAEL. YOUR FLIGHT PATH IS CURRENTLY NORTH OF PROJECTED MISSILE TRACKS. EXERCIZE EXTREME CAUTION.'); 49 | expect(decodeResult.formatted.items[3].label).toBe('Message Checksum'); 50 | expect(decodeResult.formatted.items[3].value).toBe('0x4a99'); 51 | expect(decodeResult.remaining.text).toBe('- #MD/MR2,'); 52 | }); 53 | 54 | test('decodes Label H1 Preamble POS ', () => { 55 | 56 | const text = 'FTX Bogus message'; 57 | const decodeResult = plugin.decode({ text: text }); 58 | 59 | expect(decodeResult.decoded).toBe(false); 60 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 61 | expect(decodeResult.formatted.description).toBe('Free Text'); 62 | expect(decodeResult.message.text).toBe(text); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /lib/plugins/Label_H1_OHMA.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { ResultFormatter } from '../utils/result_formatter'; 4 | 5 | import * as zlib from "minizlib"; 6 | 7 | export class Label_H1_OHMA extends DecoderPlugin { 8 | name = 'label-h1-ohma'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ["H1"], 13 | preambles: ['OHMA', '/RTNBOCR.OHMA', '#T1B/RTNBOCR.OHMA'], 14 | }; 15 | } 16 | 17 | decode(message: Message, options: Options = {}) : DecodeResult { 18 | let decodeResult = this.defaultResult(); 19 | decodeResult.decoder.name = this.name; 20 | decodeResult.formatted.description = 'OHMA Message'; 21 | decodeResult.message = message; 22 | 23 | const data = message.text.split('OHMA')[1]; // throw out '/RTNOCR.' - even though it means something 24 | try { 25 | const compressedBuffer = Buffer.from(data, 'base64'); 26 | const decompress = new zlib.Inflate({windowBits: 15}); 27 | decompress.write(compressedBuffer); 28 | decompress.flush(zlib.constants.Z_SYNC_FLUSH); 29 | const result = decompress.read(); 30 | const jsonText = result.toString(); 31 | 32 | let formattedMsg; 33 | let jsonMessage; 34 | try { 35 | jsonMessage = JSON.parse(jsonText).message; 36 | } catch { 37 | jsonMessage = jsonText; 38 | } 39 | 40 | try { 41 | const ohmaMsg = JSON.parse(jsonMessage); 42 | formattedMsg = JSON.stringify(ohmaMsg, null, 2); 43 | } catch { 44 | formattedMsg = jsonMessage; 45 | } 46 | decodeResult.decoded = true; 47 | decodeResult.decoder.decodeLevel = 'full'; 48 | decodeResult.raw.ohma = jsonText; 49 | decodeResult.formatted.items.push({ 50 | type: 'ohma', 51 | code: 'OHMA' , 52 | label: 'OHMA Downlink', 53 | value: formattedMsg, 54 | }); 55 | } catch (e) { 56 | // Unknown 57 | if (options.debug) { 58 | console.log(`Decoder: Unknown H1 OHMA message: ${message.text}`, e); 59 | } 60 | ResultFormatter.unknown(decodeResult, message.text); 61 | decodeResult.decoded = false; 62 | decodeResult.decoder.decodeLevel = 'none'; 63 | } 64 | 65 | return decodeResult; 66 | } 67 | } 68 | 69 | export default {}; 70 | 71 | -------------------------------------------------------------------------------- /lib/plugins/Label_H1_PWI.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_H1 } from './Label_H1'; 3 | 4 | describe('Label H1 PWI', () => { 5 | let plugin: Label_H1; 6 | 7 | beforeEach(() => { 8 | const decoder = new MessageDecoder(); 9 | plugin = new Label_H1(decoder); 10 | }); 11 | 12 | test('matches Label H1 Preamble PWI qualifiers', () => { 13 | expect(plugin.decode).toBeDefined(); 14 | expect(plugin.name).toBe('label-h1'); 15 | expect(plugin.qualifiers).toBeDefined(); 16 | expect(plugin.qualifiers()).toEqual({ 17 | labels: ['H1'], 18 | }); 19 | }); 20 | 21 | 22 | test('decodes Label H1 Preamble PWI valid', () => { 23 | const text = 'PWI/WD390,COLZI,258070.AWYAT,252071.IPTAY,250065.CHOPZ,244069.MGMRY,234065.CATLN,230060/WD340,COLZI,256073,340M41.AWYAT,252070,340M41.IPTAY,244059,340M41.CHOPZ,240059,340M41.MGMRY,232056,340M41.CATLN,218053,340M40/WD300,COLZI,256065.AWYAT,254062.IPTAY,250051.CHOPZ,248050.MGMRY,232044.CATLN,222047/WD240,COLZI,260045.AWYAT,258048.IPTAY,254043.CHOPZ,256041.MGMRY,238035.CATLN,226034/DD300214059.240214040.180236024.100250018:,,,,/CB300246040.240246017.180226015.1002100080338' 24 | const decodeResult = plugin.decode({ text: text }); 25 | 26 | expect(decodeResult.decoded).toBe(true); 27 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 28 | expect(decodeResult.formatted.items.length).toBe(25); 29 | expect(decodeResult.formatted.items[0].type).toBe('wind_data'); 30 | expect(decodeResult.formatted.items[0].code).toBe('WIND'); 31 | expect(decodeResult.formatted.items[0].label).toBe('Wind Data'); 32 | expect(decodeResult.formatted.items[0].value).toBe('COLZI at FL390: 258° at 70kt'); 33 | expect(decodeResult.formatted.items[1].type).toBe('wind_data'); 34 | expect(decodeResult.formatted.items[1].code).toBe('WIND'); 35 | expect(decodeResult.formatted.items[1].label).toBe('Wind Data'); 36 | expect(decodeResult.formatted.items[1].value).toBe('AWYAT at FL390: 252° at 71kt'); 37 | expect(decodeResult.formatted.items[24].label).toBe('Message Checksum'); 38 | expect(decodeResult.formatted.items[24].value).toBe('0x0338'); 39 | expect(decodeResult.remaining.text).toBe('DD300214059.240214040.180236024.100250018:,,,,/CB300246040.240246017.180226015.100210008'); 40 | }); 41 | 42 | test('decodes Label H1 Preamble POS ', () => { 43 | 44 | const text = 'PWI Bogus message'; 45 | const decodeResult = plugin.decode({ text: text }); 46 | 47 | expect(decodeResult.decoded).toBe(false); 48 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 49 | expect(decodeResult.decoder.name).toBe('label-h1'); 50 | expect(decodeResult.formatted.description).toBe('Weather Report'); 51 | expect(decodeResult.message.text).toBe(text); 52 | }); 53 | }); -------------------------------------------------------------------------------- /lib/plugins/Label_H1_Slash.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { Waypoint } from '../types/waypoint'; 5 | import { CoordinateUtils } from '../utils/coordinate_utils'; 6 | import { H1Helper } from '../utils/h1_helper'; 7 | import { ResultFormatter } from '../utils/result_formatter'; 8 | import { RouteUtils } from '../utils/route_utils'; 9 | 10 | export class Label_H1_Slash extends DecoderPlugin { 11 | name = 'label-h1-slash'; 12 | qualifiers() { // eslint-disable-line class-methods-use-this 13 | return { 14 | labels: ['H1'], 15 | preambles: ['/'] 16 | }; 17 | } 18 | 19 | decode(message: Message, options: Options = {}) : DecodeResult { 20 | let decodeResult = this.defaultResult(); 21 | decodeResult.decoder.name = this.name; 22 | decodeResult.formatted.description = 'Position Report'; 23 | decodeResult.message = message; 24 | 25 | const checksum = message.text.slice(-4); 26 | const data = message.text.slice(0, message.text.length - 4); 27 | 28 | const fields = data.split('/'); 29 | 30 | if(fields[0] !== '') { 31 | ResultFormatter.unknown(decodeResult, message.text); 32 | decodeResult.decoded = false; 33 | decodeResult.decoder.decodeLevel = 'none'; 34 | return decodeResult; 35 | } 36 | 37 | const headerData = fields[1].split('.'); 38 | ResultFormatter.unknown(decodeResult, headerData[0]); 39 | if(headerData[1] === 'POS' && fields[2].startsWith('TS') && fields[2].length > 15) { 40 | // variant 3 hack 41 | // rip out the timestamp and process the rest 42 | H1Helper.processPosition(decodeResult, fields[2].substring(15).split(',')); 43 | } else if(headerData[1] === 'POS') { 44 | // do nothing 45 | } else if(headerData[1].startsWith('POS')) { 46 | H1Helper.processPosition(decodeResult, headerData[1].substring(3).split(',')); 47 | } else { 48 | ResultFormatter.unknown(decodeResult, headerData[1], '.'); 49 | } 50 | 51 | for(let i=2; i { 5 | 6 | let plugin: Label_H1_StarPOS; 7 | 8 | beforeEach(() => { 9 | const decoder = new MessageDecoder(); 10 | plugin = new Label_H1_StarPOS(decoder); 11 | }); 12 | 13 | test('decodes variant 1', () => { 14 | 15 | const text = '*POS10300950N3954W07759363312045802M5230175'; 16 | const decodeResult = plugin.decode({ text: text }); 17 | 18 | expect(decodeResult.decoded).toBe(true); 19 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 20 | expect(decodeResult.message.text).toBe(text); 21 | expect(decodeResult.raw.day).toBe(30); 22 | expect(decodeResult.raw.month).toBe(10); 23 | expect(decodeResult.raw.time_of_day).toBe(35400); 24 | expect(decodeResult.raw.position.latitude).toBe(39.900000); 25 | expect(decodeResult.raw.position.longitude).toBe(-77.98333333333333); 26 | expect(decodeResult.raw.altitude).toBe(36331); 27 | expect(decodeResult.formatted.items.length).toBe(5); 28 | expect(decodeResult.formatted.items[0].label).toBe('Month of Year'); 29 | expect(decodeResult.formatted.items[0].value).toBe('10'); 30 | expect(decodeResult.formatted.items[1].label).toBe('Day of Month'); 31 | expect(decodeResult.formatted.items[1].value).toBe('30'); 32 | expect(decodeResult.formatted.items[2].label).toBe('Message Timestamp'); 33 | expect(decodeResult.formatted.items[2].value).toBe('09:50:00'); 34 | expect(decodeResult.formatted.items[3].label).toBe('Aircraft Position'); 35 | expect(decodeResult.formatted.items[3].value).toBe('39.900 N, 77.983 W'); 36 | expect(decodeResult.formatted.items[4].label).toBe('Altitude'); 37 | expect(decodeResult.formatted.items[4].value).toBe('36331 feet'); 38 | 39 | expect(decodeResult.remaining.text).toBe('2045802M5230175'); 40 | }); 41 | 42 | 43 | test('does not decode ', () => { 44 | 45 | const text = '*POS Bogus message'; 46 | const decodeResult = plugin.decode({ text: text }); 47 | 48 | expect(decodeResult.decoded).toBe(false); 49 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 50 | expect(decodeResult.formatted.description).toBe('Position Report'); 51 | expect(decodeResult.formatted.items.length).toBe(0); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/plugins/Label_H1_StarPOS.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { CoordinateUtils } from '../utils/coordinate_utils'; 5 | import { ResultFormatter } from '../utils/result_formatter'; 6 | 7 | export class Label_H1_StarPOS extends DecoderPlugin { 8 | name = 'label-h1-star-pos'; 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['H1'], 12 | preambles: ['*POS'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | let decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'Position Report'; 20 | decodeResult.message = message; 21 | 22 | const msg = message.text; 23 | // assuming fixed length until other variants found 24 | if (msg.length !== 43 || !msg.startsWith('*POS')) { 25 | if (options.debug) { 26 | console.log(`Decoder: Unknown H1 message: ${msg}`); 27 | } 28 | ResultFormatter.unknown(decodeResult, msg); 29 | decodeResult.decoded = false; 30 | decodeResult.decoder.decodeLevel = 'none'; 31 | return decodeResult; 32 | } 33 | 34 | ResultFormatter.month(decodeResult, Number(msg.substring(4, 6))); 35 | ResultFormatter.day(decodeResult, Number(msg.substring(6, 8))); 36 | ResultFormatter.time_of_day(decodeResult, DateTimeUtils.convertHHMMSSToTod(msg.substring(8, 12))); 37 | ResultFormatter.position(decodeResult, { // Deg Min, no sec 38 | latitude: CoordinateUtils.getDirection(msg.substring(12,13)) * (Number(msg.substring(13, 15)) + Number(msg.substring(15, 17))/60), 39 | longitude: CoordinateUtils.getDirection(msg.substring(17,18)) * (Number(msg.substring(18, 21)) + Number(msg.substring(21, 23))/60) 40 | }); 41 | ResultFormatter.altitude(decodeResult, Number(msg.substring(23, 28))); 42 | ResultFormatter.unknown(decodeResult, msg.substring(28)); 43 | decodeResult.decoded = true; 44 | decodeResult.decoder.decodeLevel = 'partial'; 45 | return decodeResult; 46 | } 47 | } 48 | 49 | export default {}; 50 | -------------------------------------------------------------------------------- /lib/plugins/Label_H1_WRN.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_H1_WRN extends DecoderPlugin { 7 | name = 'label-h1-wrn'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ["H1"], 12 | preambles: ['WRN', '#CFBWRN'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | let decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.formatted.description = 'Warning Message'; 20 | decodeResult.message = message; 21 | 22 | const parts = message.text.split('/WN'); 23 | 24 | if(parts.length > 1){ 25 | //decode header 26 | // can go away, here because i want to decode it 27 | const fields = parts[0].split('/'); 28 | // 0 is the msg type 29 | ResultFormatter.unknownArr(decodeResult, fields.slice(1), '/'); 30 | 31 | const data = parts[1].substring(0,20); 32 | const msg = parts[1].substring(20); 33 | const datetime = data.substring(0,12); // YYMMDDHHMMSS (SS might be next 2 chars) 34 | const date = datetime.substring(4,6) + datetime.substring(2,4)+ datetime.substring(0,2); 35 | 36 | ResultFormatter.unknown(decodeResult, data.substring(12), '/'); 37 | decodeResult.raw.message_timestamp = DateTimeUtils.convertDateTimeToEpoch(datetime.substring(6), date); 38 | // TODO: decode further 39 | decodeResult.raw.warning_message = msg; 40 | decodeResult.formatted.items.push({ 41 | type: 'warning', 42 | code: 'WRN', 43 | label: 'Warning Message', 44 | value: decodeResult.raw.warning_message, 45 | }); 46 | decodeResult.decoded = true; 47 | decodeResult.decoder.decodeLevel = 'partial'; 48 | } else { 49 | // Unknown 50 | if (options.debug) { 51 | console.log(`Decoder: Unknown H1 message: ${message.text}`); 52 | } 53 | ResultFormatter.unknown(decodeResult, message.text); 54 | decodeResult.decoded = false; 55 | decodeResult.decoder.decodeLevel = 'none'; 56 | } 57 | 58 | return decodeResult; 59 | } 60 | } 61 | 62 | export default {}; 63 | -------------------------------------------------------------------------------- /lib/plugins/Label_HX.test.ts: -------------------------------------------------------------------------------- 1 | import { MessageDecoder } from '../MessageDecoder'; 2 | import { Label_HX } from './Label_HX'; 3 | 4 | test('matches Label HX qualifiers', () => { 5 | const decoder = new MessageDecoder(); 6 | const decoderPlugin = new Label_HX(decoder); 7 | 8 | expect(decoderPlugin.decode).toBeDefined(); 9 | expect(decoderPlugin.name).toBe('label-hx'); 10 | }); 11 | 12 | test('decodes Label HX variant 1', () => { 13 | const decoder = new MessageDecoder(); 14 | const decoderPlugin = new Label_HX(decoder); 15 | 16 | // https://globe.adsbexchange.com/?icao=A41722&showTrace=2024-09-24×tamp=1727202494 17 | const text = 'RA FMT LOCATION N4009.6 W07540.8'; 18 | const decodeResult = decoderPlugin.decode({ text: text }); 19 | 20 | expect(decodeResult.decoded).toBe(true); 21 | expect(decodeResult.decoder.decodeLevel).toBe('full'); 22 | expect(decodeResult.decoder.name).toBe('label-hx'); 23 | expect(decodeResult.formatted.description).toBe('Undelivered Uplink Report'); 24 | expect(decodeResult.message.text).toBe(text); 25 | expect(decodeResult.raw.position.latitude).toBe(40.16); 26 | expect(decodeResult.raw.position.longitude).toBe(-75.68); 27 | expect(decodeResult.formatted.items.length).toBe(1); 28 | expect(decodeResult.formatted.items[0].type).toBe('aircraft_position'); 29 | expect(decodeResult.formatted.items[0].code).toBe('POS'); 30 | expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position'); 31 | expect(decodeResult.formatted.items[0].value).toBe('40.160 N, 75.680 W'); 32 | }); 33 | 34 | test('decodes Label HX variant 2', () => { 35 | const decoder = new MessageDecoder(); 36 | const decoderPlugin = new Label_HX(decoder); 37 | 38 | // https://globe.adsbexchange.com/?icao=A92EA0&showTrace=2024-09-22×tamp=1727038330 39 | const text = 'RA FMT 43 GSP B02'; 40 | const decodeResult = decoderPlugin.decode({ text: text }); 41 | 42 | expect(decodeResult.decoded).toBe(true); 43 | expect(decodeResult.decoder.decodeLevel).toBe('partial'); 44 | expect(decodeResult.decoder.name).toBe('label-hx'); 45 | expect(decodeResult.formatted.description).toBe('Undelivered Uplink Report'); 46 | expect(decodeResult.message.text).toBe(text); 47 | expect(decodeResult.raw.departure_icao).toBe('GSP'); 48 | expect(decodeResult.remaining.text).toBe('B02'); 49 | expect(decodeResult.formatted.items.length).toBe(1); 50 | expect(decodeResult.formatted.items[0].type).toBe('icao'); 51 | expect(decodeResult.formatted.items[0].code).toBe('ORG'); 52 | expect(decodeResult.formatted.items[0].label).toBe('Origin'); 53 | expect(decodeResult.formatted.items[0].value).toBe('GSP'); 54 | }); 55 | 56 | test('decodes Label HX ', () => { 57 | const decoder = new MessageDecoder(); 58 | const decoderPlugin = new Label_HX(decoder); 59 | 60 | const text = 'HX Bogus message'; 61 | const decodeResult = decoderPlugin.decode({ text: text }); 62 | 63 | expect(decodeResult.decoded).toBe(false); 64 | expect(decodeResult.decoder.decodeLevel).toBe('none'); 65 | expect(decodeResult.decoder.name).toBe('label-hx'); 66 | expect(decodeResult.formatted.description).toBe('Undelivered Uplink Report'); 67 | expect(decodeResult.formatted.items.length).toBe(0); 68 | }); 69 | -------------------------------------------------------------------------------- /lib/plugins/Label_HX.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_HX extends DecoderPlugin { 7 | name = 'label-hx'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['HX'], 12 | preambles: ['RA FMT LOCATION', 'RA FMT 43'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.message = message; 20 | decodeResult.formatted.description = 'Undelivered Uplink Report'; 21 | 22 | const parts = message.text.split(' '); 23 | 24 | decodeResult.decoded = true; 25 | if (parts[2] === "LOCATION") { 26 | let latdir = parts[3].substring(0, 1); 27 | let latdeg = Number(parts[3].substring(1, 3)); 28 | let latmin = Number(parts[3].substring(3, 7)); 29 | let londir = parts[4].substring(0, 1); 30 | let londeg = Number(parts[4].substring(1, 4)); 31 | let lonmin = Number(parts[4].substring(4, 8)); 32 | let pos = { 33 | latitude: (latdeg + latmin/60) * (latdir === 'N' ? 1 : -1), 34 | longitude: (londeg + lonmin/60) * (londir === 'E' ? 1 : -1), 35 | }; 36 | ResultFormatter.unknownArr(decodeResult, parts.slice(5), ' '); 37 | ResultFormatter.position(decodeResult, pos); 38 | } else if (parts[2] === "43") { 39 | ResultFormatter.departureAirport(decodeResult, parts[3]); 40 | ResultFormatter.unknownArr(decodeResult, parts.slice(4), ' '); 41 | } else { 42 | decodeResult.decoded = false; 43 | } 44 | 45 | if (decodeResult.decoded) { 46 | if(!decodeResult.remaining.text) 47 | decodeResult.decoder.decodeLevel = 'full'; 48 | else 49 | decodeResult.decoder.decodeLevel = 'partial'; 50 | } else { 51 | decodeResult.decoder.decodeLevel = 'none'; 52 | } 53 | 54 | return decodeResult; 55 | } 56 | } 57 | 58 | export default {}; 59 | -------------------------------------------------------------------------------- /lib/plugins/Label_QP.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_QP extends DecoderPlugin { 7 | name = 'label-qp'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['QP'], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}) : DecodeResult { 16 | const decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.formatted.description = 'OUT Report'; 19 | 20 | ResultFormatter.departureAirport(decodeResult, message.text.substring(0, 4)); 21 | ResultFormatter.arrivalAirport(decodeResult, message.text.substring(4, 8)); 22 | ResultFormatter.out(decodeResult, DateTimeUtils.convertHHMMSSToTod(message.text.substring(8, 12))); 23 | ResultFormatter.unknown(decodeResult, message.text.substring(12)); 24 | 25 | decodeResult.decoded = true; 26 | if(!decodeResult.remaining.text) 27 | decodeResult.decoder.decodeLevel = 'full'; 28 | else 29 | decodeResult.decoder.decodeLevel = 'partial'; 30 | 31 | return decodeResult; 32 | } 33 | } 34 | 35 | export default {}; 36 | -------------------------------------------------------------------------------- /lib/plugins/Label_QQ.ts: -------------------------------------------------------------------------------- 1 | import { DecoderPlugin } from '../DecoderPlugin'; 2 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 3 | import { CoordinateUtils } from '../utils/coordinate_utils'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | import { DateTimeUtils } from '../DateTimeUtils'; 6 | 7 | export class Label_QQ extends DecoderPlugin { 8 | name = 'label-qq'; 9 | 10 | qualifiers() { // eslint-disable-line class-methods-use-this 11 | return { 12 | labels: ['QQ'], 13 | }; 14 | } 15 | 16 | decode(message: Message, options: Options = {}) : DecodeResult { 17 | const decodeResult = this.defaultResult(); 18 | decodeResult.decoder.name = this.name; 19 | decodeResult.message = message; 20 | decodeResult.formatted.description = 'OFF Report'; 21 | 22 | ResultFormatter.departureAirport(decodeResult, message.text.substring(0, 4)); 23 | ResultFormatter.arrivalAirport(decodeResult, message.text.substring(4, 8)); 24 | 25 | if (message.text.substring(12, 19) === "\r\n001FE") { 26 | decodeResult.raw.day = message.text.substring(19, 21); 27 | ResultFormatter.off(decodeResult, DateTimeUtils.convertHHMMSSToTod(message.text.substring(21, 27))); 28 | let latdir = message.text.substring(27, 28); 29 | let latdeg = Number(message.text.substring(28, 30)); 30 | let latmin = Number(message.text.substring(30, 34)); 31 | let londir = message.text.substring(34, 35); 32 | let londeg = Number(message.text.substring(35, 38)); 33 | let lonmin = Number(message.text.substring(38, 42)); 34 | let pos = { 35 | latitude: (latdeg + latmin/60) * (latdir === 'N' ? 1 : -1), 36 | longitude: (londeg + lonmin/60) * (londir === 'E' ? 1 : -1), 37 | }; 38 | ResultFormatter.unknown(decodeResult, message.text.substring(42, 45)); 39 | ResultFormatter.position(decodeResult, pos); 40 | 41 | if (decodeResult.remaining.text !== "---") { 42 | ResultFormatter.groundspeed(decodeResult, Number(message.text.substring(45, 48))); 43 | } else { 44 | ResultFormatter.unknown(decodeResult, message.text.substring(45, 48)); 45 | } 46 | ResultFormatter.unknown(decodeResult, message.text.substring(48)); 47 | } else { 48 | ResultFormatter.off(decodeResult, DateTimeUtils.convertHHMMSSToTod(message.text.substring(8, 12) + "00")); 49 | ResultFormatter.unknown(decodeResult, message.text.substring(12)); 50 | } 51 | 52 | decodeResult.decoded = true; 53 | if(!decodeResult.remaining.text) 54 | decodeResult.decoder.decodeLevel = 'full'; 55 | else 56 | decodeResult.decoder.decodeLevel = 'partial'; 57 | 58 | return decodeResult; 59 | } 60 | } 61 | 62 | export default {}; 63 | -------------------------------------------------------------------------------- /lib/plugins/Label_QR.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_QR extends DecoderPlugin { 7 | name = 'label-qr'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['QR'], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}) : DecodeResult { 16 | const decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.formatted.description = 'ON Report'; 19 | 20 | ResultFormatter.departureAirport(decodeResult, message.text.substring(0, 4)); 21 | ResultFormatter.arrivalAirport(decodeResult, message.text.substring(4, 8)); 22 | ResultFormatter.on(decodeResult, DateTimeUtils.convertHHMMSSToTod(message.text.substring(8, 12))); 23 | ResultFormatter.unknown(decodeResult, message.text.substring(12)); 24 | 25 | decodeResult.decoded = true; 26 | if(!decodeResult.remaining.text) 27 | decodeResult.decoder.decodeLevel = 'full'; 28 | else 29 | decodeResult.decoder.decodeLevel = 'partial'; 30 | 31 | return decodeResult; 32 | } 33 | } 34 | 35 | export default {}; 36 | -------------------------------------------------------------------------------- /lib/plugins/Label_QS.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from '../DateTimeUtils'; 2 | import { DecoderPlugin } from '../DecoderPlugin'; 3 | import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; 4 | import { ResultFormatter } from '../utils/result_formatter'; 5 | 6 | export class Label_QS extends DecoderPlugin { 7 | name = 'label-qs'; 8 | 9 | qualifiers() { // eslint-disable-line class-methods-use-this 10 | return { 11 | labels: ['QS'], 12 | }; 13 | } 14 | 15 | decode(message: Message, options: Options = {}) : DecodeResult { 16 | const decodeResult = this.defaultResult(); 17 | decodeResult.decoder.name = this.name; 18 | decodeResult.formatted.description = 'IN Report'; 19 | 20 | ResultFormatter.departureAirport(decodeResult, message.text.substring(0, 4)); 21 | ResultFormatter.arrivalAirport(decodeResult, message.text.substring(4, 8)); 22 | ResultFormatter.in(decodeResult, DateTimeUtils.convertHHMMSSToTod(message.text.substring(8, 12))); 23 | ResultFormatter.unknown(decodeResult, message.text.substring(12)); 24 | 25 | decodeResult.decoded = true; 26 | if(!decodeResult.remaining.text) 27 | decodeResult.decoder.decodeLevel = 'full'; 28 | else 29 | decodeResult.decoder.decodeLevel = 'partial'; 30 | 31 | return decodeResult; 32 | } 33 | } 34 | 35 | export default {}; 36 | -------------------------------------------------------------------------------- /lib/plugins/official.ts: -------------------------------------------------------------------------------- 1 | export * from './Label_5Z_Slash'; 2 | export * from './Label_10_LDR'; 3 | export * from './Label_10_POS'; 4 | export * from './Label_10_Slash'; 5 | export * from './Label_12_N_Space'; 6 | export * from './Label_12_POS'; 7 | export * from './Label_13Through18_Slash'; 8 | export * from './Label_15'; 9 | export * from './Label_15_FST'; 10 | export * from './Label_16_N_Space'; 11 | export * from './Label_16_POSA1'; 12 | export * from './Label_16_TOD'; 13 | export * from './Label_1J_2J_FTX'; 14 | export * from './Label_1M_Slash'; 15 | export * from './Label_1L_3-line'; 16 | export * from './Label_1L_070'; 17 | export * from './Label_1L_660'; 18 | export * from './Label_1L_Slash'; 19 | export * from './Label_20_POS'; 20 | export * from './Label_21_POS'; 21 | export * from './Label_22_OFF'; 22 | export * from './Label_22_POS'; 23 | export * from './Label_2P_FM3'; 24 | export * from './Label_2P_FM4'; 25 | export * from './Label_2P_FM5'; 26 | export * from './Label_2P_POS'; 27 | export * from './Label_24_Slash'; 28 | export * from './Label_30_Slash_EA'; 29 | export * from './Label_44_ETA'; 30 | export * from './Label_44_IN'; 31 | export * from './Label_44_OFF'; 32 | export * from './Label_44_ON'; 33 | export * from './Label_44_POS'; 34 | export * from './Label_4A'; 35 | export * from './Label_4A_01'; 36 | export * from './Label_4A_DIS'; 37 | export * from './Label_4A_DOOR'; 38 | export * from './Label_4A_Slash_01'; 39 | export * from './Label_4J_POS'; 40 | export * from './Label_4N'; 41 | export * from './Label_4T_AGFSR'; 42 | export * from './Label_58'; 43 | export * from './Label_4T_ETA'; 44 | export * from './Label_80'; 45 | export * from './Label_83'; 46 | export * from './Label_8E'; 47 | export * from './Label_B6'; 48 | export * from './Label_ColonComma'; 49 | export * from './Label_H1'; 50 | export * from './Label_H1_FLR'; 51 | export * from './Label_H1_OHMA'; 52 | export * from './Label_H1_Slash'; 53 | export * from './Label_H1_StarPOS'; 54 | export * from './Label_H1_WRN'; 55 | export * from './Label_HX'; 56 | export * from './Label_SQ'; 57 | export * from './Label_QR'; 58 | export * from './Label_QP'; 59 | export * from './Label_QS'; 60 | export * from './Label_QQ'; 61 | -------------------------------------------------------------------------------- /lib/types/route.ts: -------------------------------------------------------------------------------- 1 | import { Waypoint } from "./waypoint"; 2 | 3 | /** 4 | * Representation of a route 5 | * 6 | * Typically a list of waypoints, this can also be a named company route. 7 | */ 8 | export interface Route { 9 | /** optional name. If not set, `waypoints` is required */ 10 | name?: string, 11 | 12 | /** optional runway */ 13 | runway?: string, 14 | 15 | /** optional list of waypoints. If not set, `name` is required */ 16 | waypoints?: Waypoint[], 17 | } -------------------------------------------------------------------------------- /lib/types/waypoint.ts: -------------------------------------------------------------------------------- 1 | import { CoordinateUtils } from "../utils/coordinate_utils"; 2 | 3 | /** 4 | * Represenation of a waypoint. 5 | * 6 | * Usually used in Routes which is an array of waypoints. 7 | * Airways/Jetways can also be represented as a waypoint, by just the name. 8 | * There is no distinction between the two currently because there is no difference from the current messages 9 | * Distinction must be determined from tne name. 10 | * In the event that a waypoint is a GPS position, the name can be the raw position string (N12345W012345) 11 | */ 12 | export interface Waypoint { 13 | /** unique identifier of the waypoint*/ 14 | name: string; 15 | /** 16 | * latitude in decimal degrees 17 | * 18 | * if set, longitude must be provided 19 | */ 20 | latitude?: number; 21 | /** longitude in decimal degrees 22 | * 23 | * if set, latitude must be provided 24 | */ 25 | longitude?: number; 26 | /** 27 | * time of arrival. If in future, it is an ETA. 28 | * 29 | * if set, timeFormat must be provided 30 | */ 31 | time?: number; 32 | /** 33 | * tod = 'Time of Day. seoconds since midnight', epoch = 'unix time. seconds since Jan 1, 1970 UTC' 34 | * 35 | * if set, time must be provided 36 | */ 37 | timeFormat?: 'tod' | 'epoch' 38 | 39 | /** 40 | * offset from the actual waypoint 41 | * 42 | * bearing: degrees from the waypoint 43 | * distance: distance in nautical miles 44 | */ 45 | offset?: {bearing: number, distance: number}; 46 | 47 | } -------------------------------------------------------------------------------- /lib/utils/coordinate_utils.ts: -------------------------------------------------------------------------------- 1 | export class CoordinateUtils { 2 | /** 3 | * Decode a string of coordinates into an object with latitude and longitude in millidegrees 4 | * @param stringCoords - The string of coordinates to decode 5 | * 6 | * @returns An object with latitude and longitude properties 7 | */ 8 | public static decodeStringCoordinates(stringCoords: string) : {latitude: number, longitude: number} | undefined{ // eslint-disable-line class-methods-use-this 9 | var results : any = {}; 10 | // format: N12345W123456 or N12345 W123456 11 | const firstChar = stringCoords.substring(0, 1); 12 | let middleChar = stringCoords.substring(6, 7); 13 | let longitudeChars = stringCoords.substring(7, 13); 14 | if (middleChar == ' ') { 15 | middleChar = stringCoords.substring(7, 8); 16 | longitudeChars = stringCoords.substring(8, 14); 17 | } 18 | if ((firstChar === 'N' || firstChar === 'S') && (middleChar === 'W' || middleChar === 'E')) { 19 | results.latitude = (Number(stringCoords.substring(1, 6)) / 1000) * CoordinateUtils.getDirection(firstChar); 20 | results.longitude = (Number(longitudeChars) / 1000) * CoordinateUtils.getDirection(middleChar); 21 | } else { 22 | return; 23 | } 24 | 25 | return results; 26 | } 27 | 28 | /** 29 | * Decode a string of coordinates into an object with latitude and longitude in degrees and decimal minutes 30 | * @param stringCoords - The string of coordinates to decode 31 | * 32 | * @returns An object with latitude and longitude properties 33 | */ 34 | public static decodeStringCoordinatesDecimalMinutes(stringCoords: string) : {latitude: number, longitude: number} | undefined{ // eslint-disable-line class-methods-use-this 35 | var results : any = {}; 36 | // format: N12345W123456 or N12345 W123456 37 | const firstChar = stringCoords.substring(0, 1); 38 | let middleChar = stringCoords.substring(6, 7); 39 | let longitudeChars = stringCoords.substring(7, 13); 40 | if (middleChar ==' ') { 41 | middleChar = stringCoords.substring(7, 8); 42 | longitudeChars = stringCoords.substring(8, 14); 43 | } 44 | const latDeg = Math.trunc(Number(stringCoords.substring(1, 6)) / 1000); 45 | const latMin = (Number(stringCoords.substring(1, 6)) % 1000) / 10; 46 | const lonDeg = Math.trunc(Number(longitudeChars) / 1000); 47 | const lonMin = (Number(longitudeChars) % 1000) / 10; 48 | 49 | if ((firstChar === 'N' || firstChar === 'S') && (middleChar === 'W' || middleChar === 'E')) { 50 | results.latitude = (latDeg + (latMin / 60)) * CoordinateUtils.getDirection(firstChar); 51 | results.longitude = (lonDeg + (lonMin / 60)) * CoordinateUtils.getDirection(middleChar); 52 | } else { 53 | return; 54 | } 55 | 56 | return results; 57 | } 58 | public static coordinateString(coords: {latitude: number, longitude: number}) : string { 59 | const latDir = coords.latitude > 0 ? 'N' : 'S'; 60 | const lonDir = coords.longitude > 0 ? 'E' : 'W'; 61 | return `${Math.abs(coords.latitude).toFixed(3)} ${latDir}, ${Math.abs(coords.longitude).toFixed(3)} ${lonDir}`; 62 | } 63 | 64 | public static getDirection(coord: string):number { 65 | if(coord.startsWith('N') || coord.startsWith('E')) { 66 | return 1; 67 | } else if(coord.startsWith('S') || coord.startsWith('W')) { 68 | return -1; 69 | } 70 | return NaN; 71 | } 72 | 73 | public static dmsToDecimalDegrees(degrees: number, minutes: number, seconds: number ) : number { 74 | return degrees + minutes / 60 + seconds / 3600; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/utils/route_utils.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeUtils } from "../DateTimeUtils"; 2 | import { Route } from "../types/route"; 3 | import { Waypoint } from "../types/waypoint"; 4 | import { CoordinateUtils } from "./coordinate_utils"; 5 | 6 | export class RouteUtils { 7 | 8 | public static formatFlightState(state: string): string { 9 | switch (state) { 10 | case "TO": return "Takeoff"; 11 | case "IC": return "Initial Climb"; 12 | case "CL": return "Climb"; 13 | case "ER": return "En Route"; 14 | case "DC": return "Descent"; 15 | case "AP": return "Approach"; 16 | default: return `Unknown ${state}`; 17 | } 18 | } 19 | 20 | public static routeToString(route: Route): string { 21 | let str = ''; 22 | if(route.name) { 23 | str += route.name; 24 | } 25 | if(route.runway) { 26 | str += `(${route.runway})`; 27 | } 28 | if(str.length!==0 && route.waypoints && route.waypoints.length === 1) { 29 | str += ' starting at ' 30 | } 31 | else if(str.length!==0 && route.waypoints) { 32 | str += ': '; 33 | } 34 | 35 | if(route.waypoints) { 36 | str += RouteUtils.waypointsToString(route.waypoints); 37 | } 38 | 39 | return str; 40 | } 41 | 42 | public static waypointToString(waypoint: Waypoint): string { 43 | let s = waypoint.name; 44 | if(waypoint.latitude && waypoint.longitude) { 45 | s += `(${CoordinateUtils.coordinateString({latitude:waypoint.latitude, longitude: waypoint.longitude})})`; 46 | } 47 | if(waypoint.offset) { 48 | s += `[${waypoint.offset.bearing}° ${waypoint.offset.distance}nm]`; 49 | } 50 | if(waypoint.time && waypoint.timeFormat) { 51 | s +=`@${DateTimeUtils.timestampToString(waypoint.time, waypoint.timeFormat)}`; 52 | } 53 | return s; 54 | } 55 | 56 | public static getWaypoint(leg: string): Waypoint { 57 | const regex = leg.match(/^([A-Z]+)(\d{3})-(\d{4})$/); // {name}{bearing}-{distance} 58 | if(regex?.length == 4) { 59 | return {name: regex[1], offset: {bearing: parseInt(regex[2]), distance: parseInt(regex[3])/10}}; 60 | } 61 | 62 | const waypoint = leg.split(','); 63 | if(waypoint.length ==2) { 64 | const position = CoordinateUtils.decodeStringCoordinatesDecimalMinutes(waypoint[1]); 65 | if(position) { 66 | return {name: waypoint[0], latitude: position.latitude, longitude: position.longitude}; 67 | } 68 | } 69 | if(leg.length == 13 || leg.length == 14) { //looks like coordinates 70 | const position = CoordinateUtils.decodeStringCoordinatesDecimalMinutes(leg); 71 | const name = waypoint.length == 2 ? waypoint[0] : ''; 72 | if(position) { 73 | return {name: name, latitude: position.latitude, longitude: position.longitude}; 74 | } 75 | } 76 | return {name: leg}; 77 | } 78 | 79 | private static waypointsToString(waypoints: Waypoint[]): string { 80 | let str = waypoints.map((x) => RouteUtils.waypointToString(x)).join( ' > ').replaceAll('> >', '>>'); 81 | if(str.startsWith(' > ')) { 82 | str = '>>' + str.slice(2); 83 | } 84 | return str; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@airframes/acars-decoder", 3 | "version": "1.6.19", 4 | "description": "ACARS Message Decoder for TypeScript/JavaScript", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "bin": { 12 | "acars-decoder": "./dist/bin/acars-decoder.js", 13 | "acars-decoder-test": "./dist/bin/acars-decoder-test.js" 14 | }, 15 | "scripts": { 16 | "build": "tsup", 17 | "test": "jest" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/airframesio/acars-decoder-typescript.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/airframesio/acars-decoder-typescript/issues" 25 | }, 26 | "homepage": "https://github.com/airframesio/acars-decoder-typescript#readme", 27 | "author": "Kevin Elliott ", 28 | "license": "UNLICENSED", 29 | "dependencies": { 30 | "@types/node": "^22.13.10", 31 | "base85": "^3.1.0", 32 | "minizlib": "^3.0.1" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.26.9", 36 | "@babel/preset-env": "^7.26.9", 37 | "@babel/preset-typescript": "^7.26.0", 38 | "@types/jest": "^29.5.14", 39 | "@types/minizlib": "^2.1.7", 40 | "babel-jest": "^29.7.0", 41 | "jest": "^29.7.0", 42 | "ts-jest": "^29.2.6", 43 | "ts-node": "^10.9.2", 44 | "tsup": "^8.4.0", 45 | "typescript": "^5.8.2" 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | }, 50 | "packageManager": "yarn@4.5.1" 51 | } 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esNext", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true 8 | }, 9 | "include": [ 10 | "lib/*.ts", 11 | "lib/**/*.ts", 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["index.ts"], 5 | format: ["cjs", "esm"], // Build for commonJS and ESmodules 6 | dts: true, // Generate declaration file (.d.ts) 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | }); 11 | --------------------------------------------------------------------------------