Skip to content

Commit

Permalink
adding H1-FPN decoding
Browse files Browse the repository at this point in the history
Parses:
* Flight Number
* Route Status
* Departure Airport
* Departure Runway
* Arrivial Airport
* First Waypoint
* Message Checksum

Does not Parse:
* Departure Procedure
* Arrival Procedure
* Current Route(?)

Also does not put multiple 'First Waypoints' into context
  • Loading branch information
makrsmark committed Feb 5, 2024
1 parent a0f1de3 commit f5580b7
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 2 deletions.
1 change: 1 addition & 0 deletions lib/MessageDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class MessageDecoder {
this.registerPlugin(new Plugins.Label_44_ON(this));
this.registerPlugin(new Plugins.Label_44_POS(this));
this.registerPlugin(new Plugins.Label_B6_Forwardslash(this));
this.registerPlugin(new Plugins.Label_H1_FPN(this));
this.registerPlugin(new Plugins.Label_H1_M1BPOS(this));
this.registerPlugin(new Plugins.Label_H1_POS(this));
this.registerPlugin(new Plugins.Label_80(this));
Expand Down
129 changes: 129 additions & 0 deletions lib/plugins/Label_H1_FPN.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { MessageDecoder } from '../MessageDecoder';
import { Label_H1_FPN } from './Label_H1_FPN';

test('decodes Label H1 Preamble FPN landing', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_H1_FPN(decoder);

expect(decoderPlugin.decode).toBeDefined();
expect(decoderPlugin.name).toBe('label-h1-fpn');
expect(decoderPlugin.qualifiers).toBeDefined();
expect(decoderPlugin.qualifiers()).toEqual({
labels: ['H1'],
preambles: ['FPN'],
});

const text = 'FPN/RI:DA:KEWR:AA:KDFW:CR:EWRDFW01(17L)..SAAME.J6.HVQ.Q68.LITTR..MEEOW..FEWWW:A:SEEVR4.FEWWW:F:VECTOR..DISCO..RIVET:AP:ILS 17L.RIVET:F:TACKEC8B5';
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-fpn');
expect(decodeResult.formatted.description).toBe('Flight Plan');
expect(decodeResult.formatted.items.length).toBe(6);
expect(decodeResult.formatted.items[0].label).toBe('Route Status');
expect(decodeResult.formatted.items[0].value).toBe('Route Inactive');
expect(decodeResult.formatted.items[1].label).toBe('Origin');
expect(decodeResult.formatted.items[1].value).toBe('KEWR');
expect(decodeResult.formatted.items[2].label).toBe('Destination');
expect(decodeResult.formatted.items[2].value).toBe('KDFW');
expect(decodeResult.formatted.items[3].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[3].value).toBe('VECTOR >> DISCO >> RIVET');
expect(decodeResult.formatted.items[4].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[4].value).toBe('TACKE');
expect(decodeResult.formatted.items[5].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[5].value).toBe('0xc8b5');
});
test('decodes Label H1 Preamble FPN full flight', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_H1_FPN(decoder);

expect(decoderPlugin.decode).toBeDefined();
expect(decoderPlugin.name).toBe('label-h1-fpn');
expect(decoderPlugin.qualifiers).toBeDefined();
expect(decoderPlugin.qualifiers()).toEqual({
labels: ['H1'],
preambles: ['FPN'],
});

// https://app.airframes.io/messages/2161768398
const text = 'FPN/FNAAL1956/RP:DA:KPHL:AA:KPHX:CR:PHLPHX61:R:27L(26O):D:PHL3:A:EAGUL6.ZUN:AP:ILS26..AIR,N40010W080490.J110.BOWRR..VLA,N39056W089097..STL,N38516W090289..GIBSN,N38430W092244..TYGER,N38410W094050..GCK,N37551W100435..DIXAN,N36169W105573..ZUN,N34579W109093293B';
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-fpn');
expect(decodeResult.raw.flight_number).toBe('AAL1956')
expect(decodeResult.formatted.description).toBe('Flight Plan');
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].label).toBe('Route Status');
expect(decodeResult.formatted.items[0].value).toBe('Route Planned');
expect(decodeResult.formatted.items[1].label).toBe('Origin');
expect(decodeResult.formatted.items[1].value).toBe('KPHL');
expect(decodeResult.formatted.items[2].label).toBe('Destination');
expect(decodeResult.formatted.items[2].value).toBe('KPHX');
expect(decodeResult.formatted.items[3].label).toBe('Runway');
expect(decodeResult.formatted.items[3].value).toBe('27L(26O)');
expect(decodeResult.formatted.items[4].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[4].value).toBe('0x293b');
});

test('decodes Label H1 Preamble FPN in-flight', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_H1_FPN(decoder);

expect(decoderPlugin.decode).toBeDefined();
expect(decoderPlugin.name).toBe('label-h1-fpn');
expect(decoderPlugin.qualifiers).toBeDefined();
expect(decoderPlugin.qualifiers()).toEqual({
labels: ['H1'],
preambles: ['FPN'],
});

// https://app.airframes.io/messages/2161761202
const text = 'FPN/FNUAL1187/RP:DA:KSFO:AA:KPHX:F:KAYEX,N36292W120569..LOSHN,N35509W120000..BOILE,N34253W118016..BLH,N33358W114457DDFB';
const decodeResult = decoderPlugin.decode({ text: text });
console.log(JSON.stringify(decodeResult, null, 2));

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('full');
expect(decodeResult.decoder.name).toBe('label-h1-fpn');
expect(decodeResult.raw.flight_number).toBe('UAL1187')
expect(decodeResult.formatted.description).toBe('Flight Plan');
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].label).toBe('Route Status');
expect(decodeResult.formatted.items[0].value).toBe('Route Planned');
expect(decodeResult.formatted.items[1].label).toBe('Origin');
expect(decodeResult.formatted.items[1].value).toBe('KSFO');
expect(decodeResult.formatted.items[2].label).toBe('Destination');
expect(decodeResult.formatted.items[2].value).toBe('KPHX');
expect(decodeResult.formatted.items[3].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[3].value).toBe('KAYEX(36.292 N, 120.569 W) >> LOSHN(35.509 N, 120 W) >> BOILE(34.253 N, 118.016 W) >> BLH(33.358 N, 114.457 W)');
expect(decodeResult.formatted.items[4].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[4].value).toBe('0xddfb');
});

test('decodes Label H1 Preamble FPN <invalid>', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_H1_FPN(decoder);

expect(decoderPlugin.decode).toBeDefined();
expect(decoderPlugin.name).toBe('label-h1-fpn');
expect(decoderPlugin.qualifiers).toBeDefined();
expect(decoderPlugin.qualifiers()).toEqual({
labels: ['H1'],
preambles: ['FPN'],
});

const text = 'FPN Bogus message';
const decodeResult = decoderPlugin.decode({ text: text });
console.log(JSON.stringify(decodeResult, null, 2));

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.decoder.name).toBe('label-h1-fpn');
expect(decodeResult.formatted.description).toBe('Flight Plan');
expect(decodeResult.message.text).toBe(text);
});
158 changes: 158 additions & 0 deletions lib/plugins/Label_H1_FPN.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { decode } from 'base85';
import { DateTimeUtils } from '../DateTimeUtils';
import { DecoderPlugin } from '../DecoderPlugin';
import { CoordinateUtils } from '../utils/coordinate_utils';
import { RouteUtils } from '../utils/route_utils';

export class Label_H1_FPN extends DecoderPlugin {
name = 'label-h1-fpn';

qualifiers() { // eslint-disable-line class-methods-use-this
return {
labels: ["H1"],
preambles: ['FPN'],
};
}

decode(message: any, options: any = {} ): any {
let decodeResult: any = this.defaultResult;
decodeResult.decoder.name = this.name;
decodeResult.formatted.description = 'Flight Plan';
decodeResult.message = message;

const checksum = message.text.slice(-4);
const data = message.text.slice(0, message.text.length-4).split(':');

if(data[0].startsWith('FPN/')) {
let allKnownFields = parseHeader(decodeResult, data[0]);
for(let i=1; i< data.length; i+=2) {
const key = data[i];
const value = data[i+1];
// TODO: discuss how store commented out bits as both raw and formatted
switch(key) {
// case 'A': // Arrival Procedure (?)
// break;
case 'AA':
addArrivalAirport(decodeResult, value);
break;
// case 'CR': // Current Route (?)
// break;
// case 'D': // Departure Procedure
// break;
case 'DA':
addDepartureAirport(decodeResult, value);
break;
case 'F': // First Waypoint
addRoute(decodeResult, value);
break;
case 'R':
addDepartureRunway(decodeResult, value);
default:
if(allKnownFields) {
decodeResult.remaining.text = '';
allKnownFields = false;
}
decodeResult.remaining.text += `,${data[i]}`;
decodeResult.decoder.decodeLevel = 'partial';
}
}

addChecksum(decodeResult, checksum);
decodeResult.decoded = true;
if(allKnownFields) {
decodeResult.decoder.decodeLevel = 'full';
}
} else {// Unknown
if (options?.debug) {
console.log(`Decoder: Unknown H1 message: ${message.text}`);
}
decodeResult.remaining.text = message.text;
decodeResult.decoded = false;
decodeResult.decoder.decodeLevel = 'none';
}
return decodeResult;
}
}

export default {};

function parseHeader(decodeResult: any, header: string): boolean {
let allKnownFields = true;
const fields = header.split('/');
if(fields.length == 3) {
decodeResult.raw.flight_number = fields[1].substring(2); // Strip off 'FN'
} else if(fields.length > 3) {
decodeResult.remaining.text = fields.slice(2,-1).join('/');
allKnownFields = false
}
decodeResult.raw.route_status = fields[fields.length - 1];

var text;
if(decodeResult.raw.route_status == 'RP'){
text = 'Route Planned';
} else if(decodeResult.raw.route_status == 'RI') {
text = 'Route Inactive';
} else {
text = decodeResult.raw.route_status;
}
decodeResult.formatted.items.push({
type: 'status',
code: 'ROUTE_STATUS',
label: 'Route Status',
value: text,
});
return allKnownFields;
};

function addChecksum(decodeResult: any, 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),
});
};

function addArrivalAirport(decodeResult: any, value: string) {
decodeResult.raw.arrival_icao = value;
decodeResult.formatted.items.push({
type: 'destination',
code: 'DST',
label: 'Destination',
value: decodeResult.raw.arrival_icao,
});
};

function addDepartureAirport(decodeResult: any, value: string) {
decodeResult.raw.departure_icao = value;
decodeResult.formatted.items.push({
type: 'origin',
code: 'ORG',
label: 'Origin',
value: decodeResult.raw.departure_icao,
});
};

function addDepartureRunway(decodeResult: any, value: string) {
decodeResult.raw.runway = value;
decodeResult.formatted.items.push({
type: 'runway',
label: 'Runway',
value: decodeResult.raw.runway,
});
};

function addRoute(decodeResult: any, value: string) {
const route = value.split('.');
decodeResult.raw.route = route.map((leg)=> CoordinateUtils.getWaypoint(leg));
decodeResult.formatted.items.push({
type: 'aircraft_route',
code: 'ROUTE',
label: 'Aircraft Route',
value: RouteUtils.routeToString(decodeResult.raw.route),
});
};



3 changes: 2 additions & 1 deletion lib/plugins/official.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './Label_12_N_Space';
export * from './Label_15';
export * from './Label_15_FST';
export * from './Label_16_N_Space';
export * from './Label_1M_Slash';
export * from './Label_20_POS';
export * from './Label_30_Slash_EA';
export * from './Label_44_ETA';
Expand All @@ -14,9 +15,9 @@ export * from './Label_80';
export * from './Label_8E';
export * from './Label_B6';
export * from './Label_ColonComma';
export * from './Label_H1_FPN';
export * from './Label_H1_M1BPOS';
export * from './Label_H1_M1BPRG';
export * from './Label_1M_Slash';
export * from './Label_H1_POS';
export * from './Label_SQ';
export * from './Label_QR';
Expand Down
15 changes: 15 additions & 0 deletions lib/utils/coordinate_utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Waypoint } from "../types/waypoint";

export class CoordinateUtils {
public static decodeStringCoordinates(stringCoords: String) : any { // eslint-disable-line class-methods-use-this
var results : any = {};
Expand All @@ -21,6 +23,19 @@ export class CoordinateUtils {
return results;
}

public static getWaypoint(leg: string): Waypoint {
const waypoint = leg.split(',');
if(waypoint.length ==2) {
const position = CoordinateUtils.decodeStringCoordinates(waypoint[1]);
return {name: waypoint[0], latitude: position.latitude, longitude: position.longitude};
}
if(leg.length == 14) { //looks like coordinates
const position = CoordinateUtils.decodeStringCoordinates(leg);
return {name: waypoint[0], latitude: position.latitude, longitude: position.longitude};
}
return {name: leg};
}

public static coordinateString(coords: any) : String {
return `${Math.abs(coords.latitude)} ${coords.latitudeDirection}, ${Math.abs(coords.longitude)} ${coords.longitudeDirection}`;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/route_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CoordinateUtils } from "./coordinate_utils";
export class RouteUtils {

public static routeToString(route: Waypoint[]): string {
return route.map((x) => RouteUtils.waypointToString(x)).join( ' > ');
return route.map((x) => RouteUtils.waypointToString(x)).join( ' > ').replaceAll('> >', '>>');
}

public static waypointToString(waypoint: Waypoint): string {
Expand Down

0 comments on commit f5580b7

Please sign in to comment.