Skip to content

Commit 668b1cc

Browse files
committed
feat: added TXT parser
1 parent deb0f32 commit 668b1cc

File tree

3 files changed

+252
-5
lines changed

3 files changed

+252
-5
lines changed

gps.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ declare class GPS {
3939
* @param line NMEA string
4040
* @returns NMEA object or False
4141
*/
42-
static Parse<T = any>(line: string): false | T;
42+
static Parse<T = any>(line: string, GPSObject?: GPS | undefined): false | T;
4343

4444
/**
4545
* Calculates the distance between two geo-coordinates using Haversine formula
@@ -90,6 +90,7 @@ declare namespace GPS {
9090
[key: string]: any;
9191
processed: number;
9292
errors: number;
93+
txtBuffer: Record<string, string[]>
9394

9495
time?: Date;
9596
lat?: number;
@@ -229,4 +230,11 @@ declare namespace GPS {
229230
valid: boolean;
230231
type: 'HDT';
231232
}
233+
234+
export interface TXT {
235+
message: string | null
236+
completed: boolean,
237+
rawMessages: string[],
238+
sentenceAmount: number,
239+
}
232240
}

src/gps.js

Lines changed: 178 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,50 @@ function parseDist(num, unit) {
338338
throw new Error('Unknown unit: ' + unit);
339339
}
340340

341+
342+
/**
343+
* @description Escapes a string, according to spec
344+
* @see https://www.plaisance-pratique.com/IMG/pdf/NMEA0183-2.pdf Section 5.1.3
345+
*
346+
* @param {string} str string to escape
347+
* @returns {string}
348+
*/
349+
function escapeString(str){
350+
// invalid characters according to:
351+
// https://www.plaisance-pratique.com/IMG/pdf/NMEA0183-2.pdf
352+
// Section 6.1 - Table 1
353+
const invalidCharacters = [
354+
"\r",
355+
"\n",
356+
"$",
357+
"*",
358+
",",
359+
"!",
360+
"\\",
361+
// this is excluded, as it is removed later, since identifies escape sequences
362+
//"^"
363+
"~",
364+
"\u007F", // <DEL>
365+
]
366+
367+
for (const invalidCharacter of invalidCharacters) {
368+
if (str.includes(invalidCharacter)) {
369+
throw new Error(
370+
`Message may not contain invalid Character '${invalidCharacter}'`
371+
)
372+
}
373+
}
374+
375+
// escaping according to https://www.plaisance-pratique.com/IMG/pdf/NMEA0183-2.pdf
376+
// Section 5.1.3
377+
return str.replaceAll(
378+
/\^([a-zA-Z0-9]{2})/g,
379+
(fullMatch, escapeSequence) => {
380+
return String.fromCharCode(parseInt(escapeSequence, 16))
381+
}
382+
)
383+
}
384+
341385
/**
342386
*
343387
* @constructor
@@ -349,7 +393,7 @@ function GPS() {
349393
}
350394

351395
this['events'] = {};
352-
this['state'] = { 'errors': 0, 'processed': 0 };
396+
this['state'] = { 'errors': 0, 'processed': 0, 'txtBuffer': {}};
353397
}
354398

355399
GPS.prototype['events'] = null;
@@ -448,6 +492,136 @@ GPS['mod'] = {
448492
'system': gsa.length > 19 ? parseSystemId(parseNumber(gsa[18])) : 'unknown'
449493
};
450494
},
495+
// Text Transmission
496+
// according to https://www.plaisance-pratique.com/IMG/pdf/NMEA0183-2.pdf
497+
// Section 6.3 - Site 69
498+
'TXT': function(str, txt,gps) {
499+
500+
if (txt.length !== 6) {
501+
throw new Error("Invalid TXT length: " + str)
502+
}
503+
504+
/*
505+
1 2 3 4 5
506+
| | | | |
507+
$--TXT,xx,xx,xx,c--c*hh<CR><LF>
508+
509+
1) Total number of sentences, 01 to 99
510+
2) Sentence number, 01 to 99
511+
3) Text identifier, 01 to 99
512+
4) Text message (with ^ escapes, see below)
513+
5) Checksum
514+
515+
eg1. $--TXT,01,01,02,GPS;GLO;GAL;BDS*77
516+
eg2. $--TXT,01,01,02,SBAS;IMES;QZSS*49
517+
*/
518+
519+
if (txt[1].length !== 2) {
520+
throw new Error("Invalid TXT Number of sequences Length: " + txt[1])
521+
}
522+
523+
const sequenceLength = parseInt(txt[1], 10)
524+
525+
if (txt[2].length !== 2) {
526+
throw new Error("Invalid TXT Sentence number Length: " + txt[2])
527+
}
528+
529+
const sentenceNumber = parseInt(txt[2], 10)
530+
531+
if (txt[3].length !== 2) {
532+
throw new Error("Invalid TXT Text identifier Length: " + txt[3])
533+
}
534+
535+
//this is used to identify the multiple sentence messages, it doesn't mean anything, when there is only one sentence
536+
const textIdentifier = `identifier_${parseInt(txt[3], 10)}`
537+
538+
if (txt[4].length > 61) {
539+
throw new Error("Invalid TXT Message Length: " + txt[4])
540+
}
541+
542+
const message = escapeString(txt[4])
543+
544+
if (message === "") {
545+
throw new Error("Invalid empty TXT message: " + message)
546+
}
547+
548+
// 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!
549+
if (sequenceLength != 1) {
550+
if(gps === undefined){
551+
throw new Error(`Can't parse multi sequence with the static function, it can't store partial messages!`)
552+
}
553+
554+
if (gps["state"]["txtBuffer"][textIdentifier] === undefined) {
555+
// the map is necessary, otherwise the values in there refer all to the same value, and if you change one, you change all
556+
gps["state"]["txtBuffer"][textIdentifier] = new Array(
557+
sequenceLength + 1
558+
).map((_, i) => {
559+
if (i === sequenceLength) {
560+
const SECONDS = 20
561+
// 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!
562+
return setTimeout(
563+
(_identifier, _SECONDS,_gps) => {
564+
const errorMessage = `The multi sentence messsage with the identifier ${_identifier} timed out while waiting fro all pieces of the sentence for ${_SECONDS} seconds`
565+
_gps["state"]["errors"]++
566+
567+
_gps["emit"]("data", null)
568+
console.error(errorMessage)
569+
},
570+
SECONDS * 1000,
571+
textIdentifier,
572+
SECONDS,
573+
gps
574+
) // 20 seconds is the arbitrary timeout
575+
}
576+
return ""
577+
})
578+
}
579+
580+
gps["state"]["txtBuffer"][textIdentifier][sentenceNumber - 1] = message;
581+
582+
const receivedMessages = gps["state"]["txtBuffer"][textIdentifier].reduce(
583+
(acc, elem, i) => {
584+
if (i === sequenceLength) {
585+
return acc
586+
}
587+
return acc + (elem === "" ? 0 : 1)
588+
},
589+
0
590+
)
591+
592+
if (receivedMessages === sequenceLength) {
593+
const rawMessages = gps["state"]["txtBuffer"][textIdentifier].filter(
594+
(_, i) => i !== sequenceLength
595+
)
596+
597+
const timerID = gps["state"]["txtBuffer"][textIdentifier][sequenceLength]
598+
clearTimeout(timerID)
599+
600+
delete gps["state"]["txtBuffer"][textIdentifier]
601+
602+
return {
603+
message: rawMessages.join(""),
604+
completed: true,
605+
rawMessages: rawMessages,
606+
sentenceAmount: sequenceLength,
607+
}
608+
} else {
609+
return {
610+
message: null,
611+
completed: false,
612+
rawMessages: [],
613+
sentenceAmount: sequenceLength,
614+
}
615+
}
616+
}
617+
618+
return {
619+
message: message,
620+
completed: true,
621+
rawMessages: [message],
622+
sentenceAmount: sequenceLength,
623+
}
624+
},
451625
// Recommended Minimum data for gps
452626
'RMC': function (str, rmc) {
453627

@@ -782,7 +956,7 @@ GPS['mod'] = {
782956
}
783957
};
784958

785-
GPS['Parse'] = function (line) {
959+
GPS['Parse'] = function (line, gps) {
786960

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

806980
if (GPS['mod'][nmea[0]] !== undefined) {
807981
// set raw data here as well?
808-
var data = this['mod'][nmea[0]](line, nmea);
982+
var data = this['mod'][nmea[0]](line, nmea, gps);
809983
data['raw'] = line;
810984
data['valid'] = isValid(line, nmea[nmea.length - 1]);
811985
data['type'] = nmea[0];
@@ -883,7 +1057,7 @@ GPS['TotalDistance'] = function (path) {
8831057

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

886-
var parsed = GPS['Parse'](line);
1060+
var parsed = GPS['Parse'](line, this);
8871061

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

tests/parser.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,71 @@ const tests = {
11521152
"signalId": null,
11531153
"type": "GSV",
11541154
"valid": true
1155+
},
1156+
'$GNTXT,01,01,02,PF=3FF*4B':{
1157+
"completed": true,
1158+
"message": "PF=3FF",
1159+
"raw": "$GNTXT,01,01,02,PF=3FF*4B",
1160+
"rawMessages": [
1161+
"PF=3FF",
1162+
],
1163+
"sentenceAmount": 1,
1164+
"type": "TXT",
1165+
"valid": true
1166+
},
1167+
'$GNTXT,01,01,02,ANTSTATUS=OK*25':{
1168+
"completed": true,
1169+
"message": "ANTSTATUS=OK",
1170+
"raw": "$GNTXT,01,01,02,ANTSTATUS=OK*25",
1171+
"rawMessages": [
1172+
"ANTSTATUS=OK",
1173+
],
1174+
"sentenceAmount": 1,
1175+
"type": "TXT",
1176+
"valid": true
1177+
},
1178+
'$GNTXT,01,01,02,LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD*2F':{
1179+
"completed": true,
1180+
"message": "LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD",
1181+
"raw": "$GNTXT,01,01,02,LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD*2F",
1182+
"rawMessages": [
1183+
"LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD",
1184+
],
1185+
"sentenceAmount": 1,
1186+
"type": "TXT",
1187+
"valid": true
1188+
},
1189+
'$GNTXT,01,01,02,some escape chars: ^21*2F':{
1190+
"completed": true,
1191+
"message": "some escape chars: !",
1192+
"raw": "$GNTXT,01,01,02,some escape chars: ^21*2F",
1193+
"rawMessages": [
1194+
"some escape chars: !",
1195+
],
1196+
"sentenceAmount": 1,
1197+
"type": "TXT",
1198+
"valid": false
1199+
},
1200+
'$GNTXT,02,01,02,a multipart message^2C this is part 1^0D^0A*34':{
1201+
"completed": false,
1202+
"message": null,
1203+
"raw": "$GNTXT,02,01,02,a multipart message^2C this is part 1^0D^0A*34",
1204+
"rawMessages": [],
1205+
"sentenceAmount": 2,
1206+
"type": "TXT",
1207+
"valid": true
1208+
},
1209+
'$GNTXT,02,02,02,a multipart message^2C this is part 2^0D^0A*34':{
1210+
"completed": true,
1211+
"message": "a multipart message, this is part 1\r\na multipart message, this is part 2\r\n",
1212+
"raw": "$GNTXT,02,02,02,a multipart message^2C this is part 2^0D^0A*34",
1213+
"rawMessages": [
1214+
"a multipart message, this is part 1\r\n",
1215+
"a multipart message, this is part 2\r\n",
1216+
],
1217+
"sentenceAmount": 2,
1218+
"type": "TXT",
1219+
"valid": true
11551220
}
11561221
};
11571222
var collect = {};

0 commit comments

Comments
 (0)