Skip to content

Commit

Permalink
additional POS parsing (#116)
Browse files Browse the repository at this point in the history
also parsing for 4J messages
  • Loading branch information
makrsmark authored Sep 5, 2024
1 parent 84fce20 commit 2fb6482
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 123 deletions.
87 changes: 61 additions & 26 deletions lib/plugins/Label_H1_POS.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test('matches Label H1 Preamble POS qualifiers', () => {
expect(decoderPlugin.name).toBe('label-h1-pos');
expect(decoderPlugin.qualifiers).toBeDefined();
expect(decoderPlugin.qualifiers()).toEqual({
labels: ['H1'],
labels: ['H1', '4J'],
preambles: ['POS', '#M1BPOS', '/.POS'],
});
});
Expand Down Expand Up @@ -327,18 +327,21 @@ test('decodes Label H1 Preamble POS variant 7', () => {
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('label-h1-pos');
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[0].value).toBe('39.277 N, 77.359 W');
expect(decodeResult.formatted.items[1].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[1].value).toBe('(39.300 N, 77.110 W)@2024-03-03T14:28:00Z > (38.560 N, 77.150 W)@2024-03-03T03:14:30Z > ?');
expect(decodeResult.formatted.items[2].label).toBe('Altitude');
expect(decodeResult.formatted.items[2].value).toBe('24000 feet');
expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[3].value).toBe('-28');
expect(decodeResult.formatted.items[4].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[4].value).toBe('0x9071');
expect(decodeResult.remaining.text).toBe('/ID91459S,BANKR31,142813/MR64,0,/ET31539,27619,MT370/CG311,160,350/FB732/VR32');
expect(decodeResult.raw.flight_number).toBe('BANKR31');
expect(decodeResult.formatted.items.length).toBe(6);
expect(decodeResult.formatted.items[0].label).toBe('Tail');
expect(decodeResult.formatted.items[0].value).toBe('91459S');
expect(decodeResult.formatted.items[1].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[1].value).toBe('39.277 N, 77.359 W');
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[2].value).toBe('(39.300 N, 77.110 W)@2024-03-03T14:28:00Z > (38.560 N, 77.150 W)@2024-03-03T03:14:30Z > ?');
expect(decodeResult.formatted.items[3].label).toBe('Altitude');
expect(decodeResult.formatted.items[3].value).toBe('24000 feet');
expect(decodeResult.formatted.items[4].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[4].value).toBe('-28');
expect(decodeResult.formatted.items[5].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[5].value).toBe('0x9071');
expect(decodeResult.remaining.text).toBe(',142813/MR64,0,/ET31539,27619,MT370/CG311,160,350/FB732/VR32');
});

test('decodes Label H1 Preamble #M1BPOS variant 7', () => {
Expand All @@ -354,19 +357,21 @@ test('decodes Label H1 Preamble #M1BPOS variant 7', () => {
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('label-h1-pos');
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.raw.flight_number).toBe('AMCLL93');
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[0].value).toBe('42.579 N, 108.090 W');
expect(decodeResult.formatted.items[1].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[1].value).toBe('WAIDE@2024-03-03T17:32:07Z > WEDAK@2024-03-03T03:17:59Z > ?');
expect(decodeResult.formatted.items[2].label).toBe('Altitude');
expect(decodeResult.formatted.items[2].value).toBe('32000 feet');
expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[3].value).toBe('-49');
expect(decodeResult.formatted.items[4].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[4].value).toBe('0x4e17');
expect(decodeResult.remaining.text).toBe('F37#M1B/ID746026,,173207/MR1,,/ET031846,267070,T468/CG264,110,360/FB742/VR32');
expect(decodeResult.raw.flight_number).toBe('');
expect(decodeResult.formatted.items.length).toBe(6);
expect(decodeResult.formatted.items[0].label).toBe('Tail');
expect(decodeResult.formatted.items[0].value).toBe('746026');
expect(decodeResult.formatted.items[1].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[1].value).toBe('42.579 N, 108.090 W');
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[2].value).toBe('WAIDE@2024-03-03T17:32:07Z > WEDAK@2024-03-03T03:17:59Z > ?');
expect(decodeResult.formatted.items[3].label).toBe('Altitude');
expect(decodeResult.formatted.items[3].value).toBe('32000 feet');
expect(decodeResult.formatted.items[4].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[4].value).toBe('-49');
expect(decodeResult.formatted.items[5].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[5].value).toBe('0x4e17');
expect(decodeResult.remaining.text).toBe('F37#M1B,173207/MR1,,/ET031846,267070,T468/CG264,110,360/FB742/VR32');
});

test('decodes Label H1 Preamble POS variant 8', () => {
Expand Down Expand Up @@ -490,6 +495,36 @@ test('decodes Label H1 Preamble /.POS variant 2', () => {
expect(decodeResult.remaining.text).toBe('/.POS,27282,241,MANUAL,0,813');
});

test('decodes Label 4J Preamble POS variant 7', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_H1_POS(decoder);

// https://app.airframes.io/messages/3157551384
const text = 'POS/ID91517S,WIDE21,7PZWTCP21222/DC09082024,140706/MR238,2/ET91456/PSN37375W077368,140700,300,JAXSN,091417,LOOEY,M26,21329,M080T490/CG293,160,350/FB583/VR32C696';
const decodeResult = decoderPlugin.decode({ text: text });
console.log(JSON.stringify(decodeResult, null, 2));

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.name).toBe('label-h1-pos');
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.raw.flight_number).toBe('WIDE21');
expect(decodeResult.formatted.items.length).toBe(6);
expect(decodeResult.formatted.items[0].label).toBe('Tail');
expect(decodeResult.formatted.items[0].value).toBe('91517S');
expect(decodeResult.formatted.items[1].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[1].value).toBe('37.375 N, 77.368 W');
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[2].value).toBe('JAXSN@14:07:00 > LOOEY@09:14:17 > ?');
expect(decodeResult.formatted.items[3].label).toBe('Altitude');
expect(decodeResult.formatted.items[3].value).toBe('30000 feet');
expect(decodeResult.formatted.items[4].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[4].value).toBe('-26');
expect(decodeResult.formatted.items[5].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[5].value).toBe('0xc696');
expect(decodeResult.remaining.text).toBe(',7PZWTCP21222/DC09082024,140706/MR238,2,/ET91456,21329,M080T490/CG293,160,350/FB583/VR32');
});

test('decodes Label H1 Preamble #M1BPOS <invalid>', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_H1_POS(decoder);
Expand Down
6 changes: 3 additions & 3 deletions lib/plugins/Label_H1_POS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class Label_H1_POS extends DecoderPlugin {

qualifiers() { // eslint-disable-line class-methods-use-this
return {
labels: ["H1"],
labels: ["H1", '4J'],
preambles: ['POS', '#M1BPOS', '/.POS'], //TODO - support data before #
};
}
Expand Down Expand Up @@ -91,8 +91,8 @@ export class Label_H1_POS extends DecoderPlugin {

decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = 'partial';
} else if(parts.length === 15) { // variant 6
processUnknown(decodeResult, parts[1]);
} else if(parts.length === 15) { // variant 7
decodeResult.raw.flight_number = parts[1];
let date = undefined;
if(parts[2].startsWith('/DC')) {
date = parts[2].substring(3);
Expand Down
30 changes: 4 additions & 26 deletions lib/plugins/Label_H1_PRG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca
const data = message.text.substring(3, message.text.length-4);
const fields = data.split(',');
if(fields.length === 5) {
const allKnownFields = parseHeader(decodeResult, fields[0]);
const allKnownFields = FlightPlanUtils.parseHeader(decodeResult, fields[0]);
processRunway(decodeResult, fields[1]);
processCurrentFuel(decodeResult, fields[2]);
processETA(decodeResult, fields[3]);
Expand All @@ -37,7 +37,7 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca
decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = allKnownFields ? 'full' : 'partial';
} else if(fields.length === 6) {
const allKnownFields = parseHeader(decodeResult, fields[0]);
const allKnownFields = FlightPlanUtils.parseHeader(decodeResult, fields[0]);
processRunway(decodeResult, fields[1]);
processCurrentFuel(decodeResult, fields[2]);
processETA(decodeResult, fields[3]);
Expand All @@ -47,7 +47,7 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca
decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = allKnownFields ? 'full' : 'partial';
} else if(fields.length === 19) {
const allKnownFields = parseHeader(decodeResult, fields[0]);
const allKnownFields = FlightPlanUtils.parseHeader(decodeResult, fields[0]);
processUnknown(decodeResult, fields[1]);
processFlightNumber(decodeResult, fields[2]);
processDeptApt(decodeResult, fields[3]);
Expand All @@ -59,7 +59,7 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca
decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = 'partial';
} else if(fields.length === 21) {
const allKnownFields = parseHeader(decodeResult, fields[0]);
const allKnownFields = FlightPlanUtils.parseHeader(decodeResult, fields[0]);
processRunway(decodeResult, fields[1]);
processUnknown(decodeResult, fields.slice(2, 21).join(','));
FlightPlanUtils.processFlightPlan(decodeResult, fields[21].split(':'));
Expand All @@ -83,28 +83,6 @@ export class Label_H1_PRG extends DecoderPlugin { // eslint-disable-line camelca

export default {};

function parseHeader(decodeResult: DecodeResult, header: string): boolean {
let allKnownFields = true;
const fields = header.split('/');
// fields[0] is msg type - we already know this
for(let i=1; i<fields.length; ++i) {
if (fields[i].startsWith('FN')) {
decodeResult.raw.flight_number = fields[i].substring(2); // Strip off 'FN'
} else if (fields[i].startsWith('SN')) {
decodeResult.raw.serial_number = fields[i].substring(2); // Strip off 'SN'
} else if (fields[i].startsWith('TS')) {
const ts = fields[i].substring(2).split(',');
decodeResult.raw.message_timestamp = DateTimeUtils.convertDateTimeToEpoch(ts[0],ts[1]);
} else if (fields[i].startsWith('DT')) {
processArrvApt(decodeResult, fields[i].substring(2)) // Strip off 'DT'
} else {
decodeResult.remaining.text += '/' + fields[i];
allKnownFields = false
}
}
return allKnownFields;
};

function processFlightNumber(decodeResult: any, value: string) {
decodeResult.raw.flight_number = value;
};
Expand Down
142 changes: 74 additions & 68 deletions lib/utils/flight_plan_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class FlightPlanUtils {
* @returns whether all fields were processed or not
*/
public static processFlightPlan(decodeResult: any, data: string[]): boolean {
let allKnownFields = parseHeader(decodeResult, data[0]);
let allKnownFields = FlightPlanUtils.parseHeader(decodeResult, data[0]);
for (let i = 1; i < data.length; i += 2) {
const key = data[i];
const value = data[i + 1];
Expand Down Expand Up @@ -56,6 +56,79 @@ export class FlightPlanUtils {
}
return allKnownFields;
}
public static parseHeader(decodeResult: any, header: string): boolean {
let allKnownFields = true;
const fields = header.split('/');
allKnownFields = allKnownFields && parseMessageType(decodeResult, fields[0]);
for (let i = 1; i < fields.length; ++i) {
if (fields[i].startsWith('FN')) {
decodeResult.raw.flight_number = fields[i].substring(2); // Strip off 'FN'
} else if (fields[i].startsWith('SN')) {
decodeResult.raw.serial_number = fields[i].substring(2); // Strip off 'SN'
} else if (fields[i].startsWith('TS')) {
const ts = fields[i].substring(2).split(','); // Strip off PS
let time = DateTimeUtils.convertDateTimeToEpoch(ts[0], ts[1]);

if(Number.isNaN(time)) { // convert DDMMYY to MMDDYY - TODO figure out a better way to determine
const date = ts[1].substring(2,4) + ts[1].substring(0,2) + ts[1].substring(4,6);
time = DateTimeUtils.convertDateTimeToEpoch(ts[0], date);
}
decodeResult.raw.message_timestamp = time;
} else if (fields[i].startsWith('PS')) {
const pos = fields[i].substring(2); // Strip off PS
allKnownFields == allKnownFields && processPosition(decodeResult, pos);
} else if (fields[i].startsWith('DT')) {
const icao = fields[i].substring(2); // Strip off DT
decodeResult.raw.arrival_icao = icao;
decodeResult.formatted.items.push({
type: 'destination',
code: 'DST',
label: 'Destination',
value: decodeResult.raw.arrival_icao,
});
} else if (fields[i].startsWith('ID')) {
const tail = fields[i].substring(2); // Strip off ID
decodeResult.raw.tail = tail;
decodeResult.formatted.items.push({
type: 'tail',
label: 'Tail',
value: decodeResult.raw.tail,
});
} else if (fields[i].startsWith('RF')) {
decodeResult.formatted.items.push({
type: 'status',
code: 'ROUTE_STATUS',
label: 'Route Status',
value: 'Route Filed',
});
decodeResult.raw.route_status = 'RF';
if (fields[i].length > 2) {
addRoute(decodeResult, fields[i].substring(2));
}
} else if (fields[i] == 'RP') {
decodeResult.raw.route_status = 'RP';
decodeResult.formatted.items.push({
type: 'status',
code: 'ROUTE_STATUS',
label: 'Route Status',
value: 'Route Planned',
});
decodeResult.raw.route_status = fields[i];
} else if (fields[i] == 'RI') {
decodeResult.raw.route_status = 'RI';
decodeResult.formatted.items.push({
type: 'status',
code: 'ROUTE_STATUS',
label: 'Route Status',
value: 'Route Inactive',
});
} else {
decodeResult.remaining.text += '/' + fields[i];
allKnownFields = false
}
}
return allKnownFields;
};
}

function parseMessageType(decodeResult: any, messageType: string): boolean {
Expand Down Expand Up @@ -84,73 +157,6 @@ function parseMessageType(decodeResult: any, messageType: string): boolean {
return false;
}

function parseHeader(decodeResult: any, header: string): boolean {
let allKnownFields = true;
const fields = header.split('/');
allKnownFields = allKnownFields && parseMessageType(decodeResult, fields[0]);
for (let i = 1; i < fields.length; ++i) {
if (fields[i].startsWith('FN')) {
decodeResult.raw.flight_number = fields[i].substring(2); // Strip off 'FN'
} else if (fields[i].startsWith('SN')) {
decodeResult.raw.serial_number = fields[i].substring(2); // Strip off 'SN'
} else if (fields[i].startsWith('TS')) {
const ts = fields[i].substring(2).split(','); // Strip off PS
let time = DateTimeUtils.convertDateTimeToEpoch(ts[0], ts[1]);

if(Number.isNaN(time)) { // convert DDMMYY to MMDDYY - TODO figure out a better way to determine
const date = ts[1].substring(2,4) + ts[1].substring(0,2) + ts[1].substring(4,6);
time = DateTimeUtils.convertDateTimeToEpoch(ts[0], date);
}
decodeResult.raw.message_timestamp = time;
} else if (fields[i].startsWith('PS')) {
const pos = fields[i].substring(2); // Strip off PS
allKnownFields == allKnownFields && processPosition(decodeResult, pos);
} else if (fields[i].startsWith('DT')) {
const icao = fields[i].substring(2); // Strip off DT
decodeResult.raw.arrival_icao = icao;
decodeResult.formatted.items.push({
type: 'destination',
code: 'DST',
label: 'Destination',
value: decodeResult.raw.arrival_icao,
});
} else if (fields[i].startsWith('RF')) {
decodeResult.formatted.items.push({
type: 'status',
code: 'ROUTE_STATUS',
label: 'Route Status',
value: 'Route Filed',
});
decodeResult.raw.route_status = 'RF';
if (fields[i].length > 2) {
addRoute(decodeResult, fields[i].substring(2));
}
} else if (fields[i] == 'RP') {
decodeResult.raw.route_status = 'RP';
decodeResult.formatted.items.push({
type: 'status',
code: 'ROUTE_STATUS',
label: 'Route Status',
value: 'Route Planned',
});
decodeResult.raw.route_status = fields[i];
} else if (fields[i] == 'RI') {
decodeResult.raw.route_status = 'RI';
decodeResult.formatted.items.push({
type: 'status',
code: 'ROUTE_STATUS',
label: 'Route Status',
value: 'Route Inactive',
});
} else {
decodeResult.remaining.text += '/' + fields[i];
allKnownFields = false
}
}
return allKnownFields;
};


function processPosition(decodeResult: any, value: string): boolean {
const position = CoordinateUtils.decodeStringCoordinates(value);
if (position) {
Expand Down

0 comments on commit 2fb6482

Please sign in to comment.