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
4 changes: 4 additions & 0 deletions lib/MessageDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ export class MessageDecoder {
this.registerPlugin(new Plugins.Label_4T_ETA(this));
this.registerPlugin(new Plugins.Label_B6_Forwardslash(this));
this.registerPlugin(new Plugins.Label_H2_02E(this));
this.registerPlugin(new Plugins.Label_H1_ATIS(this));
this.registerPlugin(new Plugins.Label_H1_EZF(this));
this.registerPlugin(new Plugins.Label_H1_FLR(this));
this.registerPlugin(new Plugins.Label_H1_M_POS(this));
this.registerPlugin(new Plugins.Label_H1_OHMA(this));
this.registerPlugin(new Plugins.Label_H1_OFP(this));
this.registerPlugin(new Plugins.Label_H1_Paren(this));
this.registerPlugin(new Plugins.Label_H1_WRN(this));
this.registerPlugin(new Plugins.Label_H1_StarPOS(this));
Expand Down
90 changes: 90 additions & 0 deletions lib/plugins/Label_H1_ATIS.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { MessageDecoder } from '../MessageDecoder';
import { Label_H1_ATIS } from './Label_H1_ATIS';

describe('Label H1 ATIS Subscription', () => {
let plugin: Label_H1_ATIS;
const message = { label: 'H1', text: '' };

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

test('matches qualifiers', () => {
expect(plugin.decode).toBeDefined();
expect(plugin.name).toBe('label-h1-atis');
expect(plugin.qualifiers()).toEqual({
labels: ['H1', '5Z'],
preambles: ['L'],
});
});

test('decodes KSFO ATIS subscription', () => {
message.text = 'L95AQF0073/KSFO.TI2/030KSFOAFF5C';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('full');
expect(decodeResult.decoder.name).toBe('label-h1-atis');
expect(decodeResult.formatted.description).toBe('ATIS Subscription');
expect(decodeResult.message).toBe(message);
expect(decodeResult.raw.flight_number).toBe('QF0073');
expect(decodeResult.raw.arrival_icao).toBe('KSFO');
expect(decodeResult.raw.atis_code).toBe('030');
expect(decodeResult.raw.checksum).toBe(0xaff5c);
});

test('decodes OERK ATIS subscription', () => {
message.text = 'L01AMC5477/OERK.TI2/024OERKC8781';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('full');
expect(decodeResult.raw.flight_number).toBe('MC5477');
expect(decodeResult.raw.arrival_icao).toBe('OERK');
expect(decodeResult.raw.atis_code).toBe('024');
expect(decodeResult.raw.checksum).toBe(0xc8781);
});

test('decodes LICC ATIS subscription', () => {
message.text = 'L11AMC5477/LICC.TI2/024LICCAFBD9';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('full');
expect(decodeResult.raw.flight_number).toBe('MC5477');
expect(decodeResult.raw.arrival_icao).toBe('LICC');
expect(decodeResult.raw.atis_code).toBe('024');
expect(decodeResult.raw.checksum).toBe(0xafbd9);
});

test('decodes with label 5Z', () => {
const msg5Z = {
label: '5Z',
text: 'L95AQF0073/KSFO.TI2/030KSFOAFF5C',
};
const decodeResult = plugin.decode(msg5Z);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.raw.flight_number).toBe('QF0073');
expect(decodeResult.raw.arrival_icao).toBe('KSFO');
});

test('rejects invalid message', () => {
message.text = 'LINVALID MESSAGE';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.decoder.name).toBe('label-h1-atis');
expect(decodeResult.remaining.text).toBe(message.text);
});

test('rejects non-ATIS message', () => {
message.text = 'FLR/FR24030411230034583106FWC2';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
});
});
74 changes: 74 additions & 0 deletions lib/plugins/Label_H1_ATIS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { DecoderPlugin } from '../DecoderPlugin';
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
import { ResultFormatter } from '../utils/result_formatter';
import { DateTimeUtils } from '../DateTimeUtils';

export class Label_H1_ATIS extends DecoderPlugin {
name = 'label-h1-atis';

qualifiers() {
return {
labels: ['H1', '5Z'],
preambles: ['L'],
};
}

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

// Pattern: L[2-digit seq]A[flight]/[facility].TI2/[code][airport][checksum]
const regex =
/^L(\d{2})A([A-Z0-9]+)\/([A-Z]{4})\.TI2\/(\d{3})([A-Z]{4})([A-F0-9]+)$/;
const match = message.text.match(regex);

if (!match) {
if (options.debug) {
console.log(`Decoder: Unknown H1 ATIS message: ${message.text}`);
}
ResultFormatter.unknown(decodeResult, message.text);
decodeResult.decoded = false;
decodeResult.decoder.decodeLevel = 'none';
return decodeResult;
}

const seq = match[1];
const flight = match[2];
const facility = match[3];
const code = match[4];
const airport = match[5];
const checksum = match[6];

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

can we validate this? 5 nibbles seems not right


ResultFormatter.flightNumber(decodeResult, flight);
ResultFormatter.arrivalAirport(decodeResult, facility);

decodeResult.raw.atis_code = code;
decodeResult.formatted.items.push({
type: 'atis',
code: 'ATIS_CODE',
label: 'ATIS Code',
value: code,
});

// The airport in the TI2 section
if (airport !== facility) {
decodeResult.raw.atis_airport = airport;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

i don't see this in tests

decodeResult.formatted.items.push({
type: 'icao',
code: 'ATIS_ARPT',
label: 'ATIS Airport',
value: airport,
});
}

// Checksum
ResultFormatter.checksum(decodeResult, parseInt(checksum, 16));

decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = 'full';

return decodeResult;
}
}
87 changes: 87 additions & 0 deletions lib/plugins/Label_H1_EZF.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { MessageDecoder } from '../MessageDecoder';
import { Label_H1_EZF } from './Label_H1_EZF';

describe('Label H1 Preamble EZF Load Sheet', () => {
let plugin: Label_H1_EZF;
const message = { label: 'H1', text: '' };

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

test('matches qualifiers', () => {
expect(plugin.decode).toBeDefined();
expect(plugin.name).toBe('label-h1-ezf');
expect(plugin.qualifiers()).toEqual({
labels: ['H1', '1M'],
preambles: ['EZF'],
});
});

test('decodes full load sheet', () => {
message.text =
'EZF\nNO0246/10/EI-NEO\n/C28Y331/3/9\n-SCT/MXP-ZNZ\n-STD/2200\n-FLT STATUS/CLOSED\n-UOM/KG\n-ZFW/162075\n-PAX/305\n-PXT/122/160/19/02\n-PXW/22601\n-CGO/3573\n-BAG/4783\n-OTH/4336\n-TTL/35293\n-FWT/51063\n-TOW/213138\n-DOW/126782';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('label-h1-ezf');
expect(decodeResult.formatted.description).toBe('Load Sheet');
expect(decodeResult.message).toBe(message);
expect(decodeResult.raw.flight_number).toBe('NO0246');
expect(decodeResult.raw.tail).toBe('EI-NEO');
expect(decodeResult.raw.departure_iata).toBe('MXP');
expect(decodeResult.raw.arrival_iata).toBe('ZNZ');
expect(decodeResult.raw.loadsheet['ZFW']).toBe('162075');
expect(decodeResult.raw.loadsheet['PAX']).toBe('305');
expect(decodeResult.raw.loadsheet['TOW']).toBe('213138');
expect(decodeResult.raw.loadsheet['FLT STATUS']).toBe('CLOSED');
expect(decodeResult.raw.loadsheet['UOM']).toBe('KG');
});

test('decodes minimal load sheet', () => {
message.text = 'EZF\nAB1234/05/D-ABCD\n-SCT/JFK-LAX\n-PAX/180\n-ZFW/55000';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.raw.flight_number).toBe('AB1234');
expect(decodeResult.raw.tail).toBe('D-ABCD');
expect(decodeResult.raw.departure_iata).toBe('JFK');
expect(decodeResult.raw.arrival_iata).toBe('LAX');
expect(decodeResult.raw.loadsheet['PAX']).toBe('180');
expect(decodeResult.raw.loadsheet['ZFW']).toBe('55000');
});

test('decodes with label 1M', () => {
const msg1M = {
label: '1M',
text: 'EZF\nXY5678/12/G-TEST\n-SCT/LHR-CDG\n-PAX/90',
};
const decodeResult = plugin.decode(msg1M);

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.raw.flight_number).toBe('XY5678');
expect(decodeResult.raw.departure_iata).toBe('LHR');
expect(decodeResult.raw.arrival_iata).toBe('CDG');
});

test('rejects invalid message', () => {
message.text = 'NOT_EZF data here';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.decoder.name).toBe('label-h1-ezf');
expect(decodeResult.remaining.text).toBe(message.text);
});

test('rejects message with only EZF header', () => {
message.text = 'EZF';
const decodeResult = plugin.decode(message);

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
});
});
113 changes: 113 additions & 0 deletions lib/plugins/Label_H1_EZF.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { DecoderPlugin } from '../DecoderPlugin';
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
import { ResultFormatter } from '../utils/result_formatter';

export class Label_H1_EZF extends DecoderPlugin {
name = 'label-h1-ezf';

qualifiers() {
return {
labels: ['H1', '1M'],
preambles: ['EZF'],
};
}

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

const lines = message.text
.split('\n')
.map((l) => l.trim())
.filter((l) => l.length > 0);

if (lines.length < 2 || lines[0] !== 'EZF') {
if (options.debug) {
console.log(`Decoder: Unknown H1 EZF message: ${message.text}`);
}
ResultFormatter.unknown(decodeResult, message.text);
decodeResult.decoded = false;
decodeResult.decoder.decodeLevel = 'none';
return decodeResult;
}

const loadsheet: Record<string, string> = {};
const unknowns: string[] = [];

for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('-')) {
const kvMatch = line.match(/^-([^/]+)\/(.+)$/);
if (kvMatch) {
loadsheet[kvMatch[1]] = kvMatch[2];
} else {
unknowns.push(line);
}
} else if (i === 1) {
// Second line is the flight identifier, e.g. NO0246/10/EI-NEO
const idParts = line.split('/');
if (idParts.length >= 1) {
ResultFormatter.flightNumber(decodeResult, idParts[0]);
}
if (idParts.length >= 3) {
ResultFormatter.tail(decodeResult, idParts[2]);
}
// Remaining parts of identifier line
if (idParts.length >= 2) {
// idParts[1] is day or other info - store but don't format
}
} else if (i === 2) {
// Third line typically contains config info, e.g. /C28Y331/3/9
unknowns.push(line);
} else {
unknowns.push(line);
}
}

// Extract known fields from loadsheet
if (loadsheet['SCT']) {
const route = loadsheet['SCT'].split('-');
if (route.length === 2) {
ResultFormatter.departureAirport(decodeResult, route[0], 'IATA');
ResultFormatter.arrivalAirport(decodeResult, route[1], 'IATA');
}
}

// Store all loadsheet data in raw
decodeResult.raw.loadsheet = loadsheet;

// Format key loadsheet items
const formatFields: [string, string, string][] = [
['STD', 'Scheduled Time of Departure', 'std'],
['FLT STATUS', 'Flight Status', 'flight_status'],
['UOM', 'Unit of Measure', 'uom'],
['ZFW', 'Zero Fuel Weight', 'zfw'],
['PAX', 'Passengers', 'pax'],
['TOW', 'Takeoff Weight', 'tow'],
['DOW', 'Dry Operating Weight', 'dow'],
['FWT', 'Fuel Weight', 'fuel_weight'],
];

for (const [key, label, code] of formatFields) {
if (loadsheet[key]) {
decodeResult.formatted.items.push({
type: 'loadsheet',
code: code.toUpperCase(),
label: label,
value: loadsheet[key],
});
}
}

if (unknowns.length > 0) {
ResultFormatter.unknownArr(decodeResult, unknowns, '\n');
}

decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = 'partial';

return decodeResult;
}
}
Loading