Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding H1-FPN decoding #27

Merged
merged 3 commits into from
Feb 14, 2024
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
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');
Comment on lines +31 to +34
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 weird, but right now First Waypoint isn't being taken into context

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
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i should probably discuss before putting out a PR, but sometimes i find it easier when there's code to point at

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)=> RouteUtils.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
16 changes: 15 additions & 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 All @@ -17,6 +17,20 @@ export class RouteUtils {
}
return s;
}

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};
}

// move out if we want public
private static timestampToString(time: number, format: 'tod' | 'epoch'): string {
const date = new Date(time * 1000);
Expand Down
Loading