Skip to content
Closed
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
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