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

more FPN parsing #29

Merged
merged 9 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
84 changes: 69 additions & 15 deletions lib/plugins/Label_H1_FPN.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,28 @@ test('decodes Label H1 Preamble FPN landing', () => {
console.log(JSON.stringify(decodeResult, null, 2));

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.decodeLevel).toBe('full');
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.length).toBe(9);
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');
expect(decodeResult.formatted.items[3].label).toBe('Company Route');
expect(decodeResult.formatted.items[3].value).toBe('EWRDFW01(17L): >> SAAME > J6 > HVQ > Q68 > LITTR >> MEEOW >> FEWWW');
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's a little funky, open to suggestions to changes in route_utils.ts

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is fine for now. Once we see how the formatted versions look on Airframes and AcarsHub and/or get other feedback, we can always refine it. I'll keep you posted.

expect(decodeResult.formatted.items[4].label).toBe('Arrival Procedure');
expect(decodeResult.formatted.items[4].value).toBe('SEEVR4 starting at FEWWW');
expect(decodeResult.formatted.items[5].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[5].value).toBe('VECTOR >> DISCO >> RIVET');
expect(decodeResult.formatted.items[6].label).toBe('Approach Procedure');
expect(decodeResult.formatted.items[6].value).toBe('ILS 17L starting at RIVET');
expect(decodeResult.formatted.items[7].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[7].value).toBe('TACKE');
expect(decodeResult.formatted.items[8].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[8].value).toBe('0xc8b5');
});
test('decodes Label H1 Preamble FPN full flight', () => {
const decoder = new MessageDecoder();
Expand All @@ -53,21 +59,30 @@ test('decodes Label H1 Preamble FPN full flight', () => {
console.log(JSON.stringify(decodeResult, null, 2));

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.decoder.decodeLevel).toBe('full');
expect(decodeResult.decoder.name).toBe('label-h1-fpn');
expect(decodeResult.raw.flight_number).toBe('AAL1956')
expect(decodeResult.raw.flight_number).toBe('AAL1956');
expect(decodeResult.raw.company_route.waypoints).toBeUndefined();
expect(decodeResult.formatted.description).toBe('Flight Plan');
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items.length).toBe(9);
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');
expect(decodeResult.formatted.items[3].label).toBe('Company Route');
expect(decodeResult.formatted.items[3].value).toBe('PHLPHX61');
expect(decodeResult.formatted.items[4].label).toBe('Runway');
expect(decodeResult.formatted.items[4].value).toBe('27L(26O)');
expect(decodeResult.formatted.items[5].label).toBe('Departure Procedure');
expect(decodeResult.formatted.items[5].value).toBe('PHL3');
expect(decodeResult.formatted.items[6].label).toBe('Arrival Procedure');
expect(decodeResult.formatted.items[6].value).toBe('EAGUL6 starting at ZUN');
expect(decodeResult.formatted.items[7].label).toBe('Approach Procedure');
expect(decodeResult.formatted.items[7].value).toBe('ILS26: >> AIR(40.01 N, 80.49 W) > J110 > BOWRR >> VLA(39.056 N, 89.097 W) >> STL(38.516 N, 90.289 W) >> GIBSN(38.43 N, 92.244 W) >> TYGER(38.41 N, 94.05 W) >> GCK(37.551 N, 100.435 W) >> DIXAN(36.169 N, 105.573 W) >> ZUN(34.579 N, 109.093 W)');
makrsmark marked this conversation as resolved.
Show resolved Hide resolved
expect(decodeResult.formatted.items[8].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[8].value).toBe('0x293b');
});

test('decodes Label H1 Preamble FPN in-flight', () => {
Expand Down Expand Up @@ -105,6 +120,45 @@ test('decodes Label H1 Preamble FPN in-flight', () => {
expect(decodeResult.formatted.items[4].value).toBe('0xddfb');
});


test('decodes Label H1 Preamble with WS', () => {
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/2372685289
const text = 'FPN/TS140017,021724/RP:DA:EHAM:AA:KMSP..N55064W000477..N55163W001141..ERAKA..N60000W020000..N61000W030000:WS:N61000W030000,370..N61000W040000..N60000W050000..URTAK:WS:URTAK,380..LAKES:WS:LAKES,400..N57000W070000..N54300W080000..N49000W090000..DLH..COLDD:A:BAINY3:AP:ILS30L(30L)/PR4356,344,360,1060,,,13,,,30,,,P50,M40,36090,,3296,292/DTKMSP,30L,172,215117156D';
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 one is a beast - not sure how to approach it. adding it as a reference

Copy link
Contributor

Choose a reason for hiding this comment

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

I like how you added a comment to the URL of the message for further info if we need it later.

Yeah, this is huge.

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('140017,021724') // FIXME - this is a timestamp (2024-02-17T14:00:17.000Z)
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 Planned');
expect(decodeResult.formatted.items[1].label).toBe('Origin');
expect(decodeResult.formatted.items[1].value).toBe('EHAM');
expect(decodeResult.formatted.items[2].label).toBe('Destination');
expect(decodeResult.formatted.items[2].value).toBe('KMSP..N55064W000477..N55163W001141..ERAKA..N60000W020000..N61000W030000'); // FIXME - just 'KMSP'
expect(decodeResult.formatted.items[3].label).toBe('Arrival Procedure');
expect(decodeResult.formatted.items[3].value).toBe('BAINY3');
expect(decodeResult.formatted.items[4].label).toBe('Approach Procedure');
expect(decodeResult.formatted.items[4].value).toBe('ILS30L(30L)/PR4356,344,360,1060,,,13,,,30,,,P50,M40,36090,,3296,292/DTKMSP,30L,172,215117'); //FIXME - just 'ILS30L'
expect(decodeResult.formatted.items[5].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[5].value).toBe('0x156d');
expect(decodeResult.remaining.text).toBe(':WS:N61000W030000,370..N61000W040000..N60000W050000..URTAK:WS:URTAK,380..LAKES:WS:LAKES,400..N57000W070000..N54300W080000..N49000W090000..DLH..COLDD');
});

test('decodes Label H1 Preamble FPN <invalid>', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_H1_FPN(decoder);
Expand Down
74 changes: 66 additions & 8 deletions lib/plugins/Label_H1_FPN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,21 @@ export class Label_H1_FPN extends DecoderPlugin {
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 'A': // Arrival Procedure (?)
addProcedure(decodeResult, value, 'arrival');
break;
case 'AA':
addArrivalAirport(decodeResult, value);
break;
// case 'CR': // Current Route (?)
// break;
// case 'D': // Departure Procedure
// break;
case 'AP':
addProcedure(decodeResult, value, 'approach');
break;
case 'CR':
addCompanyRoute(decodeResult, value);
break;
case 'D': // Departure Procedure
addProcedure(decodeResult, value, 'departure');
break;
case 'DA':
addDepartureAirport(decodeResult, value);
break;
Expand All @@ -47,12 +53,15 @@ export class Label_H1_FPN extends DecoderPlugin {
break;
case 'R':
addDepartureRunway(decodeResult, value);
break;
// case 'WS': // something about routes, has altitude, so current parsing won't work
// break;
default:
if(allKnownFields) {
decodeResult.remaining.text = '';
allKnownFields = false;
}
decodeResult.remaining.text += `:${data[i]}`;
decodeResult.remaining.text += `:${key}:${value}`;
decodeResult.decoder.decodeLevel = 'partial';
}
}
Expand Down Expand Up @@ -124,6 +133,55 @@ function addArrivalAirport(decodeResult: any, value: string) {
});
};

function addProcedure(decodeResult: any, value: string, type: string) {
if(decodeResult.raw.procedures === undefined) {
decodeResult.raw.procedures = [];
}
const data = value.split('.');
let waypoints;
if(data.length>1) {
waypoints = data.slice(1).map((leg)=> RouteUtils.getWaypoint(leg));
}
const route = {name: data[0], waypoints: waypoints};
decodeResult.raw.procedures.push({type: type, route: route});
const procedureName = type.substring(0,1).toUpperCase() + type.slice(1);
let procedureValue = route.name;
decodeResult.formatted.items.push({
type: `procedure`,
code: 'proc',
label: `${procedureName} Procedure`,
value: RouteUtils.routeToString(route),
});
};

function addCompanyRoute(decodeResult: any, value: string) {
const segments = value.split('.');
const parens_idx = segments[0].indexOf('(');
let name;
let runway;
if(parens_idx === -1) {
name = segments[0];
} else {
name = segments[0].slice(0, parens_idx);
runway = segments[0].slice(parens_idx+1, segments[0].indexOf(')'));
}
let waypoints;
if(segments.length > 1) {
waypoints = segments.slice(1).map((leg) => RouteUtils.getWaypoint(leg));
}
decodeResult.raw.company_route = {
name: name,
runway: runway,
waypoints: waypoints,
};
decodeResult.formatted.items.push({
type: 'company_route',
code: 'CR',
label: 'Company Route',
value: RouteUtils.routeToString(decodeResult.raw.company_route),
});
};

function addDepartureAirport(decodeResult: any, value: string) {
decodeResult.raw.departure_icao = value;
decodeResult.formatted.items.push({
Expand All @@ -145,7 +203,7 @@ function addDepartureRunway(decodeResult: any, value: string) {

function addRoute(decodeResult: any, value: string) {
const route = value.split('.');
decodeResult.raw.route = route.map((leg)=> RouteUtils.getWaypoint(leg));
decodeResult.raw.route = {waypoints: route.map((leg)=> RouteUtils.getWaypoint(leg))};
decodeResult.formatted.items.push({
type: 'aircraft_route',
code: 'ROUTE',
Expand Down
57 changes: 57 additions & 0 deletions lib/plugins/Label_H1_M1BPOS.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { MessageDecoder } from '../MessageDecoder';
import { Label_H1_M1BPOS } from './Label_H1_M1BPOS';

test('decodes Label H1 Preamble #M1BPOS', () => {
const decoder = new MessageDecoder();
const decoderPlugin = new Label_H1_M1BPOS(decoder);

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

// https://app.airframes.io/messages/2366921571
const text = '#M1BPOSN29510W098448,RW04,140407,188,TATAR,4,140445,ALISS,M12,246048,374K,282K,1223,133,KSAT,KELP,,70,151437,415,73/PR1223,222,240,133,,44,40,252074,M22,180,P0,P0/RI:DA:KSAT:AA:KELP..TATAR:D:ALISS6:F:ALISS..FST';
const decodeResult = decoderPlugin.decode({ text: text });
console.log(JSON.stringify(decodeResult, null, 2));

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('none'); //should be partial
expect(decodeResult.decoder.name).toBe('label-h1-m1bpos');
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.formatted.items.length).toBe(2);
expect(decodeResult.formatted.items[0].label).toBe('Position');
expect(decodeResult.formatted.items[0].value).toBe('29.51 N, 98.448 W');
expect(decodeResult.formatted.items[1].label).toBe('Route');
expect(decodeResult.formatted.items[1].value).toBe('RW04 > TATAR > 4 > ALISS > M12 > KSAT > KELP > ?');
expect(decodeResult.remaining.text).toBe('PR1223,222,240,133,,44,40,252074,M22,180,P0,P0');
});

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

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

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

expect(decodeResult.decoded).toBe(true); // expect false?
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.decoder.name).toBe('label-h1-m1bpos');
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.formatted.items.length).toBe(1); // should be 0
expect(decodeResult.formatted.items[0].label).toBe('Route');
expect(decodeResult.formatted.items[0].value).toBe('');

});
9 changes: 5 additions & 4 deletions lib/plugins/Label_H1_M1BPOS.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DecoderPlugin } from '../DecoderPlugin';
import { CoordinateUtils } from '../utils/coordinate_utils';
import { RouteUtils } from '../utils/route_utils';

export class Label_H1_M1BPOS extends DecoderPlugin { // eslint-disable-line camelcase
name = 'label-h1-m1bpos';
Expand Down Expand Up @@ -33,16 +34,16 @@ export class Label_H1_M1BPOS extends DecoderPlugin { // eslint-disable-line came
});
}

let route = items.slice(1).filter((part: any) => !/^\d(.+)$/.test(part));
route = route.map((hop: any) => hop || '?');
decodeResult.raw.route = route;
const route = items.slice(1).filter((part: any) => !/^\d(.+)$/.test(part));
const waypoints = route.map((hop: any) => RouteUtils.getWaypoint(hop || '?'));
decodeResult.raw.route = {waypoints: waypoints};

decodeResult.formatted.description = 'Position Report';

decodeResult.formatted.items.push({
type: 'route',
label: 'Route',
value: `${route.join(' > ')}`,
value: RouteUtils.routeToString(decodeResult.raw.route),
});

decodeResult.decoded = true;
Expand Down
18 changes: 10 additions & 8 deletions lib/plugins/Label_H1_POS.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DateTimeUtils } from '../DateTimeUtils';
import { DecoderPlugin } from '../DecoderPlugin';
import { Waypoint } from '../types/waypoint';
import { RouteUtils } from '../utils/route_utils';

export class Label_H1_POS extends DecoderPlugin {
Expand All @@ -24,7 +25,7 @@ export class Label_H1_POS extends DecoderPlugin {
const fields = data.split(',');

if(fields.length==1 && data.startsWith('/RF')) {
decodeResult.raw.route = data.substring(3,data.length).split('.').map((leg: string) => {return {name: leg}});
decodeResult.raw.route = {waypoints: data.substring(3,data.length).split('.').map((leg: string) => RouteUtils.getWaypoint(leg))};
decodeResult.formatted.items.push({
type: 'aircraft_route',
code: 'ROUTE',
Expand Down Expand Up @@ -126,16 +127,17 @@ export class Label_H1_POS extends DecoderPlugin {
longitude: decodeResult.raw.longitude * (decodeResult.raw.longitude_direction === 'W' ? -1 : 1),
};

let waypoints : Waypoint[];
if(fields.length == 11) {//variant 1
decodeResult.raw.route = [{name: fields[1] || '?,', time: convertDateTimeToEpoch(fields[2], fields[10]), timeFormat: 'epoch'},
{name: fields[4] || '?', time: convertDateTimeToEpoch(fields[5], fields[10]), timeFormat: 'epoch'},
{name: fields[6] || '?'}];
waypoints = [{name: fields[1] || '?,', time: convertDateTimeToEpoch(fields[2], fields[10]), timeFormat: 'epoch'},
{name: fields[4] || '?', time: convertDateTimeToEpoch(fields[5], fields[10]), timeFormat: 'epoch'},
{name: fields[6] || '?'}]
} else {
decodeResult.raw.route = [{name: fields[1] || '?,', time: convertHHMMSSToTod(fields[2]), timeFormat: 'tod'},
{name: fields[4] || '?', time: convertHHMMSSToTod(fields[5]), timeFormat: 'tod'},
{name: fields[6] || '?'}];
waypoints = [{name: fields[1] || '?,', time: convertHHMMSSToTod(fields[2]), timeFormat: 'tod'},
{name: fields[4] || '?', time: convertHHMMSSToTod(fields[5]), timeFormat: 'tod'},
{name: fields[6] || '?'}];
}

decodeResult.raw.route = {waypoints: waypoints};
decodeResult.raw.outside_air_temperature = Number(fields[7].substring(1)) * (fields[7].charAt(0) === 'M' ? -1 : 1);

decodeResult.formatted.items.push({
Expand Down
17 changes: 17 additions & 0 deletions lib/types/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Waypoint } from "./waypoint";

/**
* Representation of a route
*
* Typically a list of waypoints, this can also be a named company route.
*/
export interface Route {
/** optional name. If not set, `waypoints` is required */
name?: string,

/** optional runway */
runway?: string,

/** optional list of waypoints. If not set, `name` is required */
waypoints?: Waypoint[],
}
Loading
Loading