@@ -338,6 +338,50 @@ function parseDist(num, unit) {
338
338
throw new Error ( 'Unknown unit: ' + unit ) ;
339
339
}
340
340
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 - z A - Z 0 - 9 ] { 2 } ) / g,
379
+ ( fullMatch , escapeSequence ) => {
380
+ return String . fromCharCode ( parseInt ( escapeSequence , 16 ) )
381
+ }
382
+ )
383
+ }
384
+
341
385
/**
342
386
*
343
387
* @constructor
@@ -349,7 +393,7 @@ function GPS() {
349
393
}
350
394
351
395
this [ 'events' ] = { } ;
352
- this [ 'state' ] = { 'errors' : 0 , 'processed' : 0 } ;
396
+ this [ 'state' ] = { 'errors' : 0 , 'processed' : 0 , 'txtBuffer' : { } } ;
353
397
}
354
398
355
399
GPS . prototype [ 'events' ] = null ;
@@ -448,6 +492,136 @@ GPS['mod'] = {
448
492
'system' : gsa . length > 19 ? parseSystemId ( parseNumber ( gsa [ 18 ] ) ) : 'unknown'
449
493
} ;
450
494
} ,
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
+ } ,
451
625
// Recommended Minimum data for gps
452
626
'RMC' : function ( str , rmc ) {
453
627
@@ -782,7 +956,7 @@ GPS['mod'] = {
782
956
}
783
957
} ;
784
958
785
- GPS [ 'Parse' ] = function ( line ) {
959
+ GPS [ 'Parse' ] = function ( line , gps ) {
786
960
787
961
if ( typeof line !== 'string' )
788
962
return false ;
@@ -805,7 +979,7 @@ GPS['Parse'] = function (line) {
805
979
806
980
if ( GPS [ 'mod' ] [ nmea [ 0 ] ] !== undefined ) {
807
981
// 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 ) ;
809
983
data [ 'raw' ] = line ;
810
984
data [ 'valid' ] = isValid ( line , nmea [ nmea . length - 1 ] ) ;
811
985
data [ 'type' ] = nmea [ 0 ] ;
@@ -883,7 +1057,7 @@ GPS['TotalDistance'] = function (path) {
883
1057
884
1058
GPS . prototype [ 'update' ] = function ( line ) {
885
1059
886
- var parsed = GPS [ 'Parse' ] ( line ) ;
1060
+ var parsed = GPS [ 'Parse' ] ( line , this ) ;
887
1061
888
1062
this [ 'state' ] [ 'processed' ] ++ ;
889
1063
0 commit comments