UNPKG

node-gps

Version:
704 lines (559 loc) 17.9 kB
/** * @license GPS.js v0.2.0 26/01/2016 * * Copyright (c) 2016, Robert Eisele (robert@xarg.org) * Dual licensed under the MIT or GPL Version 2 licenses. **/ (function(root) { var D2R = Math.PI / 180; var collectSats = []; function updateState(state, data) { if (data['type'] === 'RMC' || data['type'] === 'GGA' || data['type'] === 'GLL') { state['time'] = data['time']; state['lat'] = data['lat']; state['lon'] = data['lon']; } if (data['type'] === 'ZDA') { state['time'] = data['time']; } if (data['type'] === 'GGA') { state['alt'] = data['alt']; } if (data['type'] === 'RMC'/* || data['type'] === 'VTG'*/) { // TODO: is rmc speed/track really interchangeable with vtg speed/track? state['speed'] = data['speed']; state['track'] = data['track']; } if (data['type'] === 'GSA') { state['satsActive'] = data['satellites']; state['fix'] = data['fix']; state['hdop'] = data['hdop']; state['pdop'] = data['pdop']; state['vdop'] = data['vdop']; } // TODO: better merge algorithm if (data['type'] === 'GSV') { var sats = data['satellites']; for (var i = 0; i < sats.length; i++) { collectSats.push(sats[i]); } // Reset stats if (data['msgNumber'] === data['msgsTotal']) { state['satsVisible'] = collectSats; collectSats = []; } } } function parseTime(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; } function parseCoord(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); } function parseNumber(num) { if (num === '') { return null; } return parseFloat(num); } function parseKnots(knots) { if (knots === '') return null; return parseFloat(knots) * 1.852; } function parseGSAMode(mode) { switch (mode) { case 'M': return 'manual'; case 'A': return 'automatic'; case '': return null; } throw 'INVALID GSA MODE: ' + mode; } function parseGGAFix(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; } function parseGSAFix(fix) { switch (fix) { case '1': case '': return null; case '2': return '2D'; case '3': return '3D'; } throw 'INVALID GSA FIX: ' + fix; } function parseRMC_GLLStatus(status) { switch (status) { case 'A': return 'active'; case 'V': return 'void'; case '': return null; } throw 'INVALID RMC/GLL STATUS: ' + status; } function parseFAA(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; } function parseRMCVariation(vari, dir) { if (vari === '' || dir === '') return null; var q = (dir === 'W') ? -1.0 : 1.0; return parseFloat(vari) * q; } function isValid(str, crc) { var checksum = 0; for (var i = 1; i < str.length; i++) { var c = str.charCodeAt(i); if (c === 42) // Asterisk: * break; checksum ^= c; } return checksum === parseInt(crc, 16); } function parseDist(num, unit) { if (unit === 'M' || unit === '') { return parseNumber(num); } throw 'Unknown unit: ' + unit; } function GPS() { this['events'] = {}; this['state'] = {}; } GPS.prototype['events'] = null; GPS.prototype['state'] = null; GPS['mod'] = { // Global Positioning System Fix Data 'GGA': function(str, gga) { 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]), 'lon': 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?? }; }, // GPS DOP and active satellites 'GSA': function(str, gsa) { if (gsa.length !== 19) { throw 'Invalid GSA length: ' + str; } /* eg1. $GPGSA,A,3,,,,,,16,18,,22,24,,,3.6,2.1,2.2*3C eg2. $GPGSA,A,3,19,28,14,18,27,22,31,39,,,,,1.7,1.0,1.3*35 1 = Mode: M=Manual, forced to operate in 2D or 3D A=Automatic, 3D/2D 2 = Mode: 1=Fix not available 2=2D 3=3D 3-14 = PRNs of Satellite Vehicles (SVs) used in position fix (null for unused fields) 15 = PDOP 16 = HDOP 17 = VDOP 18 = Checksum */ var sats = []; for (var i = 3; i < 12 + 3; i++) { if (gsa[i] !== '') { sats.push(parseInt(gsa[i], 10)); } } return { 'mode': parseGSAMode(gsa[1]), 'fix': parseGSAFix(gsa[2]), 'satellites': sats, 'pdop': parseNumber(gsa[15]), 'hdop': parseNumber(gsa[16]), 'vdop': parseNumber(gsa[17]) }; }, // Recommended Minimum data for gps 'RMC': function(str, rmc) { if (rmc.length !== 13 && rmc.length !== 14) { throw 'Invalid RMC length: ' + str; } /* $GPRMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,ddmmyy,x.x,a*hh RMC = Recommended Minimum Specific GPS/TRANSIT Data 1 = UTC of position fix 2 = Data status (A-ok, V-invalid) 3 = Latitude of fix 4 = N or S 5 = Longitude of fix 6 = E or W 7 = Speed over ground in knots 8 = Track made good in degrees True 9 = UT date 10 = Magnetic variation degrees (Easterly var. subtracts from true course) 11 = E or W (12) = NMEA 2.3 introduced FAA mode indicator (A=Autonomous, D=Differential, E=Estimated, N=Data not valid) 12 = Checksum */ return { 'time': parseTime(rmc[1], rmc[9]), 'status': parseRMC_GLLStatus(rmc[2]), 'lat': parseCoord(rmc[3], rmc[4]), 'lon': parseCoord(rmc[5], rmc[6]), 'speed': parseKnots(rmc[7]), 'track': parseNumber(rmc[8]), 'variation': parseRMCVariation(rmc[10], rmc[11]), 'faa': rmc.length === 14 ? parseFAA(rmc[12]) : null }; }, // Track info 'VTG': function(str, vtg) { if (vtg.length !== 10 && vtg.length !== 11) { throw 'Invalid VTG length: ' + str; } /* ------------------------------------------------------------------------------ 1 2 3 4 5 6 7 8 9 10 | | | | | | | | | | $--VTG,x.x,T,x.x,M,x.x,N,x.x,K,m,*hh<CR><LF> ------------------------------------------------------------------------------ 1 = Track degrees 2 = Fixed text 'T' indicates that track made good is relative to true north 3 = not used 4 = not used 5 = Speed over ground in knots 6 = Fixed text 'N' indicates that speed over ground in in knots 7 = Speed over ground in kilometers/hour 8 = Fixed text 'K' indicates that speed over ground is in kilometers/hour (9) = FAA mode indicator (NMEA 2.3 and later) 9/10 = Checksum */ if (vtg[2] === '' && vtg[8] === '' && vtg[6] === '') { return { 'track': null, 'speed': null, 'faa': null }; } if (vtg[2] !== 'T') { throw 'Invalid VTG track mode: ' + str; } if (vtg[8] !== 'K' || vtg[6] !== 'N') { throw 'Invalid VTG speed tag: ' + str; } return { 'track': parseNumber(vtg[1]), 'speed': parseKnots(vtg[5]), 'faa': vtg.length === 11 ? parseFAA(vtg[9]) : null }; }, // satelites in view 'GSV': function(str, gsv) { if (gsv.length < 9 || gsv.length % 4 !== 1) { throw 'Invalid GSV length: ' + str; } /* $GPGSV,1,1,13,02,02,213,,03,-3,000,,11,00,121,,14,13,172,05*67 1 = Total number of messages of this type in this cycle 2 = Message number 3 = Total number of SVs in view 4 = SV PRN number 5 = Elevation in degrees, 90 maximum 6 = Azimuth, degrees from true north, 000 to 359 7 = SNR (signal to noise ratio), 00-99 dB (null when not tracking, higher is better) 8-11 = Information about second SV, same as field 4-7 12-15= Information about third SV, same as field 4-7 16-19= Information about fourth SV, same as field 4-7 8/12/16/20 = Checksum */ var sats = []; for (var i = 4; i < gsv.length - 1; i += 4) { var prn = parseNumber(gsv[i]); var snr = parseNumber(gsv[i + 3]); sats.push({ 'prn': prn, 'elevation': parseNumber(gsv[i + 1]), 'azimuth': parseNumber(gsv[i + 2]), 'snr': snr, 'status': prn !== null ? (snr !== null ? 'tracking' : 'in view') : null }); } return { 'msgNumber': parseNumber(gsv[2]), 'msgsTotal': parseNumber(gsv[1]), //'satsInView' : parseNumber(gsv[3]), // Can be obtained by satellites.length 'satellites': sats }; }, // Geographic Position - Latitude/Longitude 'GLL': function(str, gll) { if (gll.length !== 9) { throw 'Invalid GLL length: ' + str; } /* ------------------------------------------------------------------------------ 1 2 3 4 5 6 7 8 | | | | | | | | $--GLL,llll.ll,a,yyyyy.yy,a,hhmmss.ss,a,m,*hh<CR><LF> ------------------------------------------------------------------------------ 1. Latitude 2. N or S (North or South) 3. Longitude 4. E or W (East or West) 5. Universal Time Coordinated (UTC) 6. Status A - Data Valid, V - Data Invalid 7. FAA mode indicator (NMEA 2.3 and later) 8. Checksum */ return { 'time': parseTime(gll[5]), 'status': parseRMC_GLLStatus(gll[6]), 'lat': parseCoord(gll[1], gll[2]), 'lon': parseCoord(gll[3], gll[4]) }; }, // UTC Date / Time and Local Time Zone Offset 'ZDA': function(str, zda) { /* 1 = hhmmss.ss = UTC 2 = xx = Day, 01 to 31 3 = xx = Month, 01 to 12 4 = xxxx = Year 5 = xx = Local zone description, 00 to +/- 13 hours 6 = xx = Local zone minutes description (same sign as hours) */ // TODO: incorporate local zone information return { 'time': parseTime(zda[1], zda[2] + zda[3] + zda[4]) //'delta': time === null ? null : (Date.now() - time) / 1000 }; } }; GPS['Parse'] = function(line) { if (typeof line !== 'string') return false; var nmea = line.split(','); var last = nmea.pop(); if (nmea.length < 4 || line.charAt(0) !== '$' || last.indexOf('*') === -1) { return false; } last = last.split('*'); nmea.push(last[0]); nmea.push(last[1]); // Remove $ character and first two chars from the beginning nmea[0] = nmea[0].slice(3); if (GPS['mod'][nmea[0]] !== undefined) { // set raw data here as well? var data = this['mod'][nmea[0]](line, nmea); data['raw'] = line; data['valid'] = isValid(line, nmea[nmea.length - 1]); data['type'] = nmea[0]; return data; } return false; }; // Heading (N=0, E=90, S=189, W=270) from point 1 to point 2 GPS['Heading'] = function(lat1, lon1, lat2, lon2) { var dlon = (lon2 - lon1) * D2R; lat1 = lat1 * D2R; lat2 = lat2 * D2R; var sdlon = Math.sin(dlon); var cdlon = Math.cos(dlon); var slat1 = Math.sin(lat1); var clat1 = Math.cos(lat1); var slat2 = Math.sin(lat2); var clat2 = Math.cos(lat2); var n = sdlon * clat2; var d = clat1 * slat2 - slat1 * clat2 * cdlon; var head = Math.atan2(n, d) * 180 / Math.PI; return (head + 360) % 360; }; GPS['Distance'] = function(lat1, lon1, lat2, lon2) { // Haversine Formula // R.W. Sinnott, "Virtues of the Haversine", Sky and Telescope, vol. 68, no. 2, 1984, p. 159 // Because Earth is no exact sphere, rounding errors may be up to 0.5%. // var RADIUS = 6371; // Earth radius average // var RADIUS = 6378.137; // Earth radius at equator var RADIUS = 6372.8; // Earth radius in km var hLat = (lat2 - lat1) * D2R * 0.5; // Half of lat difference var hLon = (lon2 - lon1) * D2R * 0.5; // Half of lon difference lat1 = lat1 * D2R; lat2 = lat2 * D2R; var shLat = Math.sin(hLat); var shLon = Math.sin(hLon); var clat1 = Math.cos(lat1); var clat2 = Math.cos(lat2); var tmp = shLat * shLat + clat1 * clat2 * shLon * shLon; //return RADIUS * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1.0 - a)); return RADIUS * 2 * Math.asin(Math.sqrt(tmp)); }; GPS.prototype['update'] = function(line) { var parsed = GPS['Parse'](line); if (parsed === false) return false; updateState(this.state, parsed); if (this['events']['data'] !== undefined) { this['events']['data'].call(this, parsed); } if (this['events'][parsed.type] !== undefined) { this['events'][parsed.type].call(this, parsed); } return true; }; GPS.prototype['partial'] = ""; GPS.prototype['updatePartial'] = function(chunk) { this['partial'] += chunk; while (true) { var pos = this['partial'].indexOf("\r\n"); if (pos === -1) break; var line = this['partial'].slice(0, pos); if (line.charAt(0) === '$') { this['update'](line); } this['partial'] = this['partial'].slice(pos + 2); } }; GPS.prototype['on'] = function(ev, cb) { if (this['events'][ev] === undefined) { this['events'][ev] = cb; return this; } return null; }; GPS.prototype['off'] = function(ev) { if (this['events'][ev] !== undefined) { this['events'][ev] = undefined; } return this; }; if (typeof exports === 'object') { module.exports = GPS; } else { root['GPS'] = GPS; } })(this);