Skip to content

Commit

Permalink
Adding H1 / Decoding (#199)
Browse files Browse the repository at this point in the history
- currently only parsing POS (not sure if there's others)
- refactored H1 Helper to pull up common methods
- fixed H1 to stop parsing when message type isn't known
- fixed route generation for /PS
  • Loading branch information
makrsmark authored Nov 20, 2024
1 parent ea7ae0a commit cf66086
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 101 deletions.
1 change: 1 addition & 0 deletions lib/MessageDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class MessageDecoder {
this.registerPlugin(new Plugins.Label_H1_OHMA(this));
this.registerPlugin(new Plugins.Label_H1_WRN(this));
this.registerPlugin(new Plugins.Label_H1(this));
this.registerPlugin(new Plugins.Label_H1_Slash(this));
this.registerPlugin(new Plugins.Label_H1_StarPOS(this));
this.registerPlugin(new Plugins.Label_HX(this));
this.registerPlugin(new Plugins.Label_80(this));
Expand Down
73 changes: 51 additions & 22 deletions lib/plugins/Label_H1_POS.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ describe('Label_H1 POS', () => {
expect(decodeResult.raw.position.latitude).toBe(45.348333333333336);
expect(decodeResult.raw.position.longitude).toBe(-122.91666666666667);
expect(decodeResult.raw.altitude).toBe(13400);
expect(decodeResult.raw.groundspeed).toBe(366);
expect(decodeResult.raw.outside_air_temperature).toBe(-6);
expect(decodeResult.formatted.items.length).toBe(6);
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].type).toBe('aircraft_position');
expect(decodeResult.formatted.items[0].code).toBe('POS');
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
Expand All @@ -75,12 +74,8 @@ describe('Label_H1 POS', () => {
expect(decodeResult.formatted.items[3].code).toBe('OATEMP');
expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[3].value).toBe('-6 degrees');
expect(decodeResult.formatted.items[4].type).toBe('aircraft_groundspeed');
expect(decodeResult.formatted.items[4].code).toBe('GSPD');
expect(decodeResult.formatted.items[4].label).toBe('Aircraft Groundspeed');
expect(decodeResult.formatted.items[4].value).toBe('366 knots');
expect(decodeResult.formatted.items[5].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[5].value).toBe('0x0a5b');
expect(decodeResult.formatted.items[4].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[4].value).toBe('0x0a5b');
});

test('variant 3', () => {
Expand Down Expand Up @@ -428,32 +423,66 @@ describe('Label_H1 POS', () => {
expect(decodeResult.remaining.text).toBe('290016,191/PR1496,150,370,191,,55,10,248028,M47,30,P19,P0/FHCIV,105208,273K,3226,175,M41,252027,450,N,221,62.MEDIL,105411,267K,3439,172,M44,250028,459,N,203,15.PITHI,105533,259K,3584,170,M47,249028,456,N,203,10.LESDO,105859,252K,3700,167,M47,248028,456,N,203,25.KOVIN,110153,252K,3700,164,M47,248028,456,N,203,21.DUCRA,110705,252K,3700,160,M47,248028,456,N,213,37.RESMI,111101,251K,3700,156,M47,248028,455,N,213,28.DEKOD,111325,251K,3700,154,M47,248028,455,N,192,17.DISAK,111438,251K,3700,153,M47,248028,454,N,172,9.DIRMO,112306,251K,3700,145,M47,248028,454,N,178,63.ETAMO,112514,250K,3700,143,M47,248028,453,N,158,16.ADEKA,113339,250K,3700,136,M47,248028,454,N,147,64.MOKDI,114139,251K,3700,129,M47,248028,454,N,181,59.MEN,114429,251K,3700,127,M47,248028,454,N,181,21.BADAM,114843,251K,3700,123,M47,248028,454,N,179,31.KANIG,120154,250K,3700,111,M47,248028,453,N,185,97.KENAS,121800,250K,3700,98,M47,248028,453,N,177,119.POS,122257,250K,3018,96,M45,248023,395,N,182,34.LEIB,124503,150K,2,89,P15,000000,161,N,231,103,LEIB,,89,124503,73');
});

test('/.POS variant 2', () => {

test('does not decode /.POS', () => {
// https://app.airframes.io/messages/2500488708
const text = '/.POS/TS100316,210324/PSS35333W058220,,100316,250,S37131W059150,101916,S39387W060377,M23,27282,241,780,MANUAL,0,813E711';
const decodeResult = plugin.decode({ text: text });

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.formatted.description).toBe('Unknown H1 Message');
});

test('decodes duplicate data', () => {
const text = 'POSN39220W078258,MRB18,044034,340,EYT19,044427,COL20,M49,27369,436,813/PSN39220W078258,MRB18,044034,340,EYT19,044427,COL20,M49,27369,436,813,ECON CRZ,0,25140035'
const decodeResult = plugin.decode({ text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.formatted.description).toBe('Unknown H1 Message');
expect(decodeResult.raw.message_timestamp).toBe(1711015396);
expect(decodeResult.formatted.items.length).toBe(6);
expect(decodeResult.formatted.description).toBe('Position Report');

expect(decodeResult.formatted.items.length).toBe(9);
expect(decodeResult.formatted.items[0].type).toBe('aircraft_position');
expect(decodeResult.formatted.items[0].code).toBe('POS');
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[0].value).toBe('35.555 S, 58.367 W');
expect(decodeResult.formatted.items[0].value).toBe('39.367 N, 78.430 W');
expect(decodeResult.formatted.items[1].type).toBe('altitude');
expect(decodeResult.formatted.items[1].code).toBe('ALT');
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
expect(decodeResult.formatted.items[1].value).toBe('25000 feet');
expect(decodeResult.formatted.items[1].value).toBe('34000 feet');
expect(decodeResult.formatted.items[2].type).toBe('aircraft_route');
expect(decodeResult.formatted.items[2].code).toBe('ROUTE');
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[2].value).toBe('(37.218 S, 59.250 W)@10:03:16 > (39.645 S, 60.628 W)@10:19:16 > ?');
expect(decodeResult.formatted.items[2].value).toBe('MRB18@04:40:34 > EYT19@04:44:27 > COL20');
expect(decodeResult.formatted.items[3].type).toBe('outside_air_temperature');
expect(decodeResult.formatted.items[3].code).toBe('OATEMP');
expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[3].value).toBe('-23 degrees');
expect(decodeResult.formatted.items[4].label).toBe('Aircraft Groundspeed');
expect(decodeResult.formatted.items[4].value).toBe('780 knots');
expect(decodeResult.formatted.items[5].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[5].value).toBe('0xe711');
expect(decodeResult.remaining.text).toBe('.POS,,27282,241,MANUAL,0,813');
expect(decodeResult.formatted.items[3].value).toBe('-49 degrees');
// YAY DUPLICATES!
expect(decodeResult.formatted.items[4].type).toBe('aircraft_position');
expect(decodeResult.formatted.items[4].code).toBe('POS');
expect(decodeResult.formatted.items[4].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[4].value).toBe('39.367 N, 78.430 W');
expect(decodeResult.formatted.items[5].type).toBe('altitude');
expect(decodeResult.formatted.items[5].code).toBe('ALT');
expect(decodeResult.formatted.items[5].label).toBe('Altitude');
expect(decodeResult.formatted.items[5].value).toBe('34000 feet');
expect(decodeResult.formatted.items[6].type).toBe('aircraft_route');
expect(decodeResult.formatted.items[6].code).toBe('ROUTE');
expect(decodeResult.formatted.items[6].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[6].value).toBe('MRB18@04:40:34 > EYT19@04:44:27 > COL20');
expect(decodeResult.formatted.items[7].type).toBe('outside_air_temperature');
expect(decodeResult.formatted.items[7].code).toBe('OATEMP');
expect(decodeResult.formatted.items[7].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[7].value).toBe('-49 degrees');
// And then the end
expect(decodeResult.formatted.items[8].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[8].value).toBe('0x0035');
expect(decodeResult.remaining.text).toBe('27369,436,27369,436,813,ECON CRZ,0,2514');

});


test('decodes Label H1 Preamble #M1BPOS <invalid>', () => {

const text = '#M1BPOS Bogus message';
Expand Down
99 changes: 99 additions & 0 deletions lib/plugins/Label_H1_Slash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { decode } from 'punycode';
import { MessageDecoder } from '../MessageDecoder';
import { Label_H1_Slash } from './Label_H1_Slash';

describe('Label H1 /', () => {
let plugin: Label_H1_Slash;

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

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

test('decodes variant 1', () => {
// https://app.airframes.io/messages/2500488708
const text = '/.POS/TS100316,210324/PSS35333W058220,,100316,250,S37131W059150,101916,S39387W060377,M23,27282,241,780,MANUAL,0,813E711';
const decodeResult = plugin.decode({ text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.raw.message_timestamp).toBe(1711015396);
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[0].value).toBe('35.555 S, 58.367 W');
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
expect(decodeResult.formatted.items[1].value).toBe('25000 feet');
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[2].value).toBe('@10:03:16 > (37.218 S, 59.250 W)@10:19:16 > (39.645 S, 60.628 W)');
expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[3].value).toBe('-23 degrees');
expect(decodeResult.formatted.items[4].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[4].value).toBe('0xe711');
expect(decodeResult.remaining.text).toBe('27282,241,780,MANUAL,0,813');
});

test('decodes variant 2', () => {
const text = '/HDQDLUA.POSN38332W080082,RONZZ,135753,320,LEVII,140454,WISTA,M45,20967,194/GAHDQDLUA/CA/TS135753,1411240721';
const decodeResult = plugin.decode({ text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.raw.message_timestamp).toBe(1731592673);
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[0].value).toBe('38.553 N, 80.137 W');
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
expect(decodeResult.formatted.items[1].value).toBe('32000 feet');
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[2].value).toBe('RONZZ@13:57:53 > LEVII@14:04:54 > WISTA');
expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[3].value).toBe('-45 degrees');
expect(decodeResult.formatted.items[4].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[4].value).toBe('0x0721');
expect(decodeResult.remaining.text).toBe('HDQDLUA,20967,194/GAHDQDLUA/CA');
});

test('decodes variant 3', () => {
const text = '/.POS/TS140122,141124N38321W078003,,140122,450,,140122,,M56,24739,127,8306763';
const decodeResult = plugin.decode({ text: text });

expect(decodeResult.decoded).toBe(true);
expect(decodeResult.decoder.decodeLevel).toBe('partial');
expect(decodeResult.formatted.description).toBe('Position Report');
expect(decodeResult.raw.message_timestamp).toBe(1731592882);
expect(decodeResult.formatted.items.length).toBe(5);
expect(decodeResult.formatted.items[0].label).toBe('Aircraft Position');
expect(decodeResult.formatted.items[0].value).toBe('38.535 N, 78.005 W');
expect(decodeResult.formatted.items[1].label).toBe('Altitude');
expect(decodeResult.formatted.items[1].value).toBe('45000 feet');
expect(decodeResult.formatted.items[2].label).toBe('Aircraft Route');
expect(decodeResult.formatted.items[2].value).toBe('@14:01:22 > @14:01:22 > ?');
expect(decodeResult.formatted.items[3].label).toBe('Outside Air Temperature (C)');
expect(decodeResult.formatted.items[3].value).toBe('-56 degrees');
expect(decodeResult.formatted.items[4].label).toBe('Message Checksum');
expect(decodeResult.formatted.items[4].value).toBe('0x6763');
expect(decodeResult.remaining.text).toBe('24739,127');
});

test('does not decode invalid', () => {

const text = '/.POS Bogus message';
const decodeResult = plugin.decode({ text: text });

expect(decodeResult.decoded).toBe(false);
expect(decodeResult.decoder.decodeLevel).toBe('none');
expect(decodeResult.message.text).toBe(text);
});
});
79 changes: 79 additions & 0 deletions lib/plugins/Label_H1_Slash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { DateTimeUtils } from '../DateTimeUtils';
import { DecoderPlugin } from '../DecoderPlugin';
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
import { Waypoint } from '../types/waypoint';
import { CoordinateUtils } from '../utils/coordinate_utils';
import { H1Helper } from '../utils/h1_helper';
import { ResultFormatter } from '../utils/result_formatter';
import { RouteUtils } from '../utils/route_utils';

export class Label_H1_Slash extends DecoderPlugin {
name = 'label-h1-slash';
qualifiers() { // eslint-disable-line class-methods-use-this
return {
labels: ['H1'],
preambles: ['/']
};
}

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

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

const fields = data.split('/');

if(fields[0] !== '') {
ResultFormatter.unknown(decodeResult, message.text);
decodeResult.decoded = false;
decodeResult.decoder.decodeLevel = 'none';
return decodeResult;
}

const headerData = fields[1].split('.');
ResultFormatter.unknown(decodeResult, headerData[0]);
if(headerData[1] === 'POS' && fields[2].startsWith('TS') && fields[2].length > 15) {
// variant 3 hack
// rip out the timestamp and process the rest
H1Helper.processPosition(decodeResult, fields[2].substring(15).split(','));
} else if(headerData[1] === 'POS') {
// do nothing
} else if(headerData[1].startsWith('POS')) {
H1Helper.processPosition(decodeResult, headerData[1].substring(3).split(','));
} else {
ResultFormatter.unknown(decodeResult, headerData[1], '.');
}

for(let i=2; i<fields.length; i++) {
const field = fields[i];
if(field.startsWith('TS')) {
H1Helper.processTS(decodeResult, field.substring(2,15).split(','));
} else if(field.startsWith('PS')) {
H1Helper.processPS(decodeResult, field.substring(2).split(','));
} else {
ResultFormatter.unknown(decodeResult, field, '/');
}
}

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

ResultFormatter.checksum(decodeResult, checksum);
decodeResult.decoded = true;
decodeResult.decoder.decodeLevel = !decodeResult.remaining.text ? 'full' : 'partial';
return decodeResult;
}
}

export default {};
1 change: 1 addition & 0 deletions lib/plugins/official.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from './Label_ColonComma';
export * from './Label_H1';
export * from './Label_H1_FLR';
export * from './Label_H1_OHMA';
export * from './Label_H1_Slash';
export * from './Label_H1_StarPOS';
export * from './Label_H1_WRN';
export * from './Label_HX';
Expand Down
Loading

0 comments on commit cf66086

Please sign in to comment.