Skip to content

Commit

Permalink
feat: added TXT parser
Browse files Browse the repository at this point in the history
  • Loading branch information
Totto16 committed Oct 4, 2024
1 parent deb0f32 commit 668b1cc
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 5 deletions.
10 changes: 9 additions & 1 deletion gps.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ declare class GPS {
* @param line NMEA string
* @returns NMEA object or False
*/
static Parse<T = any>(line: string): false | T;
static Parse<T = any>(line: string, GPSObject?: GPS | undefined): false | T;

/**
* Calculates the distance between two geo-coordinates using Haversine formula
Expand Down Expand Up @@ -90,6 +90,7 @@ declare namespace GPS {
[key: string]: any;
processed: number;
errors: number;
txtBuffer: Record<string, string[]>

time?: Date;
lat?: number;
Expand Down Expand Up @@ -229,4 +230,11 @@ declare namespace GPS {
valid: boolean;
type: 'HDT';
}

export interface TXT {
message: string | null
completed: boolean,
rawMessages: string[],
sentenceAmount: number,
}
}
182 changes: 178 additions & 4 deletions src/gps.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,50 @@ function parseDist(num, unit) {
throw new Error('Unknown unit: ' + unit);
}


/**
* @description Escapes a string, according to spec
* @see https://www.plaisance-pratique.com/IMG/pdf/NMEA0183-2.pdf Section 5.1.3
*
* @param {string} str string to escape
* @returns {string}
*/
function escapeString(str){
// invalid characters according to:
// https://www.plaisance-pratique.com/IMG/pdf/NMEA0183-2.pdf
// Section 6.1 - Table 1
const invalidCharacters = [
"\r",
"\n",
"$",
"*",
",",
"!",
"\\",
// this is excluded, as it is removed later, since identifies escape sequences
//"^"
"~",
"\u007F", // <DEL>
]

for (const invalidCharacter of invalidCharacters) {
if (str.includes(invalidCharacter)) {
throw new Error(
`Message may not contain invalid Character '${invalidCharacter}'`
)
}
}

// escaping according to https://www.plaisance-pratique.com/IMG/pdf/NMEA0183-2.pdf
// Section 5.1.3
return str.replaceAll(
/\^([a-zA-Z0-9]{2})/g,
(fullMatch, escapeSequence) => {
return String.fromCharCode(parseInt(escapeSequence, 16))
}
)
}

/**
*
* @constructor
Expand All @@ -349,7 +393,7 @@ function GPS() {
}

this['events'] = {};
this['state'] = { 'errors': 0, 'processed': 0 };
this['state'] = { 'errors': 0, 'processed': 0, 'txtBuffer': {}};
}

GPS.prototype['events'] = null;
Expand Down Expand Up @@ -448,6 +492,136 @@ GPS['mod'] = {
'system': gsa.length > 19 ? parseSystemId(parseNumber(gsa[18])) : 'unknown'
};
},
// Text Transmission
// according to https://www.plaisance-pratique.com/IMG/pdf/NMEA0183-2.pdf
// Section 6.3 - Site 69
'TXT': function(str, txt,gps) {

if (txt.length !== 6) {
throw new Error("Invalid TXT length: " + str)
}

/*
1 2 3 4 5
| | | | |
$--TXT,xx,xx,xx,c--c*hh<CR><LF>
1) Total number of sentences, 01 to 99
2) Sentence number, 01 to 99
3) Text identifier, 01 to 99
4) Text message (with ^ escapes, see below)
5) Checksum
eg1. $--TXT,01,01,02,GPS;GLO;GAL;BDS*77
eg2. $--TXT,01,01,02,SBAS;IMES;QZSS*49
*/

if (txt[1].length !== 2) {
throw new Error("Invalid TXT Number of sequences Length: " + txt[1])
}

const sequenceLength = parseInt(txt[1], 10)

if (txt[2].length !== 2) {
throw new Error("Invalid TXT Sentence number Length: " + txt[2])
}

const sentenceNumber = parseInt(txt[2], 10)

if (txt[3].length !== 2) {
throw new Error("Invalid TXT Text identifier Length: " + txt[3])
}

//this is used to identify the multiple sentence messages, it doesn't mean anything, when there is only one sentence
const textIdentifier = `identifier_${parseInt(txt[3], 10)}`

if (txt[4].length > 61) {
throw new Error("Invalid TXT Message Length: " + txt[4])
}

const message = escapeString(txt[4])

if (message === "") {
throw new Error("Invalid empty TXT message: " + message)
}

// this tries to parse a sentence that is more than one message, it doesn't assume, that all sentences arrive in order, but it has a timeout for receiving all!
if (sequenceLength != 1) {
if(gps === undefined){
throw new Error(`Can't parse multi sequence with the static function, it can't store partial messages!`)
}

if (gps["state"]["txtBuffer"][textIdentifier] === undefined) {
// the map is necessary, otherwise the values in there refer all to the same value, and if you change one, you change all
gps["state"]["txtBuffer"][textIdentifier] = new Array(
sequenceLength + 1
).map((_, i) => {
if (i === sequenceLength) {
const SECONDS = 20
// the timeout ID is stored in that array at the last position, it gets cancelled, when all sentences arrived, otherwise it fires and sets an error!
return setTimeout(
(_identifier, _SECONDS,_gps) => {
const errorMessage = `The multi sentence messsage with the identifier ${_identifier} timed out while waiting fro all pieces of the sentence for ${_SECONDS} seconds`
_gps["state"]["errors"]++

_gps["emit"]("data", null)
console.error(errorMessage)
},
SECONDS * 1000,
textIdentifier,
SECONDS,
gps
) // 20 seconds is the arbitrary timeout
}
return ""
})
}

gps["state"]["txtBuffer"][textIdentifier][sentenceNumber - 1] = message;

const receivedMessages = gps["state"]["txtBuffer"][textIdentifier].reduce(
(acc, elem, i) => {
if (i === sequenceLength) {
return acc
}
return acc + (elem === "" ? 0 : 1)
},
0
)

if (receivedMessages === sequenceLength) {
const rawMessages = gps["state"]["txtBuffer"][textIdentifier].filter(
(_, i) => i !== sequenceLength
)

const timerID = gps["state"]["txtBuffer"][textIdentifier][sequenceLength]
clearTimeout(timerID)

delete gps["state"]["txtBuffer"][textIdentifier]

return {
message: rawMessages.join(""),
completed: true,
rawMessages: rawMessages,
sentenceAmount: sequenceLength,
}
} else {
return {
message: null,
completed: false,
rawMessages: [],
sentenceAmount: sequenceLength,
}
}
}

return {
message: message,
completed: true,
rawMessages: [message],
sentenceAmount: sequenceLength,
}
},
// Recommended Minimum data for gps
'RMC': function (str, rmc) {

Expand Down Expand Up @@ -782,7 +956,7 @@ GPS['mod'] = {
}
};

GPS['Parse'] = function (line) {
GPS['Parse'] = function (line, gps) {

if (typeof line !== 'string')
return false;
Expand All @@ -805,7 +979,7 @@ GPS['Parse'] = function (line) {

if (GPS['mod'][nmea[0]] !== undefined) {
// set raw data here as well?
var data = this['mod'][nmea[0]](line, nmea);
var data = this['mod'][nmea[0]](line, nmea, gps);
data['raw'] = line;
data['valid'] = isValid(line, nmea[nmea.length - 1]);
data['type'] = nmea[0];
Expand Down Expand Up @@ -883,7 +1057,7 @@ GPS['TotalDistance'] = function (path) {

GPS.prototype['update'] = function (line) {

var parsed = GPS['Parse'](line);
var parsed = GPS['Parse'](line, this);

this['state']['processed']++;

Expand Down
65 changes: 65 additions & 0 deletions tests/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,71 @@ const tests = {
"signalId": null,
"type": "GSV",
"valid": true
},
'$GNTXT,01,01,02,PF=3FF*4B':{
"completed": true,
"message": "PF=3FF",
"raw": "$GNTXT,01,01,02,PF=3FF*4B",
"rawMessages": [
"PF=3FF",
],
"sentenceAmount": 1,
"type": "TXT",
"valid": true
},
'$GNTXT,01,01,02,ANTSTATUS=OK*25':{
"completed": true,
"message": "ANTSTATUS=OK",
"raw": "$GNTXT,01,01,02,ANTSTATUS=OK*25",
"rawMessages": [
"ANTSTATUS=OK",
],
"sentenceAmount": 1,
"type": "TXT",
"valid": true
},
'$GNTXT,01,01,02,LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD*2F':{
"completed": true,
"message": "LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD",
"raw": "$GNTXT,01,01,02,LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD*2F",
"rawMessages": [
"LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD",
],
"sentenceAmount": 1,
"type": "TXT",
"valid": true
},
'$GNTXT,01,01,02,some escape chars: ^21*2F':{
"completed": true,
"message": "some escape chars: !",
"raw": "$GNTXT,01,01,02,some escape chars: ^21*2F",
"rawMessages": [
"some escape chars: !",
],
"sentenceAmount": 1,
"type": "TXT",
"valid": false
},
'$GNTXT,02,01,02,a multipart message^2C this is part 1^0D^0A*34':{
"completed": false,
"message": null,
"raw": "$GNTXT,02,01,02,a multipart message^2C this is part 1^0D^0A*34",
"rawMessages": [],
"sentenceAmount": 2,
"type": "TXT",
"valid": true
},
'$GNTXT,02,02,02,a multipart message^2C this is part 2^0D^0A*34':{
"completed": true,
"message": "a multipart message, this is part 1\r\na multipart message, this is part 2\r\n",
"raw": "$GNTXT,02,02,02,a multipart message^2C this is part 2^0D^0A*34",
"rawMessages": [
"a multipart message, this is part 1\r\n",
"a multipart message, this is part 2\r\n",
],
"sentenceAmount": 2,
"type": "TXT",
"valid": true
}
};
var collect = {};
Expand Down

0 comments on commit 668b1cc

Please sign in to comment.