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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions lib/plugins/Label_16_AUTPOS.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { MessageDecoder } from '../MessageDecoder';
import { Label_16_AUTPOS } from './Label_16_AUTPOS';

describe('Label 16 AUTPOS', () => {
let plugin: Label_16_AUTPOS;
const message = { label: '16', text: '' };

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

test('matches qualifiers', () => {
expect(plugin.decode).toBeDefined();
expect(plugin.name).toBe('label-16-autpos');
expect(plugin.qualifiers).toBeDefined();
expect(plugin.qualifiers()).toEqual({
labels: ['16'],
preambles: [''],
});
});

test('matches redacted', () => {
message.text =
'283806/AUTPOS/LLD N400547 W0774954\r\n/ALT 12932/SAT ****\r\n/WND ******/TAT ****/TAS ****/CRZ ***\r\n/FOB 065120\r\n/DAT 260228/TIM 150742';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('full');
expect(decodeResult.raw.day).toBe(28);
expect(decodeResult.raw.flight_number).toBe('3806');
expect(decodeResult.raw.position.latitude).toBeCloseTo(40.096, 3);
expect(decodeResult.raw.position.longitude).toBeCloseTo(-77.832, 3);
expect(decodeResult.raw.altitude).toBe(12932);
expect(decodeResult.raw.fuel_on_board).toBe(65120);
expect(decodeResult.raw.message_timestamp).toBe(1772291262);
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.formatted.items.length).toBe(6);
});

test('matches all values', () => {
message.text =
'289142/AUTPOS/LLD N395538 W0753341 \r\n/ALT 35000/SAT -057\r\n/WND 239065/TAT -027/TAS 476/CRZ 836\r\n/FOB 107600\r\n/DAT 260228/TIM 132714';
const decodeResult = plugin.decode(message);
expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('full');
expect(decodeResult.raw.day).toBe(28);
expect(decodeResult.raw.flight_number).toBe('9142');
expect(decodeResult.raw.position.latitude).toBeCloseTo(39.927, 3);
expect(decodeResult.raw.position.longitude).toBeCloseTo(-75.561, 3);
expect(decodeResult.raw.altitude).toBe(35000);
expect(decodeResult.raw.wind_data[0].windSpeed).toBe(65);
expect(decodeResult.raw.wind_data[0].windDirection).toBe(239);
expect(decodeResult.raw.total_air_temperature).toBe(-27);
expect(decodeResult.raw.airspeed).toBe(476);
expect(decodeResult.raw.mach).toBe(0.836);
expect(decodeResult.raw.fuel_on_board).toBe(107600);
expect(decodeResult.raw.message_timestamp).toBe(1772285234);
expect(decodeResult.formatted.items.length).toBe(11);
});

test('does not match if wrong format', () => {
message.text = 'invalid AUTPOS message';
const decodeResult = plugin.decode(message);
expect(decodeResult.decoded).toBe(false);
});
});
122 changes: 122 additions & 0 deletions lib/plugins/Label_16_AUTPOS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { DateTimeUtils } from '../DateTimeUtils';
import { DecoderPlugin } from '../DecoderPlugin';
import { DecodeResult, Message } from '../DecoderPluginInterface';
import { CoordinateUtils } from '../utils/coordinate_utils';
import { ResultFormatter } from '../utils/result_formatter';

export class Label_16_AUTPOS extends DecoderPlugin {
name = 'label-16-autpos';
qualifiers() {
return {
labels: ['16'],
preambles: [''],
};
}
Comment on lines +7 to +14
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Label_16_AUTPOS is registered in MessageDecoder
rg -n "Label_16_AUTPOS" lib/MessageDecoder.ts

# List all registered Label_16 plugins for comparison
rg -n "Label_16" lib/MessageDecoder.ts

Repository: airframesio/acars-decoder-typescript

Length of output: 331


🏁 Script executed:

#!/bin/bash
# Check if Label_16_AUTPOS.ts file exists
ls -la lib/plugins/Label_16_AUTPOS.ts

# Check if it's exported from the Plugins index/module
rg -n "Label_16_AUTPOS" lib/plugins/

Repository: airframesio/acars-decoder-typescript

Length of output: 487


🏁 Script executed:

#!/bin/bash
# Check how plugins are imported in MessageDecoder.ts
head -20 lib/MessageDecoder.ts

# Check if there's an index/barrel export in plugins directory
cat lib/plugins/index.ts 2>/dev/null || ls -la lib/plugins/*.ts | grep -i index

Repository: airframesio/acars-decoder-typescript

Length of output: 611


🏁 Script executed:

#!/bin/bash
# Check plugins/official.ts for Label_16_AUTPOS export
rg -n "Label_16_AUTPOS|export" lib/plugins/official.ts | head -50

Repository: airframesio/acars-decoder-typescript

Length of output: 1836


🏁 Script executed:

#!/bin/bash
# Get the full plugins/official.ts file to check for Label_16_AUTPOS
wc -l lib/plugins/official.ts
# Check if Label_16_AUTPOS appears anywhere in the file
rg "Label_16_AUTPOS" lib/plugins/official.ts

Repository: airframesio/acars-decoder-typescript

Length of output: 106


Plugin not exported from official plugins module.

The Label_16_AUTPOS plugin is not exported from lib/plugins/official.ts and therefore not registered in MessageDecoder.ts. The plugin class exists but won't be invoked when decoding messages through the main MessageDecoder.decode() API. Add the export line to lib/plugins/official.ts alongside the other Label_16 variants (lines 11-14):

export * from './Label_16_AUTPOS';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/plugins/Label_16_AUTPOS.ts` around lines 7 - 14, The plugin class
Label_16_AUTPOS is not exported from the central plugins export module, so
MessageDecoder never registers it; fix this by adding an export for the
Label_16_AUTPOS plugin in the same central plugins export file alongside the
other Label_16 variants so it follows the same export pattern and will be picked
up by MessageDecoder.decode().


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

// Regex to match the AUTPOS message
const regex =
/^(\d{6})\/AUTPOS\/LLD (N|S)(\d{2})(\d{2})(\d{2}) (E|W)(\d{3})(\d{2})(\d{2})\s*\r?\n\/ALT (\d+)\/SAT ([*\-\d]{4})\r?\n\/WND ([*\d]{3})([\*\d]{3})\/TAT ([*\-\d]{4})\/TAS ([*\d]{3,4})\/CRZ ([*\d]{3,4})\r?\n\/FOB (\d{6})\r?\n\/DAT (\d{6})\/TIM (\d{6})/;
const match = regex.exec(message.text);
if (!match) {
decodeResult.decoded = false;
return decodeResult;
}

decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = 'full';
decodeResult.formatted.description = 'Position Report';

// Extract fields
const [
,
flight,
latHem,
latDeg,
latMin,
latSec,
lonHem,
lonDeg,
lonMin,
lonSec,
altitude,
sat,
windDir,
windSpd,
tat,
tas,
crz,
fob,
dat,
tim,
] = match;

ResultFormatter.day(decodeResult, parseInt(flight.slice(0, 2), 10));
ResultFormatter.flightNumber(decodeResult, flight.slice(2));
let latitude = CoordinateUtils.dmsToDecimalDegrees(
parseInt(latDeg, 10),
parseInt(latMin, 10),
parseInt(latSec, 10),
);
if (latHem === 'S') latitude = -latitude;
let longitude = CoordinateUtils.dmsToDecimalDegrees(
parseInt(lonDeg, 10),
parseInt(lonMin, 10),
parseInt(lonSec, 10),
);
if (lonHem === 'W') longitude = -longitude;
ResultFormatter.position(decodeResult, {
latitude,
longitude,
});
const alt = parseInt(altitude, 10);
ResultFormatter.altitude(decodeResult, alt);

// Parse wind
if (!windSpd.includes('*') && !windDir.includes('*')) {
ResultFormatter.windData(decodeResult, [
{
waypoint: {
name: 'POSITION',
},
flightLevel: Math.round(alt / 100),
windDirection: parseInt(windDir, 10),
windSpeed: parseInt(windSpd, 10),
},
]);
}

if (!tat.includes('*')) {
ResultFormatter.totalAirTemp(decodeResult, tat);
}
if (!tas.includes('*')) {
ResultFormatter.airspeed(decodeResult, parseInt(tas, 10));
}
if (!sat.includes('*')) {
ResultFormatter.temperature(decodeResult, sat);
}
if (!crz.includes('*')) {
ResultFormatter.mach(decodeResult, parseInt(crz, 10) / 1000);
}

if (!fob.includes('*')) {
ResultFormatter.currentFuel(decodeResult, parseInt(fob, 10));
}

if (!dat.includes('*') && !tim.includes('*')) {
const yy = dat.slice(0, 2);
const mm = dat.slice(2, 4);
const dd = dat.slice(4, 6);
const mmddyy = `${mm}${dd}${yy}`;
ResultFormatter.timestamp(
decodeResult,
DateTimeUtils.convertDateTimeToEpoch(tim, mmddyy),
);
Comment on lines +110 to +118
Copy link

@coderabbitai coderabbitai bot Feb 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Date format comment is incorrect; actual format appears to be YYMMDD.

The comment states DAT is DDMMYY, but the code extracts yy = dat.slice(0, 2), mm = dat.slice(2, 4), dd = dat.slice(4, 6), which treats the format as YYMMDD. Based on the test case where 260228 produces a timestamp for Feb 28, 2026, the actual format is YYMMDD.

📝 Proposed fix
     if (!dat.includes('*') && !tim.includes('*')) {
-      // DAT is DDMMYY, TIM is HHMMSS
+      // DAT is YYMMDD, TIM is HHMMSS
       // DateTimeUtils.convertDateTimeToEpoch expects MMDDYY
       const yy = dat.slice(0, 2);
       const mm = dat.slice(2, 4);
       const dd = dat.slice(4, 6);
📝 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
if (!dat.includes('*') && !tim.includes('*')) {
// DAT is DDMMYY, TIM is HHMMSS
// DateTimeUtils.convertDateTimeToEpoch expects MMDDYY
const yy = dat.slice(0, 2);
const mm = dat.slice(2, 4);
const dd = dat.slice(4, 6);
const mmddyy = `${mm}${dd}${yy}`;
ResultFormatter.timestamp(
decodeResult,
DateTimeUtils.convertDateTimeToEpoch(tim, mmddyy),
);
if (!dat.includes('*') && !tim.includes('*')) {
// DAT is YYMMDD, TIM is HHMMSS
// DateTimeUtils.convertDateTimeToEpoch expects MMDDYY
const yy = dat.slice(0, 2);
const mm = dat.slice(2, 4);
const dd = dat.slice(4, 6);
const mmddyy = `${mm}${dd}${yy}`;
ResultFormatter.timestamp(
decodeResult,
DateTimeUtils.convertDateTimeToEpoch(tim, mmddyy),
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/plugins/Label_16_AUTPOS.ts` around lines 111 - 121, The comment above the
DAT/TIM parsing is wrong: DAT is actually in YYMMDD (e.g., 260228 ->
2026-02-28), but the code slices dat into yy = dat.slice(0,2), mm =
dat.slice(2,4), dd = dat.slice(4,6) and then builds mmddyy for
DateTimeUtils.convertDateTimeToEpoch; update the comment to state "DAT is
YYMMDD, TIM is HHMMSS" (or similar) so it matches the parsing logic in this
block that calls ResultFormatter.timestamp and
DateTimeUtils.convertDateTimeToEpoch(tim, mmddyy). Ensure the comment explicitly
notes convertDateTimeToEpoch expects MMDDYY.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is incorrect. the code is corect

Copy link

Choose a reason for hiding this comment

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

@makrsmark, I apologize for any confusion. To clarify, I was pointing out that the comment text on line 111 states // DAT is DDMMYY, but the code logic parses it as YYMMDD (year-month-day). The code itself is correct—I was suggesting updating only the comment to match what the code does.

However, if you feel the comment is fine as-is or prefer to keep it, that's perfectly okay. I'll defer to your judgment on this.


🧠 Learnings used
Learnt from: makrsmark
Repo: airframesio/acars-decoder-typescript PR: 332
File: lib/plugins/Label_H1.ts:39-40
Timestamp: 2026-02-21T01:32:05.328Z
Learning: In lib/plugins/Label_H1.ts, when parsing two-part H1 messages (split by '#'), the offset logic `parts[0] === '- ' || isNaN(parseInt(parts[1][1])) ? 3 : 4` is correct. The `parts[0] === '- '` condition ensures that messages like '- `#M1FPN/`...' use offset=3 to skip 3 characters after '#' ('M1' + next char), whereas messages like 'F37AKL0767#M1BFPN/...' use offset=4 to skip 4 characters ('M1B' + '/'). Removing the `parts[0] === '- '` check would break '- `#M1`...' messages where parts[1][1] is numeric.

}
return decodeResult;
}
}
25 changes: 25 additions & 0 deletions lib/utils/result_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,21 @@ export class ResultFormatter {
});
}

static totalAirTemp(decodeResult: DecodeResult, value: string) {
if (value.length === 0) {
return;
}
decodeResult.raw.total_air_temperature = Number(
value.replace('M', '-').replace('P', '+'),
);
decodeResult.formatted.items.push({
type: 'temperature',
code: 'TATEMP',
label: 'Total Air Temperature (C)',
value: `${decodeResult.raw.total_air_temperature} degrees`,
});
}

static heading(decodeResult: DecodeResult, value: number) {
decodeResult.raw.heading = value;
decodeResult.formatted.items.push({
Expand Down Expand Up @@ -613,6 +628,16 @@ export class ResultFormatter {
});
}

static timestamp(decodeResult: DecodeResult, value: number) {
decodeResult.raw.message_timestamp = value;
decodeResult.formatted.items.push({
type: 'epoch',
code: 'TIMESTAMP',
label: 'Message Timestamp',
value: DateTimeUtils.timestampToString(value, 'epoch'),
});
}

static unknown(decodeResult: DecodeResult, value: string, sep: string = ',') {
if (!decodeResult.remaining.text) decodeResult.remaining.text = value;
else decodeResult.remaining.text += sep + value;
Expand Down