Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion lib/MessageDecoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ describe('MessageDecoder', () => {
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;\\<F`1mQ_(5_Z\\J01]+t9T9eu6ecjOlC7.H):6MAR4XuqGajJRp&=T3T7j1ipU;'tGF-f0nNn,XY\\/!!G&F*18E3l:kWakhBW\"b31<%oM6)jcY9:;p2\\5E'k3Yr1,d",
};

const decoder = new MessageDecoder();
const decodeResult = decoder.decode(message);

expect(decodeResult.decoded).toBe(true);
if (!decodeResult.message) {
expect(decodeResult.message).toBeDefined();
Expand All @@ -26,13 +26,33 @@ describe('MessageDecoder', () => {
expect(decodeResult.raw.text).toContain('A350,000354');
});

test('C-band core seamless decode', () => {
const message = {
label: '4N',
text: 'M85AUP0109285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3',
};

const decodeResult = decoder.decode(message);

expect(decodeResult.decoded).toBe(true);
if (!decodeResult.message) {
expect(decodeResult.message).toBeDefined();
return;
}
expect(decodeResult.message.label).toBe('4N');
expect(decodeResult.message.sublabel).toBeUndefined();
expect(decodeResult.message.text).toContain('M85AUP0109285');
expect(decodeResult.formatted.items.length).toBe(7);
});

test('Handles Multiple decodes', () => {
const message = {
label: 'H1',
text: 'POSN43312W123174,EASON,215754,370,EBINY,220601,ELENN,M48,02216,185/TS215754,0921227A40',
};

decoder.decode(message);

const decodeResult = decoder.decode(message);
if (!decodeResult.message) {
expect(decodeResult.message).toBeDefined();
Expand Down
9 changes: 5 additions & 4 deletions lib/MessageDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class MessageDecoder {
this.plugins = [];
this.debug = false;

this.registerPlugin(new Plugins.CBand(this)); // first, for now, so it can wrap other plugins
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

CBand wildcard plugin can recurse infinitely through MessageDecoder.decode.

CBand is always eligible (wildcard *), and its decode method calls this.decoder.decode(...) with the stripped text. If the stripped text also matches the C-band regex (/^[A-Z]\d{2}[A-Z][A-Z0-9]{2}\d{4}/), the inner decode call will again select CBand, strip another 10 characters, and recurse — potentially until a stack overflow.

Consider either:

  • Excluding CBand from the inner decode call (e.g., pass a flag in options like { skipCBand: true }), or
  • Having CBand invoke the label-specific plugins directly instead of going through MessageDecoder.decode.
#!/bin/bash
# Check how CBand.decode invokes the decoder to confirm recursion path
ast-grep --pattern $'class CBand {
  $$$
  decode($$$) {
    $$$
    this.decoder.decode($$$)
    $$$
  }
  $$$
}'
🤖 Prompt for AI Agents
In `@lib/MessageDecoder.ts` at line 19, CBand's decode causes infinite recursion
by always calling MessageDecoder.decode and being re-selected; modify
CBand.decode to call this.decoder.decode(strippedText, { ...options, skipCBand:
true }) (or similar flag) and update MessageDecoder.decode to honor
options.skipCBand by skipping selection of the CBand plugin (e.g., treat
Plugins.CBand as ineligible when options.skipCBand is true), ensuring CBand
remains registered but will not be picked for the inner decode; alternatively,
if you prefer the second approach, have CBand decode directly delegate to
specific label plugins instead of calling MessageDecoder.decode.

this.registerPlugin(new Plugins.Label_ColonComma(this));
this.registerPlugin(new Plugins.Label_5Z_Slash(this));
this.registerPlugin(new Plugins.Label_10_LDR(this));
Expand Down Expand Up @@ -89,19 +90,19 @@ export class MessageDecoder {
}

decode(message: Message, options: Options = {}): DecodeResult {
// console.log('All plugins');
// console.log(this.plugins);
const usablePlugins = this.plugins.filter((plugin) => {
const qualifiers = plugin.qualifiers();

if (qualifiers.labels.includes(message.label)) {
if (
qualifiers.labels.includes(message.label) ||
(qualifiers.labels.length === 1 && qualifiers.labels[0] === '*')
) {
if (qualifiers.preambles && qualifiers.preambles.length > 0) {
const matching = qualifiers.preambles.filter((preamble: string) => {
// console.log(message.text.substring(0, preamble.length));
// console.log(preamble);
return message.text.substring(0, preamble.length) === preamble;
});
// console.log(matching);
return matching.length >= 1;
} else {
return true;
Expand Down
106 changes: 106 additions & 0 deletions lib/plugins/CBand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { MessageDecoder } from '../MessageDecoder';
import { CBand } from './CBand';

describe('CBand', () => {
let plugin: CBand;

beforeEach(() => {
const decoder = new MessageDecoder();
plugin = new CBand(decoder);
});

test('matches qualifiers', () => {
expect(plugin.decode).toBeDefined();
expect(plugin.name).toBe('c-band');
expect(plugin.qualifiers).toBeDefined();
expect(plugin.qualifiers()).toEqual({
labels: ['*'],
});
});

test('decodes Label 4A', () => {
// https://app.airframes.io/messages/3461407615
const text =
'M60ALH0752N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111';
const decodeResult = plugin.decode({ label: '4A', text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('c-band-label-4a');
expect(decodeResult.formatted.description).toBe('Latest New Format');
expect(decodeResult.raw.flight_number).toBe('LH752');
expect(decodeResult.raw.position.latitude).toBe(22.456);
expect(decodeResult.raw.position.longitude).toBe(77.014);
expect(decodeResult.raw.route.waypoints.length).toBe(2);
expect(decodeResult.raw.route.waypoints[0].name).toBe('OSE35');
expect(decodeResult.raw.route.waypoints[1].name).toBe('VEX36');
expect(decodeResult.raw.outside_air_temperature).toBe(-46);
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.remaining.text).toBe('275043309,85220111');
});

test('decodes Label 4N variant 2C (C-band)', () => {
// https://app.airframes.io/messages/3422221702
const text =
'M85AUP0109285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3';
const decodeResult = plugin.decode({ label: '4N', text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('c-band-label-4n');
expect(decodeResult.formatted.description).toBe('Airline Defined');
expect(decodeResult.raw.flight_number).toBe('UP109');
expect(decodeResult.raw.date).toBe('10/12');
expect(decodeResult.raw.departure_icao).toBe('NRT');
expect(decodeResult.raw.arrival_icao).toBe('ANC');
expect(decodeResult.raw.alternate_icao).toBe('ANC');
expect(decodeResult.raw.arrival_runway).toBe('07R');
expect(decodeResult.raw.alternate_runway).toBe('33');
expect(decodeResult.raw.checksum).toBe(30195);
expect(decodeResult.remaining.text).toBe(
'C,0,0,0,0,0,0,1,0,0,0,709.8,048.7,758.5',
);
expect(decodeResult.formatted.items.length).toBe(7);
});

test('decodes Label 83 variant 1 (C-band)', () => {
// https://app.airframes.io/messages/3413113024
const text =
'M05AUA0007KIAH,RJAA,110012, 39.12,-175.10,39001,265,-107.6, 64900';
const decodeResult = plugin.decode({ label: '83', text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('c-band-label-83');
expect(decodeResult.formatted.description).toBe('Airline Defined');
expect(decodeResult.raw.flight_number).toBe('UA7');
expect(decodeResult.raw.departure_icao).toBe('KIAH');
expect(decodeResult.raw.arrival_icao).toBe('RJAA');
expect(decodeResult.raw.day).toBe('11');
expect(decodeResult.raw.position.latitude).toBe(39.12);
expect(decodeResult.raw.position.longitude).toBe(-175.1);
expect(decodeResult.raw.altitude).toBe(39001);
expect(decodeResult.raw.groundspeed).toBe(265);
expect(decodeResult.raw.heading).toBe(-107.6);
expect(decodeResult.remaining.text).toBe('64900');
expect(decodeResult.formatted.items.length).toBe(7);
});

test('decodes Label 83 variant 3 (C-band)', () => {
// https://app.airframes.io/messages/3413346742
const text = 'M09AXA0001001PR11013423N0556.6E11603.0000000----';
const decodeResult = plugin.decode({ label: '83', text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('c-band-label-83');
expect(decodeResult.formatted.description).toBe('Airline Defined');
expect(decodeResult.raw.flight_number).toBe('XA1');
expect(decodeResult.raw.day).toBe('11');
expect(decodeResult.raw.position.latitude).toBe(5.943333333333333);
expect(decodeResult.raw.position.longitude).toBe(116.05);
expect(decodeResult.raw.altitude).toBe(0);
expect(decodeResult.remaining.text).toBe('0----');
expect(decodeResult.formatted.items.length).toBe(3);
});
});
51 changes: 51 additions & 0 deletions lib/plugins/CBand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { DecoderPlugin } from '../DecoderPlugin';
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
import { MIAMCoreUtils } from '../utils/miam';
import { ResultFormatter } from '../utils/result_formatter';

export class CBand extends DecoderPlugin {
name = 'c-band';
qualifiers() {
return {
labels: ['*'],
};
}

decode(message: Message, options: Options = {}): DecodeResult {
let decodeResult = this.defaultResult();
decodeResult.decoder.name = this.name;
decodeResult.message = message;

// C-Band puts a 10 char header in front of some message types
// First 4 chars are some kind of message number
// Last 6 chars are the flight number
let cband = message.text.match(
/^(?<msgno>[A-Z]\d{2}[A-Z])(?<airline>[A-Z0-9]{2})(?<number>[0-9]{4})/,
);
if (cband?.groups) {
const messageText = message.text.substring(10);
const decoded = this.decoder.decode(
{
label: message.label,
sublabel: message.sublabel,
text: messageText,
},
options,
);
if (decoded.decoded) {
ResultFormatter.flightNumber(
decodeResult,
cband.groups.airline + Number(cband.groups.number),
);
decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = decoded.decoder.decodeLevel;
decodeResult.decoder.name = this.name + '-' + decoded.decoder.name;
decodeResult.raw = { ...decodeResult.raw, ...decoded.raw };
decodeResult.formatted.description = decoded.formatted.description;
decodeResult.formatted.items.push(...decoded.formatted.items);
decodeResult.remaining = decoded.remaining;
}
}
return decodeResult;
}
}
27 changes: 0 additions & 27 deletions lib/plugins/Label_4A.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,33 +90,6 @@ describe('Label 4A', () => {
expect(decodeResult.formatted.items[3].value).toBe('4 degrees');
});

test('decodes variant 2, C-Band', () => {
// https://app.airframes.io/messages/3461407615
message.text =
'M60ALH0752N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('label-4a');
expect(decodeResult.formatted.description).toBe('Latest New Format');
expect(decodeResult.message).toBe(message);
expect(decodeResult.remaining.text).toBe('275043309,85220111');
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].code).toBe('FLIGHT');
expect(decodeResult.formatted.items[0].value).toBe('LH752');
expect(decodeResult.formatted.items[1].code).toBe('POS');
expect(decodeResult.formatted.items[1].value).toBe('22.456 N, 77.014 E');
expect(decodeResult.formatted.items[2].code).toBe('ALT');
expect(decodeResult.formatted.items[2].value).toBe('37000 feet');
expect(decodeResult.formatted.items[3].code).toBe('ROUTE');
expect(decodeResult.formatted.items[3].value).toBe(
'OSE35@19:20:27 > VEX36@19:23:16',
);
expect(decodeResult.formatted.items[4].code).toBe('OATEMP');
expect(decodeResult.formatted.items[4].value).toBe('-46 degrees');
});

test('decodes variant 3', () => {
// https://globe.adsbexchange.com/?icao=A39AC6&showTrace=2024-09-22&timestamp=1727009085
message.text = '124442,1320, 138,33467,N 41.093,W 72.677';
Expand Down
19 changes: 3 additions & 16 deletions lib/plugins/Label_4A.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,8 @@ export class Label_4A extends DecoderPlugin {
decodeResult.message = message;
decodeResult.formatted.description = 'Latest New Format';

// Inmarsat C-band seems to prefix normal messages with a message number and flight number
let text = message.text;
if (text.match(/^M\d{2}A\w{6}/)) {
ResultFormatter.flightNumber(
decodeResult,
message.text.substring(4, 10).replace(/^([A-Z]+)0*/g, '$1'),
);
text = text.substring(10);
}

decodeResult.decoded = true;
const fields = text.split(',');
const fields = message.text.split(',');
if (fields.length === 11) {
// variant 1
ResultFormatter.time_of_day(
Expand All @@ -42,10 +32,7 @@ export class Label_4A extends DecoderPlugin {
if (fields[3]) ResultFormatter.callsign(decodeResult, fields[3]);
ResultFormatter.departureAirport(decodeResult, fields[4]);
ResultFormatter.arrivalAirport(decodeResult, fields[5]);
const alt = text.substring(48, 51);
if (alt !== '') {
ResultFormatter.altitude(decodeResult, Number(alt) * 100);
}
// ResultFormatter.altitude(decodeResult, Number(alt) * 100);
ResultFormatter.unknownArr(decodeResult, fields.slice(8));
Comment on lines +35 to 36
Copy link

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Altitude extraction removed for variant 1 — this is a regression.

The previous code extracted altitude from the text (likely via substring slicing on the prefix-stripped text). Commenting it out means variant 1 of Label 4A silently drops altitude from the decode result. If the altitude data is still present in one of the fields, it should be extracted from the appropriate field index instead.

🤖 Prompt for AI Agents
In `@lib/plugins/Label_4A.ts` around lines 35 - 36, Variant 1 of Label_4A removed
altitude extraction causing a regression; restore altitude parsing by extracting
the altitude from the correct fields entry and calling
ResultFormatter.altitude(decodeResult, parsedAltitude) before or instead of
ResultFormatter.unknownArr; locate where fields is defined in Label_4A.ts (the
same scope that calls ResultFormatter.unknownArr(decodeResult, fields.slice(8)))
and parse the altitude value from the appropriate fields[index] (convert to
Number and scale if previous logic used Number(alt) * 100), then call
ResultFormatter.altitude with the decoded altitude and keep unknownArr for
remaining fields.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was straight up wrong - it needs to be fixed

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@makrsmark, understood! I'll help identify where the altitude data is located in variant 1 and plan the fix.


🏁 Script executed:

#!/bin/bash
# Find test cases for Label_4A to understand the message format and field positions
rg -A 20 "Label_4A.test.ts" --type ts | head -100

# Look for variant 1 test messages with 11 fields
rg -P "label:\s*'4A'" lib/plugins/Label_4A.test.ts -A 10 -B 2

# Search for example messages to understand field structure
rg "fields.length === 11" lib/plugins/ -A 5 -B 5

Length of output: 979


🏁 Script executed:

#!/bin/bash
# Find actual test messages for Label_4A variant 1 (11 fields)
cat lib/plugins/Label_4A.test.ts

# Also check if there are any example messages in comments or documentation
rg "4A.*text:" lib/plugins/ -A 2 -B 2

# Look at the CBand test file which may have Label_4A examples
cat lib/plugins/CBand.test.ts | grep -A 5 "Label 4A"

Length of output: 8053


🏁 Script executed:

#!/bin/bash
# Check git history to see what the removed C-Band test case for Label_4A looked like
git log --all --full-history -p -- lib/plugins/Label_4A.test.ts | grep -A 30 -B 5 "variant 2, C-Band" | head -60

# Look at the commit that removed the C-Band test
git log --oneline -- lib/plugins/Label_4A.test.ts | head -5

# Show the actual diff that removed the C-Band test
git diff HEAD~3 HEAD -- lib/plugins/Label_4A.test.ts | head -100

Length of output: 3618


🏁 Script executed:

#!/bin/bash
# Get the full diff for Label_4A.ts to see exactly what altitude code was removed
git log --all -p -- lib/plugins/Label_4A.ts | grep -B 10 -A 10 "substring(48, 51)" | head -40

# Check if there were multiple variants with altitude in the old code
git show HEAD~1:lib/plugins/Label_4A.ts | grep -A 20 -B 5 "alt"

# Look at the complete old variant 1 code section
git show HEAD~1:lib/plugins/Label_4A.ts | sed -n '/if (fields.length === 11)/,/} else if/p'

Length of output: 4791


🏁 Script executed:

#!/bin/bash
# Check the CBand.test.ts file for all test messages
cat lib/plugins/CBand.test.ts

# Check what the CBand plugin does - see if it handles variant 1
cat lib/plugins/CBand.ts

Length of output: 6478


🏁 Script executed:

#!/bin/bash
# Search entire git history for C-Band variant 1 Label 4A tests
git log --all -p -- lib/plugins/Label_4A.test.ts | grep -B 5 -A 40 "variant 1.*C-Band"

# Check if there are any 11-field C-Band messages in the history
git log --all -p -- lib/plugins/Label_4A.test.ts | grep -B 2 -A 2 "^M.*,.*,.*,.*,.*,.*,.*,.*,.*,.*,"

# Look for the example message that would have position 48-51 as altitude
# Example: if message is M60ALH0752... after stripping M60ALH0752 (10 chars), check positions
echo "Analyzing C-Band Label 4A message structure:"
echo "M60ALH0752N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111"
echo "After removing prefix (M60ALH0752):"
echo "N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111"
echo "Position 48-51 in stripped text would be:"
python3 << 'EOF'
text = "N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111"
print(f"Character count: {len(text)}")
print(f"Position 48-51: '{text[48:51] if len(text) > 51 else 'OUT OF BOUNDS'}'")
print(f"This is variant 2 (6 fields), not variant 1 (11 fields)")

# Check what an 11-field message looks like
variant1_text = "063200,1910,.N343FR,FFT2028,KSLC,KORD,1,0632,RT0,LT0,"
print(f"\nVariant 1 message: {variant1_text}")
print(f"Length: {len(variant1_text)}")
print(f"Position 48-51: '{variant1_text[48:51] if len(variant1_text) > 51 else 'ONLY ' + str(len(variant1_text)) + ' chars'}'")
EOF

Length of output: 469


@makrsmark, I've analyzed the code and need clarification on the expected fix:

Current situation:

  • The old code used text.substring(48, 51) to extract altitude for variant 1 (11 fields)
  • This was commented out when C-Band handling was moved to a separate plugin
  • Current variant 1 test messages are only ~53 characters long, where position 48-51 would be ,LT (clearly not altitude)
  • The existing variant 1 tests don't expect any altitude in the decoded result

Analysis:
The substring-based altitude extraction appears to have been designed for C-Band messages (which had a 10-char prefix). However:

  1. There are no C-Band variant 1 tests (only C-Band variant 2 exists in CBand.test.ts)
  2. Regular variant 1 messages are too short for substring(48, 51) to extract meaningful altitude data

What should the fix be?

  1. Remove the commented line entirely (altitude was never valid for variant 1)?
  2. Extract altitude from one of the fields (if so, which field index contains altitude)?
  3. Something else I'm missing?

Can you clarify whether variant 1 should extract altitude, and if so, from which field?


🧬 Code Graph Analysis Results

lib/DecoderPluginInterface.ts

  • Snippet (complete):
    At line range 4-8:
    export interface Message {
      label: string;
      sublabel?: string;
      text: string;
    }
    
  • Snippet (complete):
    At line range 13-15:
    export interface Options {
      debug?: boolean;
    }
    
  • Snippet (complete):
    At line range 20-42:
    export interface DecodeResult {
      decoded: boolean;
      decoder: {
        name: string;
        type: 'pattern-match' | 'none';
        decodeLevel: 'none' | 'partial' | 'full';
      };
      error?: string;
      formatted: {
        description: string;
        items: {
          type: string;
          code: string;
          label: string;
          value: string;
        }[];
      };
      message?: Message;
      raw: any; // eslint-disable-line `@typescript-eslint/no-explicit-any`
      remaining: {
        text?: string;
      };
    }
    

lib/types/waypoint.ts

  • Snippet (complete):
    At line range 10-44:
    export interface Waypoint {
      /** unique identifier of the waypoint*/
      name: string;
      /**
       * latitude in decimal degrees
       *
       * if set, longitude must be provided
       */
      latitude?: number;
      /** longitude in decimal degrees
       *
       * if set, latitude must be provided
       */
      longitude?: number;
      /**
       * time of arrival. If in future, it is an ETA.
       *
       * if set, timeFormat must be provided
       */
      time?: number;
      /**
       * tod = 'Time of Day. seoconds since midnight', epoch = 'unix time. seconds since Jan 1, 1970 UTC'
       *
       * if set, time must be provided
       */
      timeFormat?: 'tod' | 'epoch';
    
      /**
       * offset from the actual waypoint
       *
       * bearing: degrees from the waypoint
       * distance: distance in nautical miles
       */
      offset?: { bearing: number; distance: number };
    }
    

lib/DateTimeUtils.ts

  • Snippet (complete):
    At line range 1-86:
    export class DateTimeUtils {
      // Expects a four digit UTC time string (HHMM)
      public static UTCToString(UTCString: string) {
        let utcDate = new Date();
        utcDate.setUTCHours(+UTCString.substr(0, 2), +UTCString.substr(2, 2), 0);
        return utcDate.toTimeString();
      }
    
      // Expects a six digit date string and a four digit UTC time string
      // (DDMMYY) (HHMM)
      public static UTCDateTimeToString(dateString: string, timeString: string) {
        let utcDate = new Date();
        utcDate.setUTCDate(+dateString.substr(0, 2));
        utcDate.setUTCMonth(+dateString.substr(2, 2));
        if (dateString.length === 6) {
          utcDate.setUTCFullYear(2000 + +dateString.substr(4, 2));
        }
        if (timeString.length === 6) {
          utcDate.setUTCHours(
            +timeString.substr(0, 2),
            +timeString.substr(2, 2),
            +timeString.substr(4, 2),
          );
        } else {
          utcDate.setUTCHours(
            +timeString.substr(0, 2),
            +timeString.substr(2, 2),
            0,
          );
        }
        return utcDate.toUTCString();
      }
    
      /**
       *
       * `@param` time HHMMSS or HHMM
       * `@returns` seconds since midnight
       */
      public static convertHHMMSSToTod(time: string): number {
        if (time.length === 4) {
          time += '00';
        }
        const h = Number(time.substring(0, 2));
        const m = Number(time.substring(2, 4));
        const s = Number(time.substring(4, 6));
        const tod = h * 3600 + m * 60 + s;
        return tod;
      }
    
      /**
       *
       * `@param` time HHMMSS
       * `@param` date MMDDYY or MMDDYYYY
       * `@returns` seconds since epoch
       */
      public static convertDateTimeToEpoch(time: string, date: string): number {
        //YYYY-MM-DDTHH:mm:ss.sssZ
        if (date.length === 6) {
          date = date.substring(0, 4) + `20${date.substring(4, 6)}`;
        }
        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`;
        const millis = Date.parse(timestamp);
        return millis / 1000;
      }
    
      /**
       * Converts a timestamp to a string
       *
       * ISO-8601 format for 'epoch'
       * HH:MM:SS for 'tod'
       * `@param` time
       * `@param` format
       * `@returns`
       */
      public static timestampToString(
        time: number,
        format: 'tod' | 'epoch',
      ): string {
        const date = new Date(time * 1000);
        if (format == 'tod') {
          return date.toISOString().slice(11, 19);
        }
        //strip off millis
        return date.toISOString().slice(0, -5) + 'Z';
      }
    }
    

lib/utils/coordinate_utils.ts

  • Snippet (complete):
    At line range 1-96:
    export class CoordinateUtils {
      /**
       * Decode a string of coordinates into an object with latitude and longitude in millidegrees
       * `@param` stringCoords - The string of coordinates to decode
       *
       * `@returns` An object with latitude and longitude properties
       */
      public static decodeStringCoordinates(
        stringCoords: string,
      ): { latitude: number; longitude: number } | undefined {
        // format: N12345W123456 or N12345 W123456
        const firstChar = stringCoords.substring(0, 1);
        let middleChar = stringCoords.substring(6, 7);
        let longitudeChars = stringCoords.substring(7, 13);
        if (middleChar == ' ') {
          middleChar = stringCoords.substring(7, 8);
          longitudeChars = stringCoords.substring(8, 14);
        }
        if (
          (firstChar === 'N' || firstChar === 'S') &&
          (middleChar === 'W' || middleChar === 'E')
        ) {
          return {
            latitude:
              (Number(stringCoords.substring(1, 6)) / 1000) *
              CoordinateUtils.getDirection(firstChar),
            longitude:
              (Number(longitudeChars) / 1000) *
              CoordinateUtils.getDirection(middleChar),
          };
        }
    
        return undefined;
      }
    
      /**
       * Decode a string of coordinates into an object with latitude and longitude in degrees and decimal minutes
       * `@param` stringCoords - The string of coordinates to decode
       *
       * `@returns` An object with latitude and longitude properties
       */
      public static decodeStringCoordinatesDecimalMinutes(
        stringCoords: string,
      ): { latitude: number; longitude: number } | undefined {
        // format: N12345W123456 or N12345 W123456
        const firstChar = stringCoords.substring(0, 1);
        let middleChar = stringCoords.substring(6, 7);
        let longitudeChars = stringCoords.substring(7, 13);
        if (middleChar == ' ') {
          middleChar = stringCoords.substring(7, 8);
          longitudeChars = stringCoords.substring(8, 14);
        }
        const latDeg = Math.trunc(Number(stringCoords.substring(1, 6)) / 1000);
        const latMin = (Number(stringCoords.substring(1, 6)) % 1000) / 10;
        const lonDeg = Math.trunc(Number(longitudeChars) / 1000);
        const lonMin = (Number(longitudeChars) % 1000) / 10;
    
        if (
          (firstChar === 'N' || firstChar === 'S') &&
          (middleChar === 'W' || middleChar === 'E')
        ) {
          return {
            latitude:
              (latDeg + latMin / 60) * CoordinateUtils.getDirection(firstChar),
            longitude:
              (lonDeg + lonMin / 60) * CoordinateUtils.getDirection(middleChar),
          };
        }
        return undefined;
      }
      public static coordinateString(coords: {
        latitude: number;
        longitude: number;
      }): string {
        const latDir = coords.latitude > 0 ? 'N' : 'S';
        const lonDir = coords.longitude > 0 ? 'E' : 'W';
        return `${Math.abs(coords.latitude).toFixed(3)} ${latDir}, ${Math.abs(coords.longitude).toFixed(3)} ${lonDir}`;
      }
    
      public static getDirection(coord: string): number {
        if (coord.startsWith('N') || coord.startsWith('E')) {
          return 1;
        } else if (coord.startsWith('S') || coord.startsWith('W')) {
          return -1;
        }
        return NaN;
      }
    
      public static dmsToDecimalDegrees(
        degrees: number,
        minutes: number,
        seconds: number,
      ): number {
        return degrees + minutes / 60 + seconds / 3600;
      }
    }
    

lib/utils/result_formatter.ts

  • Snippet (summary description):
    The Label_4A.ts file uses several methods from ResultFormatter to populate decoded results. Relevant functions (with parameters and effects) include:
    • route(decodeResult, route)
      • Stores route data in decodeResult.raw/decoded form and adds a formatted item with label "Aircraft Route".
    • position(decodeResult, value)
      • Validates value; if present, stores aircraft position and adds a formatted "Aircraft Position" item using CoordinateUtils.coordinateString.
    • altitude(decodeResult, value)
      • Validates numeric altitude; stores and formats as "Altitude" in feet.
    • callsign(decodeResult, value)
      • Stores callsign and formats "Callsign".
    • departureAirport(decodeResult, value, type = 'ICAO')
      • Stores origin airport (ICAO by default) or IATA depending on type; formats "Origin".
    • arrivalAirport(decodeResult, value, type = 'ICAO')
      • Stores destination airport (ICAO by default) or IATA; formats "Destination".
    • unknown(decodeResult, value, sep = ',')
      • Appends to remaining unparsed text.
    • unknownArr(decodeResult, valueArray, sep = ',')
      • Joins and forwards to unknown.
        These methods mutate decodeResult.raw and push descriptive items into decodeResult.formatted.items as appropriate.

lib/utils/result_formatter.ts

  • Snippet (complete file portion used by Label_4A):
    At line range 11-558:
    export class ResultFormatter {
      static route(decodeResult: DecodeResult, route: Route) {
        decodeResult.raw.route = route;
        decodeResult.formatted.items.push({
          type: 'aircraft_route',
          code: 'ROUTE',
          label: 'Aircraft Route',
          value: RouteUtils.routeToString(route),
        });
      }
    
      static state_change(decodeResult: DecodeResult, from: string, to: string) {
        decodeResult.raw.state_change = {
          from: from,
          to: to,
        };
        from = RouteUtils.formatFlightState(from);
        to = RouteUtils.formatFlightState(to);
        decodeResult.formatted.items.push({
          type: 'state_change',
          code: 'STATE_CHANGE',
          label: 'State Change',
          value: `${from} -> ${to}`,
        });
      }
    
      static freetext(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.freetext = value;
        decodeResult.formatted.items.push({
          type: 'freetext',
          code: 'FREE_TEXT',
          label: 'Free Text',
          value: value,
        });
      }
    
      static door_event(decodeResult: DecodeResult, name: string, state: string) {
        decodeResult.raw.door_event = {
          door: name,
          state: state,
        };
    
        decodeResult.formatted.items.push({
          type: 'door_event',
          code: 'DOOR',
          label: 'Door Event',
          value: `${name} ${state}`,
        });
      }
    
      static position(
        decodeResult: DecodeResult,
        value: { latitude: number; longitude: number } | undefined,
      ) {
        if (!value || isNaN(value.latitude) || isNaN(value.longitude)) {
          return;
        }
        decodeResult.raw.position = value;
        decodeResult.formatted.items.push({
          type: 'aircraft_position',
          code: 'POS',
          label: 'Aircraft Position',
          value: CoordinateUtils.coordinateString(value),
        });
      }
    
      static altitude(decodeResult: DecodeResult, value: number) {
        if (isNaN(value)) {
          return;
        }
        decodeResult.raw.altitude = value;
        decodeResult.formatted.items.push({
          type: 'altitude',
          code: 'ALT',
          label: 'Altitude',
          value: `${decodeResult.raw.altitude} feet`,
        });
      }
    
      static flightNumber(decodeResult: DecodeResult, value: string) {
        if (value.length === 0) {
          return;
        }
        decodeResult.raw.flight_number = value;
        decodeResult.formatted.items.push({
          type: 'flight_number',
          code: 'FLIGHT',
          label: 'Flight Number',
          value: decodeResult.raw.flight_number,
        });
      }
    
      static callsign(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.callsign = value;
        decodeResult.formatted.items.push({
          type: 'callsign',
          code: 'CALLSIGN',
          label: 'Callsign',
          value: decodeResult.raw.callsign,
        });
      }
    
      static departureAirport(
        decodeResult: DecodeResult,
        value: string,
        type: 'IATA' | 'ICAO' = 'ICAO',
      ) {
        if (type === 'ICAO') {
          decodeResult.raw.departure_icao = value;
          decodeResult.formatted.items.push({
            type: 'icao',
            code: 'ORG',
            label: 'Origin',
            value: value,
          });
        } else {
          decodeResult.raw.departure_iata = value;
          decodeResult.formatted.items.push({
            type: 'iata',
            code: 'ORG',
            label: 'Origin',
            value: value,
          });
        }
      }
    
      static departureRunway(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.departure_runway = value;
        decodeResult.formatted.items.push({
          type: 'runway',
          code: 'DEPRWY',
          label: 'Departure Runway',
          value: decodeResult.raw.departure_runway,
        });
      }
    
      static arrivalAirport(
        decodeResult: DecodeResult,
        value: string,
        type: 'IATA' | 'ICAO' = 'ICAO',
      ) {
        if (type === 'ICAO') {
          decodeResult.raw.arrival_icao = value;
          decodeResult.formatted.items.push({
            type: 'icao',
            code: 'DST',
            label: 'Destination',
            value: value,
          });
        } else {
          decodeResult.raw.arrival_iata = value;
          decodeResult.formatted.items.push({
            type: 'iata',
            code: 'DST',
            label: 'Destination',
            value: value,
          });
        }
      }
    
      static alternateAirport(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.alternate_icao = value;
        decodeResult.formatted.items.push({
          type: 'icao',
          code: 'ALT_DST',
          label: 'Alternate Destination',
          value: decodeResult.raw.alternate_icao,
        });
      }
    
      static eta(
        decodeResult: DecodeResult,
        time: number,
        type: 'tod' | 'epoch' = 'tod',
      ) {
        if (type === 'tod') {
          decodeResult.raw.eta_time = time;
          decodeResult.formatted.items.push({
            type: 'time_of_day',
            code: 'ETA',
            label: 'Estimated Time of Arrival',
            value: DateTimeUtils.timestampToString(time, 'tod'),
          });
        } else {
          decodeResult.raw.eta_date = time;
          decodeResult.formatted.items.push({
            type: 'epoch',
            code: 'ETA',
            label: 'Estimated Time of Arrival',
            value: DateTimeUtils.timestampToString(time, 'epoch'),
          });
        }
      }
    
      static arrivalRunway(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.arrival_runway = value;
        decodeResult.formatted.items.push({
          type: 'runway',
          code: 'ARWY',
          label: 'Arrival Runway',
          value: decodeResult.raw.arrival_runway,
        });
      }
    
      static alternateRunway(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.alternate_runway = value;
        decodeResult.formatted.items.push({
          type: 'runway',
          code: 'ALT_ARWY',
          label: 'Alternate Runway',
          value: decodeResult.raw.alternate_runway,
        });
      }
    
      static currentFuel(decodeResult: DecodeResult, value: number) {
        decodeResult.raw.fuel_on_board = value;
        decodeResult.formatted.items.push({
          type: 'fuel_on_board',
          code: 'FOB',
          label: 'Fuel On Board',
          value: decodeResult.raw.fuel_on_board.toString(),
        });
      }
    
      static burnedFuel(decodeResult: DecodeResult, value: number) {
        decodeResult.raw.fuel_burned = value;
        decodeResult.formatted.items.push({
          type: 'fuel_burned',
          code: 'FB',
          label: 'Fuel Burned',
          value: decodeResult.raw.fuel_burned.toString(),
        });
      }
    
      static remainingFuel(decodeResult: DecodeResult, value: number) {
        decodeResult.raw.fuel_remaining = value;
        decodeResult.formatted.items.push({
          type: 'fuel_remaining',
          code: 'FUEL',
          label: 'Fuel Remaining',
          value: decodeResult.raw.fuel_remaining.toString(),
        });
      }
    
      static checksum(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.checksum = Number('0x' + value);
        decodeResult.formatted.items.push({
          type: 'message_checksum',
          code: 'CHECKSUM',
          label: 'Message Checksum',
          value: '0x' + ('0000' + decodeResult.raw.checksum.toString(16)).slice(-4),
        });
      }
    
      static groundspeed(decodeResult: DecodeResult, value: number) {
        decodeResult.raw.groundspeed = value;
        decodeResult.formatted.items.push({
          type: 'aircraft_groundspeed',
          code: 'GSPD',
          label: 'Aircraft Groundspeed',
          value: `${decodeResult.raw.groundspeed} knots`,
        });
      }
    
      static airspeed(decodeResult: DecodeResult, value: number) {
        decodeResult.raw.airspeed = value;
        decodeResult.formatted.items.push({
          type: 'airspeed',
          code: 'ASPD',
          label: 'True Airspeed',
          value: `${decodeResult.raw.airspeed} knots`,
        });
      }
    
      static mach(decodeResult: DecodeResult, value: number) {
        decodeResult.raw.mach = value;
        decodeResult.formatted.items.push({
          type: 'mach',
          code: 'MACH',
          label: 'Mach Number',
          value: `${decodeResult.raw.mach} mach`,
        });
      }
    
      static temperature(decodeResult: DecodeResult, value: string) {
        if (value.length === 0) {
          return;
        }
        decodeResult.raw.outside_air_temperature = Number(
          value.replace('M', '-').replace('P', '+'),
        );
        decodeResult.formatted.items.push({
          type: 'outside_air_temperature',
          code: 'OATEMP',
          label: 'Outside Air Temperature (C)',
          value: `${decodeResult.raw.outside_air_temperature} degrees`,
        });
      }
    
      static heading(decodeResult: DecodeResult, value: number) {
        decodeResult.raw.heading = value;
        decodeResult.formatted.items.push({
          type: 'heading',
          code: 'HDG',
          label: 'Heading',
          value: `${decodeResult.raw.heading}`,
        });
      }
    
      static tail(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.tail = value;
        decodeResult.formatted.items.push({
          type: 'tail',
          code: 'TAIL',
          label: 'Tail',
          value: decodeResult.raw.tail,
        });
      }
    
      static out(decodeResult: DecodeResult, time: number) {
        decodeResult.raw.out_time = time;
        decodeResult.formatted.items.push({
          type: 'time_of_day',
          code: 'OUT',
          label: 'Out of Gate Time',
          value: DateTimeUtils.timestampToString(time, 'tod'),
        });
      }
    
      static off(
        decodeResult: DecodeResult,
        time: number,
        type: 'tod' | 'epoch' = 'tod',
      ) {
        if (type === 'tod') {
          decodeResult.raw.off_time = time;
          decodeResult.formatted.items.push({
            type: 'time_of_day',
            code: 'OFF',
            label: 'Takeoff Time',
            value: DateTimeUtils.timestampToString(time, 'tod'),
          });
        } else {
          decodeResult.raw.off_date = time;
          decodeResult.formatted.items.push({
            type: 'epoch',
            code: 'OFF',
            label: 'Takeoff Time',
            value: DateTimeUtils.timestampToString(time, 'epoch'),
          });
        }
      }
    
      static on(decodeResult: DecodeResult, time: number) {
        decodeResult.raw.on_time = time;
        decodeResult.formatted.items.push({
          type: 'time_of_day',
          code: 'ON',
          label: 'Landing Time',
          value: DateTimeUtils.timestampToString(time, 'tod'),
        });
      }
    
      static in(decodeResult: DecodeResult, time: number) {
        decodeResult.raw.in_time = time;
        decodeResult.formatted.items.push({
          type: 'time_of_day',
          code: 'IN',
          label: 'In Gate Time',
          value: DateTimeUtils.timestampToString(time, 'tod'),
        });
      }
    
      static time_of_day(decodeResult: DecodeResult, time: number) {
        decodeResult.raw.time_of_day = time;
        decodeResult.formatted.items.push({
          type: 'time_of_day',
          code: 'MSG_TOD',
          label: 'Message Timestamp',
          value: DateTimeUtils.timestampToString(time, 'tod'),
        });
      }
    
      static day(decodeResult: DecodeResult, day: number) {
        decodeResult.raw.day = day;
        decodeResult.formatted.items.push({
          type: 'day',
          code: 'MSG_DAY',
          label: 'Day of Month',
          value: `${day}`,
        });
      }
    
      static month(decodeResult: DecodeResult, month: number) {
        decodeResult.raw.month = month;
        decodeResult.formatted.items.push({
          type: 'month',
          code: 'MSG_MON',
          label: 'Month of Year',
          value: `${month}`,
        });
      }
    
      static departureDay(decodeResult: DecodeResult, day: number) {
        decodeResult.raw.departure_day = day;
        decodeResult.formatted.items.push({
          type: 'day',
          code: 'DEP_DAY',
          label: 'Departure Day',
          value: `${day}`,
        });
      }
    
      static arrivalDay(decodeResult: DecodeResult, day: number) {
        decodeResult.raw.arrival_day = day;
        decodeResult.formatted.items.push({
          type: 'day',
          code: 'ARR_DAY',
          label: 'Arrival Day',
          value: `${day}`,
        });
      }
    
      static text(decodeResult: DecodeResult, text: string) {
        decodeResult.raw.text = text;
        decodeResult.formatted.items.push({
          type: 'text',
          code: 'TEXT',
          label: 'Text Message',
          value: text,
        });
      }
    
      static mac(decodeResult: DecodeResult, mac: number) {
        decodeResult.raw.mac = mac;
        decodeResult.formatted.items.push({
          type: 'mac',
          code: 'MAC',
          label: 'Mean Aerodynamic Chord',
          value: `${mac} %`,
        });
      }
    
      static trim(decodeResult: DecodeResult, trim: number) {
        decodeResult.raw.trim = trim;
        decodeResult.formatted.items.push({
          type: 'trim',
          code: 'TRIM',
          label: 'Trim',
          value: `${trim} units`,
        });
      }
    
      static windData(decodeResult: DecodeResult, windData: Wind[]) {
        decodeResult.raw.wind_data = windData;
        for (const wind of windData) {
          let text = `${RouteUtils.waypointToString(wind.waypoint)} at FL${wind.flightLevel}: ${wind.windDirection}° at ${wind.windSpeed}kt`;
          if (wind.temperature) {
            text += `, ${wind.temperature.degreesC}°C at FL${wind.temperature.flightLevel}`;
          }
          decodeResult.formatted.items.push({
            type: 'wind_data',
            code: 'WIND',
            label: 'Wind Data',
            value: text,
          });
        }
      }
    
      static cg(
        decodeResult: DecodeResult,
        value: number,
        type: 'center' | 'lower' | 'upper' = 'center',
      ) {
        switch (type) {
          case 'center':
            decodeResult.raw.center_of_gravity = value;
            decodeResult.formatted.items.push({
              type: 'center_of_gravity',
              code: 'CG',
              label: 'Center of Gravity',
              value: `${decodeResult.raw.center_of_gravity} %`,
            });
            break;
          case 'lower':
            decodeResult.raw.cg_lower_limit = value;
            decodeResult.formatted.items.push({
              type: 'cg_lower_limit',
              code: 'CG_LOWER',
              label: 'Center of Gravity Lower Limit',
              value: `${decodeResult.raw.cg_lower_limit} %`,
            });
            break;
          case 'upper':
            decodeResult.raw.cg_upper_limit = value;
            decodeResult.formatted.items.push({
              type: 'cg_upper_limit',
              code: 'CG_UPPER',
              label: 'Center of Gravity Upper Limit',
              value: `${decodeResult.raw.cg_upper_limit} %`,
            });
            break;
        }
      }
    
      static version(decodeResult: DecodeResult, value: number) {
        decodeResult.raw.version = value;
        decodeResult.formatted.items.push({
          type: 'version',
          code: 'VERSION',
          label: 'Message Version',
          value: `v${decodeResult.raw.version.toFixed(1)}`,
        });
      }
    
      static label(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.label = value;
        decodeResult.formatted.items.push({
          type: 'label',
          code: 'LABEL',
          label: 'Message Label',
          value: `${decodeResult.raw.label}`,
        });
      }
    
      static sublabel(decodeResult: DecodeResult, value: string) {
        decodeResult.raw.sublabel = value;
        decodeResult.formatted.items.push({
          type: 'sublabel',
          code: 'SUBLABEL',
          label: 'Message Sublabel',
          value: `${decodeResult.raw.sublabel}`,
        });
      }
    
      static unknown(decodeResult: DecodeResult, value: string, sep: string = ',') {
        if (!decodeResult.remaining.text) decodeResult.remaining.text = value;
        else decodeResult.remaining.text += sep + value;
      }
    
      static unknownArr(
        decodeResult: DecodeResult,
        value: string[],
        sep: string = ',',
      ) {
        this.unknown(decodeResult, value.join(sep), sep);
      }
    }
    

} else if (fields.length === 6) {
if (fields[0].match(/^[NS]/)) {
Expand Down Expand Up @@ -92,7 +79,7 @@ export class Label_4A extends DecoderPlugin {
}
} else {
decodeResult.decoded = false;
ResultFormatter.unknown(decodeResult, text);
ResultFormatter.unknown(decodeResult, message.text);
}

if (decodeResult.decoded) {
Expand Down
33 changes: 0 additions & 33 deletions lib/plugins/Label_4N.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,39 +103,6 @@ describe('Label 4N', () => {
expect(decodeResult.formatted.items[4].value).toBe('0x9bcd');
});

test('decodes Label 4N variant 2C (C-band)', () => {
// https://app.airframes.io/messages/3422221702
message.text =
'M85AUP0109285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('label-4n');
expect(decodeResult.formatted.description).toBe('Airline Defined');
expect(decodeResult.message).toBe(message);
expect(decodeResult.raw.flight_number).toBe('UP109');
expect(decodeResult.raw.date).toBe('10/12');
expect(decodeResult.remaining.text).toBe(
'C,0,0,0,0,0,0,1,0,0,0,709.8,048.7,758.5',
);
expect(decodeResult.formatted.items.length).toBe(7);
expect(decodeResult.formatted.items[0].code).toBe('FLIGHT');
expect(decodeResult.formatted.items[0].value).toBe('UP109');
expect(decodeResult.formatted.items[1].code).toBe('ORG');
expect(decodeResult.formatted.items[1].value).toBe('NRT');
expect(decodeResult.formatted.items[2].code).toBe('DST');
expect(decodeResult.formatted.items[2].value).toBe('ANC');
expect(decodeResult.formatted.items[3].code).toBe('ALT_DST');
expect(decodeResult.formatted.items[3].value).toBe('ANC');
expect(decodeResult.formatted.items[4].code).toBe('ARWY');
expect(decodeResult.formatted.items[4].value).toBe('07R');
expect(decodeResult.formatted.items[5].code).toBe('ALT_ARWY');
expect(decodeResult.formatted.items[5].value).toBe('33');
expect(decodeResult.formatted.items[6].code).toBe('CHECKSUM');
expect(decodeResult.formatted.items[6].value).toBe('0x75f3');
});

test('decodes Label 4N <invalid>', () => {
message.text = '4N Bogus message';
const decodeResult = plugin.decode(message);
Expand Down
11 changes: 1 addition & 10 deletions lib/plugins/Label_4N.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,8 @@ export class Label_4N extends DecoderPlugin {
decodeResult.message = message;
decodeResult.formatted.description = 'Airline Defined';

// Inmarsat C-band seems to prefix normal messages with a message number and flight number
let text = message.text;
if (text.match(/^M\d{2}A\w{6}/)) {
ResultFormatter.flightNumber(
decodeResult,
message.text.substring(4, 10).replace(/^([A-Z]+)0*/g, '$1'),
);
text = text.substring(10);
}

decodeResult.decoded = true;
const text = message.text;
const fields = text.split(',');
if (text.length === 51) {
// variant 1
Expand Down
Loading