Skip to content

pull up c-band prefix decoding#174

Open
rpatel3001 wants to merge 11 commits intoairframesio:masterfrom
rpatel3001:cband_prefix
Open

pull up c-band prefix decoding#174
rpatel3001 wants to merge 11 commits intoairframesio:masterfrom
rpatel3001:cband_prefix

Conversation

@rpatel3001
Copy link
Contributor

@rpatel3001 rpatel3001 commented Oct 28, 2024

doing it this way means using top level MessageDecoder instance in all the tests instead of an instance of the specific plugin being tested. if that's ok i can update the rest to match (and check if the C-Band prefix test matches any messages it shouldn't).

Summary by CodeRabbit

  • New Features

    • Added C-Band message decoder plugin to support decoding of C-Band formatted aviation messages across multiple label types.
  • Bug Fixes

    • Removed direct console logging from the message decoding flow to reduce logging overhead.
  • Tests

    • Added comprehensive test suite validating C-Band decoder functionality and message parsing accuracy.
    • Updated existing decoder tests to reflect changes in message processing logic.

// https://app.airframes.io/messages/3452310240
const text = '101606,1910,.N317FR,,KMDW,----,1,1016,RT0,LT1,';
const decodeResult = decoderPlugin.decode({ text: text });
const decodeResult = decoder.decode({ label: "4A", text: text });
Copy link
Collaborator

Choose a reason for hiding this comment

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

this isn't a bad idea, but it's going to make the PR huge. Do we want a separate PR that just makes this change? Or do we want to move the c-band messages to the MessageDecoder suite? I'm leaning towards moving the c-band tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moving the c band message tests makes sense with two drawbacks:

  1. Need to remember to add tests in a second location for types with this prefix
  2. More importantly, there's no way to check if the cband prefix accidentally catches non cband messages and incorrectly truncates the message that it passes into a plugin.

Copy link
Collaborator

Choose a reason for hiding this comment

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

do we have messages (minus the prefix) that are unique to c-band? If not, i don't think we need to have c-band label-specific tests other than a couple to test the prefix extraction.

As for the second point, idk. Is it worth slowing down the test suite by making every test run through the plugin selection code just so we can verify this edge case that we don't even know exists?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure about any cband specific messages, but the regex for the prefix is pretty permissive. i would be surprised if it never matched any non cband messages. maybe it would be better to send more metadata about the source of the message into the decoders and just tell it explicitly if it's cband?

or we could try to decode as cband if the prefix is detected then retry with the prefix still attached if it fails to decode. that wouldn't necessarily catch every edge case though

Copy link
Collaborator

Choose a reason for hiding this comment

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

@kevinelliott and @andermatt64 - Do you have any opinions here?

@rpatel3001 rpatel3001 force-pushed the cband_prefix branch 2 times, most recently from 84e959e to cf15587 Compare October 30, 2024 23:21
@makrsmark makrsmark force-pushed the master branch 3 times, most recently from 3447c21 to 1e877a4 Compare November 20, 2024 11:03
@rpatel3001 rpatel3001 force-pushed the cband_prefix branch 2 times, most recently from ad6e34a to da1033b Compare November 24, 2024 04:25
@kevinelliott
Copy link
Contributor

@rpatel3001 @andermatt64 @makrsmark Any further thoughts here? It would be good to get this one finished up.

@andermatt64
Copy link
Contributor

@rpatel3001 @andermatt64 @makrsmark Any further thoughts here? It would be good to get this one finished up.

I think I'm ok with the airline+number regex if historical C-band messages can be consistently caught with it.

@rpatel3001
Copy link
Contributor Author

last thing to resolve is the conversation about how to do c-band tests: make each plugin test use decoder instead of decoderPlugin or do a handful of c-band tests as part of the MessageDecoder test suite.

@kevinelliott
Copy link
Contributor

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2025

Warning

Rate limit exceeded

@makrsmark has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 49 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

This PR introduces a new CBand decoder plugin that wraps existing label decoders (4A, 4N, 83). It extracts a 10-character C-band header from messages and delegates remaining text decoding to the appropriate inner decoder plugin. Simultaneously, C-band specific prefix-handling logic is removed from Label_4A, Label_4N, and Label_83 plugins, consolidating that functionality into the standalone CBand wrapper.

Changes

Cohort / File(s) Summary
Core CBand Plugin Implementation
lib/plugins/CBand.ts, lib/plugins/CBand.test.ts
New CBand decoder plugin that parses 10-character header (msgno, airline, number), delegates inner decode to wrapped plugins, and formats flight number. Includes comprehensive test coverage for Labels 4A, 4N, and 83 variants.
MessageDecoder Enhancement
lib/MessageDecoder.ts, lib/MessageDecoder.test.ts
Registered CBand plugin first to wrap other plugins; expanded decode eligibility to accept wildcard (*) labels; removed internal console logging. Added test case validating C-band seamless decode flow.
Label_4A Plugin Refactor
lib/plugins/Label_4A.ts, lib/plugins/Label_4A.test.ts
Removed prefix detection (M\d{2}A\w{6}) and flight number extraction; now directly uses message.text. Removed C-Band variant 2 test case (now handled by CBand wrapper).
Label_4N Plugin Refactor
lib/plugins/Label_4N.ts, lib/plugins/Label_4N.test.ts
Removed Inmarsat prefix handling; replaced intermediate text variable with direct message.text references. Removed variant 2C (C-band) test case (delegated to CBand plugin).
Label_83 Plugin Refactor
lib/plugins/Label_83.ts, lib/plugins/Label_83.test.ts
Removed optional prefix pre-processing (MXXAXXXXXX); unified all variant dispatch to use message.text directly. Removed C-band variant tests (variant 1 and 3) now covered by CBand wrapper.
Plugin Export
lib/plugins/official.ts
Added CBand plugin re-export to public API surface.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MessageDecoder
    participant CBand
    participant InnerDecoder as Label Plugin<br/>(4A/4N/83)
    participant Message

    Client->>MessageDecoder: decode(Message with C-band)
    MessageDecoder->>CBand: decode(Message)
    CBand->>Message: extract header<br/>(10 chars)
    CBand->>CBand: parse msgno, airline, number
    CBand->>InnerDecoder: decode(remaining text)
    InnerDecoder->>InnerDecoder: parse fields
    InnerDecoder-->>CBand: DecodeResult
    CBand->>CBand: format flight_number<br/>merge fields
    CBand-->>MessageDecoder: DecodeResult with<br/>c-band prefix
    MessageDecoder-->>Client: final DecodeResult
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • andermatt64
  • kevinelliott

Poem

🐰 A band wrapped in C, the decoders now free,
Clean headers extracted, no prefix to flee!
Four labels unite with a wrapper so bright,
The rabbit hops sideways to code that feels right. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'pull up c-band prefix decoding' accurately describes the main change: introducing a CBand plugin that handles C-band prefix decoding at the MessageDecoder level.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c20459 and fe1cdc7.

📒 Files selected for processing (9)
  • lib/MessageDecoder.ts (3 hunks)
  • lib/plugins/Label_1L_Slash.ts (1 hunks)
  • lib/plugins/Label_30_Slash_EA.test.ts (1 hunks)
  • lib/plugins/Label_4A.test.ts (6 hunks)
  • lib/plugins/Label_4A.ts (2 hunks)
  • lib/plugins/Label_4N.test.ts (5 hunks)
  • lib/plugins/Label_4N.ts (2 hunks)
  • lib/plugins/Label_83.test.ts (6 hunks)
  • lib/plugins/Label_83.ts (2 hunks)
🔇 Additional comments (20)
lib/plugins/Label_1L_Slash.ts (1)

83-83: Good formatting change.

Adding a newline at the end of the file is a good practice that helps meet common coding standards and avoids "No newline at end of file" warnings from linters.

lib/plugins/Label_4A.ts (2)

24-24: Improved code by removing intermediate variable.

Directly using message.text.split(",") instead of an intermediate variable simplifies the code and aligns with how the other decoders have been refactored.


62-62: Consistent use of message.text.

Using message.text directly in the error case maintains consistency with other changes and the decoder pattern being implemented throughout the codebase.

lib/MessageDecoder.ts (3)

5-5: Required import added for the new functionality.

The ResultFormatter import is necessary for the new C-Band prefix handling that uses ResultFormatter.flightNumber().


99-105: Well-implemented C-Band prefix detection and extraction.

The implementation follows a clear pattern:

  1. Identify messages with a 10-character C-Band header using a descriptive regex
  2. Extract the message number, airline code, and flight number
  3. Temporarily remove the prefix from the message text for downstream processing

The regex pattern ^(?<msgno>[A-Z]\d{2}[A-Z])(?<airline>[A-Z0-9]{2})(?<number>[0-9]{4}) captures the structure documented in previous discussions about C-Band prefixes.


162-165: Good restoration and enrichment of message data.

After processing the message with plugins, the implementation:

  1. Properly checks if a C-Band prefix was detected
  2. Adds the flight number information using ResultFormatter.flightNumber()
  3. Restores the original message text with the prefix

This approach preserves the original message while enriching the decoded result with flight information.

lib/plugins/Label_30_Slash_EA.test.ts (1)

17-17: Test updated to use MessageDecoder instead of plugin instance directly.

This change aligns with the PR objective of using a top-level MessageDecoder instance across all tests. The test now passes both the label and text to the decoder, which better matches the actual usage pattern in the application.

lib/plugins/Label_4N.test.ts (3)

17-17: Refactoring to use MessageDecoder directly

This change aligns with the PR objective of using a top-level MessageDecoder instance for tests instead of individual plugin instances. This approach is more consistent with how the decoder would be used in production.


42-42: Consistent use of MessageDecoder across all test cases

All test cases have been updated to use the MessageDecoder instance directly, providing a consistent testing approach throughout the file.

Also applies to: 75-75, 102-102, 133-133


113-126: Updated expectations for C-band message format

The order of the formatted items has been adjusted, with the flight number now appearing as the last item in the array. This change reflects the new structure where the MessageDecoder handles C-band prefix decoding rather than the individual plugin.

lib/plugins/Label_83.ts (1)

23-23: Direct use of message.text instead of intermediate variable

The code now directly references message.text instead of using a local variable. This simplification is cleaner and reduces unnecessary variable assignments, which is a positive change that helps maintain consistency across plugins.

Also applies to: 25-26, 35-35, 37-38, 42-43

lib/plugins/Label_4N.ts (1)

22-31: Simplified message text handling

Similar to the changes in Label_83.ts, this plugin now directly uses message.text instead of an intermediate variable. This simplification makes the code more straightforward and aligns with the architectural change to move C-band prefix handling to the MessageDecoder.

Also applies to: 49-49

lib/plugins/Label_83.test.ts (2)

17-17: Consistent testing methodology using MessageDecoder

All test cases now use the MessageDecoder instance directly for decoding, passing both the label and text. This approach is more consistent with real-world usage and aligns with the PR objective.

Also applies to: 53-53, 92-92, 121-121, 145-145, 171-171


71-85: Updated item order in formatted results for C-band messages

The expectations for C-band message test cases have been updated to reflect the new structure where flight number appears as a separate item. This is consistent with the changes in Label_4N.test.ts and reflects the architectural change where the MessageDecoder handles C-band prefix decoding.

Also applies to: 159-165

lib/plugins/Label_4A.test.ts (6)

21-21: Methodology update aligns with PR objectives

This change moves from direct plugin invocation to using the top-level MessageDecoder, aligning with the PR's goal of centralizing the C-band prefix decoding.


47-47: Centralized decoder approach matches PR objectives

Switching to the top-level decoder instance is consistent with the discussions in previous reviews about improving the C-band prefix handling.


55-55: Ensure item count reduction is intentional

Similar to line 29, the expected item count has been reduced from 5 to 4. This appears to be related to how the MessageDecoder now processes these messages.


71-71: Consistent methodology across all test cases

The change to use the top-level decoder is applied consistently across all test variants, which is good for maintainability and aligns with the PR objectives.

Also applies to: 95-95, 121-121


29-29:

Details

✅ Verification successful

Verify the expected item count reduction

The expected length of formatted items has been reduced from 6 to 5. This change appears to be related to how the message is now being processed by the central decoder.


🏁 Script executed:

#!/bin/bash
# Check if any similar tests were modified to reduce expected item counts
grep -n "expect.*formatted.items.length" lib/plugins/*.test.ts | grep -B 1 -A 1 "toBe"

Length of output: 16474


Central Decoder Update: Expected Item Count Reduction Confirmed

The change in expected item count—from 6 to 5—in lib/plugins/Label_4A.test.ts (line 29) is consistent with the updates observed across similar tests in the repository. The grep results confirm that the new expected value (5) reflects the revised central decoder processing logic. No additional changes are needed here.


104-114:

Details

✅ Verification successful

C-Band flight information now appears at the end

The order of formatted items in the C-Band test case has changed, with the 'FLIGHT' item now placed at the end of the array. This likely reflects how the central decoder now processes C-Band messages with flight information.


🏁 Script executed:

#!/bin/bash
# Check how the MessageDecoder is handling flight information for C-Band messages
cat lib/MessageDecoder.ts | grep -A 30 "decode" | grep -A 30 "flight"

Length of output: 1814


Flight Information Order Change Validated

The updated test snapshot in lib/plugins/Label_4A.test.ts now correctly expects the flight information (FLIGHT) to appear as the last item in the formatted items array. Verification via the MessageDecoder.ts snippet confirms that the flight number is appended—via the ResultFormatter.flightNumber call—after processing the other fields, which aligns with the expected behavior.

No further modifications are required.

@rpatel3001
Copy link
Contributor Author

revisiting this, I think I agree that adding c-band tests to each message type is perhaps too much and so it would be better to have a c-band specific test file. i can pull them out and revert the existing tests in the next couple days

@kevinelliott
Copy link
Contributor

@makrsmark We might have copilot or Claude rework this a bit.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@lib/DecoderPluginInterface.ts`:
- Around line 4-8: You've made Message.label required which breaks public API:
update every decode(...) call site to pass a label string along with text (and
optional sublabel) and add a short migration note to CHANGELOG.md describing the
breaking change and the required call shape; search for all usages of decode and
replace calls like decode({ text: ... }) with calls that include label (e.g.,
provide a meaningful label string), ensure tests are updated (150+ test files),
and include an example call pattern and rationale in the CHANGELOG entry under
Breaking Changes.

In `@lib/plugins/CBand.ts`:
- Around line 22-37: The flight number construction currently uses
Number(cband.groups.number) which strips leading zeros; instead preserve the
numeric segment as a string when calling ResultFormatter.flightNumber. Update
the code that builds the flight number (the call in the CBand plugin where cband
is matched and ResultFormatter.flightNumber(...) is invoked) to concatenate
cband.groups.airline with cband.groups.number (as a string) rather than
converting the number via Number(...), so the resulting flight string in
decodeResult retains any leading zeros.
🧹 Nitpick comments (1)
lib/plugins/CBand.test.ts (1)

22-40: Minor: Inconsistent indentation on line 39.

Line 39 has extra indentation compared to other expect statements in this test block.

🔧 Suggested fix
   expect(decodeResult.formatted.items.length).toBe(5);
-    expect(decodeResult.remaining.text).toBe('275043309,85220111');
+  expect(decodeResult.remaining.text).toBe('275043309,85220111');
 });

Comment on lines 22 to 37
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);
Copy link

@coderabbitai coderabbitai bot Feb 5, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Preserve leading zeros in the C‑Band flight number.

Number(cband.groups.number) will drop leading zeros (e.g., “0109” → “109”). If those zeros are meaningful, keep the numeric segment as a string.

🔧 Suggested fix
-          ResultFormatter.flightNumber(decodeResult, cband.groups.airline + Number(cband.groups.number));
+          ResultFormatter.flightNumber(decodeResult, cband.groups.airline + cband.groups.number);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
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 + 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);
🤖 Prompt for AI Agents
In `@lib/plugins/CBand.ts` around lines 22 - 37, The flight number construction
currently uses Number(cband.groups.number) which strips leading zeros; instead
preserve the numeric segment as a string when calling
ResultFormatter.flightNumber. Update the code that builds the flight number (the
call in the CBand plugin where cband is matched and
ResultFormatter.flightNumber(...) is invoked) to concatenate
cband.groups.airline with cband.groups.number (as a string) rather than
converting the number via Number(...), so the resulting flight string in
decodeResult retains any leading zeros.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good question - do we want XA1 or XA0001 ?

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, let me check the test expectations and existing patterns in the codebase to help answer this question.


🏁 Script executed:

#!/bin/bash
# Check the CBand test file to see what format is expected
cat lib/plugins/CBand.test.ts | grep -A5 -B5 "flight_number"

# Check other test files for flight number formatting patterns
rg -n "flight_number.*value" lib/plugins/*.test.ts -A1 -B1

# Look for any other instances where flight numbers are formatted
rg -n "ResultFormatter\.flightNumber" lib/plugins/ -A1 -B1

Length of output: 6091


🏁 Script executed:

#!/bin/bash
# Check the actual test message strings to see the raw C-Band format
cat lib/plugins/CBand.test.ts | grep -E "text:.*M[0-9]" -A2 -B2

# Also check if there are any comments or documentation about C-Band format
rg -n "C-band\|C-Band\|CBAND" lib/ -A3 -B1 --type ts | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# View the CBand test file more directly
cat lib/plugins/CBand.test.ts

# Also check for any documentation about flight number formatting
rg -n "flight.number" lib/ --type ts -C2 | head -40

Length of output: 7276


@makrsmark, based on the test data and codebase analysis:

The tests expect XA1 (stripped format):

Looking at the CBand test file, here's what the raw C-Band messages contain vs. what's expected:

Raw C-Band Format Expected Flight Number
UA0007 UA7
XA0001 XA1
UP0109 UP109
LH0752 LH752

All current tests expect leading zeros to be stripped, which matches standard aviation display conventions. In the aviation industry, flight numbers are typically displayed without leading zeros (e.g., "United 7" is written as "UA7", not "UA0007").

However, this depends on your use case:

  • For display/human consumption: XA1 (current behavior with Number())
  • For preserving raw C-Band format: XA0001 (using string concatenation)

If you need to maintain the exact 4-digit format as it appears in the C-Band transmission for data integrity or downstream processing, you should keep it as a string. Otherwise, the current Number() conversion aligns with aviation standards and your existing test expectations.

Which format does your system need?

@makrsmark
Copy link
Collaborator

@rpatel3001 @kevinelliott - finally took a look at this and pulled c-band into it's own decoder. I think this is a little cleaner. What do you guys think?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/plugins/Label_4A.test.ts`:
- Around line 109-119: The test currently calls Label_4A.plugin.decode directly
so decodeResult.decoder.name is always 'label-4a'; update the test to route the
message through MessageDecoder.decode to verify plugin selection (i.e., create a
new MessageDecoder, set message.text = 'DIS01,182103,WEN3100,WRONG CREW HAHAHA',
call decoder.decode(message) and assert decodeResult.decoder.name ===
'label-4a-dis'), or alternatively (if you prefer to keep calling
Label_4A.decode) change assertions to check that plugin.decode returns decoded
=== false and decoder.decodeLevel === 'none' while decoder.name remains
'label-4a' so the test reflects expected rejection by Label_4A.
🧹 Nitpick comments (3)
lib/plugins/Label_83.test.ts (2)

5-6: Consider using const for consistency with other test files.

The message object is declared with let, but since only message.text is reassigned (not the reference itself), const would be more accurate and consistent with Label_4N.test.ts (line 8).

Suggested fix
   let plugin: Label_83;
-  let message = { label: '83', text: '' };
+  const message = { label: '83', text: '' };

13-16: Incomplete qualifiers test compared to other test files.

This test only checks that decode is defined and name matches, but doesn't actually verify the qualifiers() method output. For consistency with Label_4A.test.ts (lines 14-21), consider adding assertions for the qualifiers.

Suggested fix
   test('matches qualifiers', () => {
     expect(plugin.decode).toBeDefined();
     expect(plugin.name).toBe('label-83');
+    expect(plugin.qualifiers).toBeDefined();
+    expect(plugin.qualifiers()).toEqual({
+      labels: ['83'],
+    });
   });
lib/plugins/Label_4N.test.ts (1)

15-18: Incomplete qualifiers test compared to Label_4A.test.ts.

Similar to Label_83.test.ts, this test doesn't verify the qualifiers() method output. For consistency with Label_4A.test.ts, consider adding assertions.

Suggested fix
   test('matches qualifiers', () => {
     expect(plugin.decode).toBeDefined();
     expect(plugin.name).toBe('label-4n');
+    expect(plugin.qualifiers).toBeDefined();
+    expect(plugin.qualifiers()).toEqual({
+      labels: ['4N'],
+    });
   });

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/plugins/Label_4N.ts (1)

34-48: ⚠️ Potential issue | 🔴 Critical

text is not defined — incomplete migration to message.text causes runtime crash.

Lines 37, 42, and 46 still reference the removed local variable text. The pipeline confirms this: ReferenceError: text is not defined during decode of Label 4N variant 1.

🐛 Proposed fix
       ResultFormatter.position(
         decodeResult,
         CoordinateUtils.decodeStringCoordinatesDecimalMinutes(
-          text.substring(30, 45).replace(/^(.)0/, '$1'),
+          message.text.substring(30, 45).replace(/^(.)0/, '$1'),
         ),
       );
       ResultFormatter.altitude(
         decodeResult,
-        Number(text.substring(48, 51)) * 100,
+        Number(message.text.substring(48, 51)) * 100,
       );
       ResultFormatter.unknownArr(
         decodeResult,
-        [text.substring(2, 4), text.substring(19, 29)],
+        [message.text.substring(2, 4), message.text.substring(19, 29)],
         ' ',
       );
🤖 Fix all issues with AI agents
In `@lib/MessageDecoder.ts`:
- 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.

In `@lib/plugins/Label_4A.ts`:
- Around line 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.
🧹 Nitpick comments (1)
lib/plugins/CBand.test.ts (1)

66-87: Label 83 variant 1 test expects heading of -107.6 — verify this is intentional.

Headings are typically in the 0–360° range. The raw field value -107.6 in the test message would parse as -107.6 via Number(). If this field actually represents something other than a magnetic heading (or the data source uses a different convention), it may be worth adding a comment. Otherwise, the test assertions look correct.

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.

Comment on lines +35 to 36
// ResultFormatter.altitude(decodeResult, Number(alt) * 100);
ResultFormatter.unknownArr(decodeResult, fields.slice(8));
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);
      }
    }
    

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants