node-gps
Version:
A GPS NMEA parser library
295 lines (268 loc) • 7.93 kB
JavaScript
const util = require('util');
const EventEmitter = require('events');
/**
* [GPS description]
*
* @wiki https://en.wikipedia.org/wiki/NMEA_0183
* @wiki https://en.wikipedia.org/wiki/NMEA_2000
*/
function GPS(){
};
util.inherits(GPS, EventEmitter);
GPS.CR = '\r'; // 0x0d;
GPS.LF = '\n'; // 0x0a;
GPS.$ = '$'; // 0x24;
GPS.SEPARATOR = ','; // 0x2c;
GPS.CHECKSUM = 0x2a; // *
GPS.TAG = '\\';
GPS.parse = function(line){
// Messages have a maximum length of 82 characters,
// including the $ or ! starting character and the ending <LF>
if(line.length > 82) throw new Error();
if(!~[ GPS.$, '!' ].indexOf(line[0])) throw new Error();
// if(line.slice(-1) !== GPS.LF) throw new Error();
// All data fields that follow are comma-delimited.
const nmea = line.split(GPS.SEPARATOR);
var last = nmea.pop();
// The first character that immediately follows the last data field character is an asterisk,
// but it is only included if a checksum is supplied.
last = last.split('*');
nmea.push(last[0]);
nmea.push(last[1]);
// The start character for each message can be either a
// $ (For conventional field delimited messages) or
// ! (for messages that have special encapsulation in them)
// The next five characters identify the talker (two characters)
// and the type of message (three characters).
const type = nmea[0][0];
const talker = nmea[0].substr(1, 2);
const messageType = nmea[0].substr(3);
const checksum = nmea[nmea.length - 1];
switch(type){
case GPS.$:
if(messageType in GPS){
GPS.checksum(line, checksum);
return GPS[messageType](nmea, line);
}else{
throw new Error(`Unknow message type "${messageType}"`);
}
break;
case '!':
break;
default:
throw new Error(`Unknow sentence start delimiter "${type}"`);
break;
}
};
GPS.parseTime = function(time, date) {
if (time === '') return null;
var ret = new Date;
if (date) {
var year = date.slice(4);
var month = date.slice(2, 4) - 1;
var day = date.slice(0, 2);
if (year.length === 4) {
ret.setUTCFullYear(year, month, day);
} else {
// If we need to parse older GPRMC data, we should hack something like
// year < 73 ? 2000+year : 1900+year
ret.setUTCFullYear('20' + year, month, day);
}
}
ret.setUTCHours(time.slice(0, 2));
ret.setUTCMinutes(time.slice(2, 4));
ret.setUTCSeconds(time.slice(4, 6));
// Extract the milliseconds, since they can be not present, be 3 decimal place, or 2 decimal places, or other?
var msStr = time.slice(7);
var msExp = msStr.length;
var ms = 0;
if (msExp !== 0) ms = parseFloat(msStr) * Math.pow(10, 3 - msExp);
ret.setUTCMilliseconds(ms);
return ret;
}
GPS.parseCoord = function (coord, dir) {
// Latitude can go from 0 to 90; longitude can go from 0 to 180.
if (coord === '') return null;
var n, sgn = 1;
switch (dir) {
case 'S':
sgn = -1;
case 'N':
n = 2;
break;
case 'W':
sgn = -1;
case 'E':
n = 3;
break;
}
/*
* Mathematically, but more expensive and not numerical stable:
*
* raw = 4807.038
* deg = Math.floor(raw / 100)
*
* dec = (raw - (100 * deg)) / 60
* res = deg + dec // 48.1173
*/
return sgn * (parseFloat(coord.slice(0, n)) + parseFloat(coord.slice(n)) / 60);
}
GPS.parseNumber = function (num) {
if (num === '') {
return null;
}
return parseFloat(num);
}
GPS.parseDist = function (num, unit) {
if (unit === 'M' || unit === '') {
return parseNumber(num);
}
throw 'Unknown unit: ' + unit;
}
GPS.parseKnots = function(knots) {
if (knots === '') return null;
return parseFloat(knots) * 1.852;
}
GPS.parseGSAMode = function(mode) {
switch (mode) {
case 'M':
return 'manual';
case 'A':
return 'automatic';
case '':
return null;
}
throw 'INVALID GSA MODE: ' + mode;
}
GPS.parseGGAFix = function (fix) {
switch (fix) {
case '':
case '0':
return null;
case '1':
return 'fix'; // valid SPS fix
case '2':
return 'dgps-fix'; // valid DGPS fix
case '3':
return 'pps-fix'; // valid PPS fix
case '4':
return 'rtk'; // valid RTK fix
case '5':
return 'rtk-float'; // valid RTK float
case '6':
return 'estimated';
case '7':
return 'manual';
case '8':
return 'simulated';
}
throw 'INVALID GGA FIX: ' + fix;
}
GPS.parseGSAFix = function (fix) {
switch (fix) {
case '1':
case '':
return null;
case '2':
return '2D';
case '3':
return '3D';
}
throw 'INVALID GSA FIX: ' + fix;
}
GPS.parseRMC_GLLStatus = function (status) {
switch (status) {
case 'A':
return 'active';
case 'V':
return 'void';
case '':
return null;
}
throw 'INVALID RMC/GLL STATUS: ' + status;
}
GPS.parseFAA = function (faa) {
// Only A and D will correspond to an Active and reliable Sentence
switch (faa) {
case 'A':
return 'autonomous';
case 'D':
return 'differential';
case 'E':
return 'estimated';
case 'M':
return 'manual input';
case 'S':
return 'simulated';
case 'N':
return 'not valid';
case 'P':
return 'precise';
}
throw 'INVALID FAA MODE: ' + faa;
}
GPS.parseRMCVariation = function (vari, dir) {
if (vari === '' || dir === '')
return null;
var q = (dir === 'W') ? -1.0 : 1.0;
return parseFloat(vari) * q;
}
/**
* checksum
* The asterisk is immediately followed by a checksum represented as a two-digit hexadecimal number.
* The checksum is the bitwise exclusive OR of ASCII codes of all characters between the $ and *.
* According to the official specification, the checksum is optional for most data sentences,
* but is compulsory for RMA, RMB, and RMC (among others).
* @ref https://en.wikipedia.org/wiki/NMEA_0183#C_implementation_of_checksum_generation
*/
GPS.checksum = function(str, crc){
var checksum = 0;
for(var i = 1; i < str.length; i++){
var c = str.charCodeAt(i);
if(c === GPS.CHECKSUM) break; // *
checksum ^= c;
}
return crc ? parseInt(crc, 16) === checksum : checksum;
};
const { parseTime, parseCoord, parseDist, parseNumber, parseGGAFix } = GPS;
GPS.GGA = function(gga, raw){
if (gga.length !== 16) {
throw 'Invalid GGA length: ' + str;
}
/*
1 2 3 4 5 6 7 8 9 10 | 12 13 14 15
| | | | | | | | | | | | | | |
$--GGA,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh
1) Time (UTC)
2) Latitude
3) N or S (North or South)
4) Longitude
5) E or W (East or West)
6) GPS Quality Indicator,
0 = Invalid, 1 = Valid SPS, 2 = Valid DGPS, 3 = Valid PPS
7) Number of satellites in view, 00 - 12
8) Horizontal Dilution of precision, lower is better
9) Antenna Altitude above/below mean-sea-level (geoid)
10) Units of antenna altitude, meters
11) Geoidal separation, the difference between the WGS-84 earth
ellipsoid and mean-sea-level (geoid), '-' means mean-sea-level below ellipsoid
12) Units of geoidal separation, meters
13) Age of differential GPS data, time in seconds since last SC104
type 1 or 9 update, null field when DGPS is not used
14) Differential reference station ID, 0000-1023
15) Checksum
*/
return {
time: parseTime(gga[1]),
lat: parseCoord(gga[2], gga[3]),
lng: parseCoord(gga[4], gga[5]),
alt: parseDist(gga[9], gga[10]),
quality: parseGGAFix(gga[6]),
satelites: parseNumber(gga[7]),
hdop: parseNumber(gga[8]), // dilution
geoidal: parseDist(gga[11], gga[12]), // aboveGeoid
age: parseNumber(gga[13]), // dgpsUpdate???
stationID: parseNumber(gga[14]) // dgpsReference??
};
};
module.exports = GPS;