UNPKG

node-gps

Version:
295 lines (268 loc) 7.93 kB
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;