pull up c-band prefix decoding#174
Conversation
lib/plugins/Label_4A.test.ts
Outdated
| // 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 }); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Moving the c band message tests makes sense with two drawbacks:
- Need to remember to add tests in a second location for types with this prefix
- 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
@kevinelliott and @andermatt64 - Do you have any opinions here?
84e959e to
cf15587
Compare
3447c21 to
1e877a4
Compare
ad6e34a to
da1033b
Compare
da1033b to
fe1cdc7
Compare
|
@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. |
|
last thing to resolve is the conversation about how to do c-band tests: make each plugin test use |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. WalkthroughThis 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 ofmessage.text.Using
message.textdirectly 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
ResultFormatterimport is necessary for the new C-Band prefix handling that usesResultFormatter.flightNumber().
99-105: Well-implemented C-Band prefix detection and extraction.The implementation follows a clear pattern:
- Identify messages with a 10-character C-Band header using a descriptive regex
- Extract the message number, airline code, and flight number
- 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:
- Properly checks if a C-Band prefix was detected
- Adds the flight number information using
ResultFormatter.flightNumber()- 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
MessageDecoderinstance across all tests. The test now passes both thelabelandtextto 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 directlyThis change aligns with the PR objective of using a top-level
MessageDecoderinstance 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 casesAll test cases have been updated to use the
MessageDecoderinstance 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 formatThe 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 variableThe code now directly references
message.textinstead 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 handlingSimilar to the changes in Label_83.ts, this plugin now directly uses
message.textinstead 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 MessageDecoderAll test cases now use the
MessageDecoderinstance 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 messagesThe 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 objectivesThis 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 objectivesSwitching 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 intentionalSimilar to line 29, the expected item count has been reduced from 5 to 4. This appears to be related to how the
MessageDecodernow processes these messages.
71-71: Consistent methodology across all test casesThe 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.tsnow correctly expects the flight information (FLIGHT) to appear as the last item in the formatted items array. Verification via theMessageDecoder.tssnippet confirms that the flight number is appended—via theResultFormatter.flightNumbercall—after processing the other fields, which aligns with the expected behavior.No further modifications are required.
|
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 |
|
@makrsmark We might have copilot or Claude rework this a bit. |
* call other plugins * only process if it can be decoded
There was a problem hiding this comment.
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
expectstatements 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'); });
lib/plugins/CBand.ts
Outdated
| 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); |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
Good question - do we want XA1 or XA0001 ?
There was a problem hiding this comment.
🧩 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 withNumber()) - 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?
|
@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? |
There was a problem hiding this comment.
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 usingconstfor consistency with other test files.The
messageobject is declared withlet, but since onlymessage.textis reassigned (not the reference itself),constwould be more accurate and consistent withLabel_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
decodeis defined andnamematches, but doesn't actually verify thequalifiers()method output. For consistency withLabel_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 thequalifiers()method output. For consistency withLabel_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'], + }); });
There was a problem hiding this comment.
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
textis not defined — incomplete migration tomessage.textcauses 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 expectsheadingof -107.6 — verify this is intentional.Headings are typically in the 0–360° range. The raw field value
-107.6in the test message would parse as -107.6 viaNumber(). 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 |
There was a problem hiding this comment.
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
optionslike{ 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.
| // ResultFormatter.altitude(decodeResult, Number(alt) * 100); | ||
| ResultFormatter.unknownArr(decodeResult, fields.slice(8)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
this was straight up wrong - it needs to be fixed
There was a problem hiding this comment.
🧩 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:
- There are no C-Band variant 1 tests (only C-Band variant 2 exists in CBand.test.ts)
- Regular variant 1 messages are too short for substring(48, 51) to extract meaningful altitude data
What should the fix be?
- Remove the commented line entirely (altitude was never valid for variant 1)?
- Extract altitude from one of the fields (if so, which field index contains altitude)?
- 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.
- Joins and forwards to unknown.
- route(decodeResult, route)
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); } }
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
Bug Fixes
Tests