Comparing version 0.1.0 to 0.2.0
850
gps.js
@@ -1,202 +0,692 @@ | ||
var serialport = require("serialport"); | ||
var SerialPort = serialport.SerialPort; | ||
var events = require('events'); | ||
function GPS() { | ||
events.EventEmitter.call(this); | ||
this.reads = 0 | ||
this.collected = [] | ||
this.obj = {} | ||
var me = this; | ||
var serialPort = new SerialPort("/dev/cu.SLAB_USBtoUART", { | ||
baudrate: 4800, | ||
parser: serialport.parsers.readline("\n") | ||
}); | ||
serialPort.on("open", function () { | ||
serialPort.on('data', function(d){ | ||
me.parseGPSData(d) | ||
}); | ||
}); | ||
/** | ||
* @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. | ||
**/ | ||
this.latLngToDecimal = function(coord){ | ||
if (coord == undefined) return | ||
negative = (parseInt(coord) < 0) | ||
decimal = null | ||
if (match = coord.match(/^-?([0-9]*?)([0-9]{2,2}\.[0-9]*)$/)){ | ||
deg = parseInt(match[1]) | ||
min = parseFloat(match[2]) | ||
decimal = deg + (min / 60) | ||
if (negative){ | ||
decimal *= -1 | ||
(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 = []; | ||
} | ||
} | ||
return decimal | ||
} | ||
this.parseGPSData = function(data){ | ||
var that = this; | ||
line = data.split(',') | ||
if (line[0].slice(0,1) != "$") { | ||
return | ||
function parseTime(time, date) { | ||
if (time === '') { | ||
return null; | ||
} | ||
type = line[0].slice(3,6) | ||
var ret = new Date; | ||
if (type == null) { | ||
return | ||
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); | ||
} | ||
} | ||
line.shift() | ||
ret.setUTCHours(time.slice(0, 2)); | ||
ret.setUTCMinutes(time.slice(2, 4)); | ||
ret.setUTCSeconds(time.slice(4, 6)); | ||
ret.setUTCMilliseconds(parseFloat(time.slice(7)) || 0); | ||
switch (type) { | ||
case "GGA": | ||
this.obj.time = line.shift() | ||
this.obj.latitude = this.latLngToDecimal(line.shift()) | ||
this.obj.lat_ref = line.shift() | ||
this.obj.longitude = this.latLngToDecimal(line.shift()) | ||
this.obj.long_ref = line.shift() | ||
this.obj.quality = line.shift() | ||
this.obj.num_sat = parseInt(line.shift()) | ||
this.obj.hdop = line.shift() | ||
this.obj.altitude = line.shift() | ||
this.obj.alt_unit = line.shift() | ||
this.obj.height_geoid = line.shift() | ||
this.obj.height_geoid_unit = line.shift() | ||
this.obj.last_dgps = line.shift() | ||
this.obj.dgps = line.shift() | ||
break; | ||
case "RMC": | ||
this.obj.time = line.shift() | ||
this.obj.validity = line.shift() | ||
this.obj.latitude = this.latLngToDecimal(line.shift()) | ||
this.obj.lat_ref = line.shift() | ||
this.obj.longitude = this.latLngToDecimal(line.shift()) | ||
this.obj.long_ref = line.shift() | ||
this.obj.speed = line.shift() | ||
this.obj.course = line.shift() | ||
this.obj.date = line.shift() | ||
this.obj.variation = line.shift() | ||
this.obj.var_direction = line.shift() | ||
break; | ||
case "GLL": | ||
this.obj.latitude = this.latLngToDecimal(line.shift()) | ||
this.obj.lat_ref = line.shift() | ||
this.obj.longitude = this.latLngToDecimal(line.shift()) | ||
this.obj.long_ref = line.shift() | ||
this.obj.time = line.shift() | ||
break; | ||
case "RMA": | ||
line.shift() | ||
this.obj.latitude = this.latLngToDecimal(line.shift()) | ||
this.obj.lat_ref = line.shift() | ||
this.obj.longitude = this.latLngToDecimal(line.shift()) | ||
this.obj.long_ref = line.shift() | ||
line.shift() | ||
line.shift() | ||
this.obj.speed = line.shift() | ||
this.obj.course = line.shift() | ||
this.obj.variation = line.shift() | ||
this.obj.var_direction = line.shift() | ||
break; | ||
case "GSA": | ||
this.obj.mode = line.shift() | ||
this.obj.mode_dimension = line.shift() | ||
if(this.obj.satellites == undefined) { this.obj.satellites = [] } | ||
for(i=0; i<=11; i++) { | ||
(function(i) { | ||
id = line.shift() | ||
if (id == ''){ | ||
that.obj.satellites[i] = {} | ||
} else { | ||
if(that.obj.satellites[i] == undefined) { that.obj.satellites[i] = {} } | ||
that.obj.satellites[i].id = id | ||
} | ||
})(i); | ||
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 '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; | ||
} | ||
this.obj.pdop = line.shift() | ||
this.obj.hdop = line.shift() | ||
this.obj.vdop = line.shift() | ||
break; | ||
case "GSV": | ||
this.obj.msg_count = line.shift() | ||
this.obj.msg_num = line.shift() | ||
this.obj.num_sat = parseInt(line.shift()) | ||
if(this.obj.satellites == undefined) { this.obj.satellites = [] } | ||
/* | ||
11 | ||
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 | ||
for(i=0; i<=3; i++) { | ||
(function(i) { | ||
if(that.obj.satellites[i] == undefined) { that.obj.satellites[i] = {} } | ||
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 | ||
*/ | ||
that.obj.satellites[i].elevation = line.shift() | ||
that.obj.satellites[i].azimuth = line.shift() | ||
that.obj.satellites[i].snr = line.shift() | ||
})(i); | ||
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; | ||
} | ||
break; | ||
case "HDT": | ||
this.obj.heading = line.shift() | ||
break; | ||
case "ZDA": | ||
this.obj.time = line.shift() | ||
day = line.shift() | ||
month = line.shift() | ||
year = line.shift() | ||
if (year.length > 2){ | ||
year = [2, 2] | ||
/* | ||
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)); | ||
} | ||
} | ||
this.obj.date = day + month + year | ||
this.obj.local_hour_offset = line.shift() | ||
this.obj.local_minute_offset = line.shift() | ||
break; | ||
default: | ||
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 | ||
}; | ||
} | ||
Object.keys(this.obj).map(function (key) { | ||
val = that.obj[key] | ||
if(val === ""){ | ||
delete that.obj[key] | ||
} | ||
}); | ||
this.reads ++ | ||
this.collected.push(type) | ||
this.collected = this.collected.filter(function(v,i,s){ | ||
return that.onlyUnique(v,i,s) | ||
}); | ||
if (this.reads > 5 && this.collected.indexOf('GGA') > -1 && this.collected.indexOf('RMC') > -1){ | ||
this.emit('location', this.obj) | ||
this.reads = 0 | ||
this.collected = [] | ||
this.obj = {} | ||
}; | ||
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.onlyUnique = function(value, index, self) { | ||
return self.indexOf(value) === index; | ||
} | ||
} | ||
GPS.prototype.__proto__ = events.EventEmitter.prototype; | ||
module.exports = GPS | ||
})(this); |
{ | ||
"author": "Andrew Nesbitt <andrewnez@gmail.com> (http://andrew.github.com)", | ||
"homepage": "https://github.com/andrew/node-gps", | ||
"keywords": [ | ||
"gps", | ||
"nodecopter", | ||
"nmea" | ||
], | ||
"bugs": { | ||
"url": "https://github.com/andrew/node-gps/issues" | ||
}, | ||
"licenses": [ | ||
{ | ||
"type": "MIT", | ||
"url": "https://github.com/andrew/node-gps/blob/master/LICENSE" | ||
"name": "gps", | ||
"title": "gps.js", | ||
"version": "0.2.0", | ||
"homepage": "https://github.com/infusion/GPS.js", | ||
"bugs": "https://github.com/infusion/GPS.js/issues", | ||
"description": "A GPS NMEA parser library", | ||
"keywords": ["nmea", "gps", "serial", "parser", "distance", "geo", "location", "rmc", "gga", "gll", "gsa", "vtg", "gva"], | ||
"author": "Robert Eisele <robert@xarg.org> (http://www.xarg.org/)", | ||
"main": "gps", | ||
"private": false, | ||
"readmeFilename": "README.md", | ||
"directories": { | ||
"example": "examples" | ||
}, | ||
"license": "MIT OR GPL-2.0", | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/infusion/GPS.js.git" | ||
}, | ||
"engines": { | ||
"node": "*" | ||
}, | ||
"scripts": { | ||
"test": "mocha tests/*.js" | ||
}, | ||
"devDependencies": { | ||
"express": "*", | ||
"socket.io": "*", | ||
"serialport": "*", | ||
"angles": "*", | ||
"mocha": "*", | ||
"chai": "*", | ||
"byline": "*" | ||
} | ||
], | ||
"main": "./sass.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/andrew/node-gps.git" | ||
}, | ||
"name": "gps", | ||
"version": "0.1.0", | ||
"description": "GPS event emitter", | ||
"main": "gps.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"dependencies": { | ||
"serialport": "~1.2.2" | ||
} | ||
} |
330
README.md
@@ -1,37 +0,325 @@ | ||
# node-gps | ||
Read GPS data from GPS serial devices | ||
![GPS.js](https://github.com/infusion/GPS.js/blob/master/res/logo.png?raw=true "Javascript GPS Parser") | ||
## Install | ||
[![NPM Package](https://img.shields.io/npm/v/gps.svg?style=flat)](https://npmjs.org/package/gps "View this project on npm") | ||
[![Build Status](https://travis-ci.org/infusion/GPS.js.svg?branch=master)](https://travis-ci.org/infusion/GPS.js) | ||
[![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) | ||
```bash | ||
npm install gps | ||
GPS.js is an extensible parser for [NMEA](http://www.gpsinformation.org/dale/nmea.htm) sentences, given by any common GPS receiver. The output is tried to be as high-level as possible to make it more useful than simply splitting the information. The aim is, that you don't have to understand NMEA, just plug in your receiver and you're ready to go. | ||
Usage | ||
=== | ||
The interface of GPS.js is as simple as the following few lines. You need to add an event-listener for the completion of the task and invoke the update method with a sentence you want to process. There are much more examples in the examples folder. | ||
```javascript | ||
var gps = new GPS; | ||
// Add an event listener on all protocols | ||
gps.on('data', function(parsed) { | ||
console.log(parsed); | ||
}); | ||
// Call the update routine directly with a NMEA sentence, which would | ||
// come from the serial port or stream-reader normally | ||
gps.update("$GPGGA,224900.000,4832.3762,N,00903.5393,E,1,04,7.8,498.6,M,48.0,M,,0000*5E"); | ||
``` | ||
## Usage | ||
It's also possible to add event-listeners only on one of the following protocols, by stating `gps.on('GGA', ...)` for example. | ||
State | ||
=== | ||
The real advantage over other NMEA implementations is, that the GPS information is interpreted and normalized. The most high-level API is the state object, which changes with every new event. You can use this information with: | ||
```javascript | ||
var GPS = require('gps') | ||
gps.on('data', function() { | ||
console.log(gps.state); | ||
}); | ||
``` | ||
var gps = new GPS(); | ||
gps.on('location', function(data) { | ||
console.log(data); | ||
Installation | ||
=== | ||
Installing GPS.js is as easy as cloning this repo or use the following command: | ||
``` | ||
npm install --save gps | ||
``` | ||
Find the serial device | ||
=== | ||
On Linux serial devices typically have names like `/dev/ttyS1`, on OSX `/dev/tty.usbmodem1411` after installing a USB to serial driver and on Windows, you're probably fine by using the highest COM device you can find in the device manager. Please note that if you have multople USB ports on your computer and use them randomly, you have to lookup the path/device again. | ||
If you find yourself on a BeagleBone, the serial device must be registered manually. Luckily, this can be done within node quite easily using [octalbonescript](https://www.npmjs.com/package/octalbonescript): | ||
```javascript | ||
var obs = require('octalbonescript'); | ||
obs.serial.enable('/dev/ttyS1', function() { | ||
console.log('serial device activated'); | ||
}); | ||
``` | ||
## Development | ||
Examples | ||
=== | ||
Source hosted at [GitHub](http://github.com/andrew/node-gps). | ||
Report Issues/Feature requests on [GitHub Issues](http://github.com/andrew/node-gps). | ||
GPS.js comes with some examples, like drawing the current latutude and longitude to Google Maps, displaying a persistent state and displaying the parsed raw data. In some cases you have to adjust the serial path to your own GPS receiver to make it work. | ||
### Note on Patches/Pull Requests | ||
Simple serial example | ||
--- | ||
* Fork the project. | ||
* Make your feature addition or bug fix. | ||
* Add tests for it. This is important so I don't break it in a future version unintentionally. | ||
* Send me a pull request. Bonus points for topic branches. | ||
```javascript | ||
var SerialPort = require('serialport'); | ||
var port = new SerialPort.SerialPort('/dev/tty.usbserial', { // change path | ||
baudrate: 4800, | ||
parser: SerialPort.parsers.readline('\r\n') | ||
}); | ||
## Copyright | ||
var GPS = require('gps'); | ||
var gps = new GPS; | ||
Copyright (c) 2013 Andrew Nesbitt. See [LICENSE](https://github.com/andrew/node-gps/blob/master/LICENSE) for details. | ||
gps.on('data', function(data) { | ||
console.log(data, gps.state); | ||
}); | ||
port.on('data', function(data) { | ||
gps.update(data); | ||
}); | ||
``` | ||
Dashboard | ||
--- | ||
Go into the folder `examples/dashboard` and start the server with | ||
``` | ||
node server | ||
``` | ||
After that you can open the browser and go to http://localhost:3000 The result should look like, which in principle is just a visualiziation of the state object `gps.state` | ||
![GPS TU Dresden](https://github.com/infusion/GPS.js/blob/master/res/dashboard.png?raw=true) | ||
Google Maps | ||
--- | ||
Go into the folder `examples/maps` and start the server with | ||
``` | ||
node server | ||
``` | ||
After that you can open the browser and go to http://localhost:3000 The result should look like | ||
![GPS Google Maps Dresden](https://github.com/infusion/GPS.js/blob/master/res/maps.png?raw=true) | ||
Confluence | ||
--- | ||
[Confluence](http://www.confluence.org/) is a project, which tries to travel to and document all integer GPS coordinates. GPS.js can assist on that goal. Go into the examples folder and run: | ||
``` | ||
node confluence | ||
``` | ||
You should see something like the following, updating as you move around | ||
``` | ||
You are at (48.53, 9.05951), | ||
The closest confluence point (49, 9) is in 51.36 km. | ||
You have to go 355.2° N | ||
``` | ||
Set Time | ||
--- | ||
On systems without a RTC - like Raspberry PI - you need to update the time yourself at runtime. If the device has an internet connection, it's quite easy to use an NTP server. An alternative for disconnected projects with access to a GPS receiver can be the high-precision time signal, sent by satellites. Go to the examples folder and run the following to update the time: | ||
``` | ||
node set-time | ||
``` | ||
Available Methods | ||
=== | ||
update(line) | ||
--- | ||
The update method is the most important function, it adds a new NMEA sentence and forces the callback to trigger | ||
updatePartial(chunk) | ||
--- | ||
Will call `update()` when a full NMEA sentence has been arrived | ||
on(event, callback) | ||
--- | ||
Adds an event listener for a protocol to occur (see implemented protocols, simply use the name - upper case) or for all sentences with `data`. Because GPS.js should be more general, it doesn't inherit `EventEmitter`, but simply invokes the callback. | ||
off(event) | ||
--- | ||
Removes an event listener | ||
Implemented Protocols | ||
=== | ||
GGA - Fix information | ||
--- | ||
Gets the data, you're most probably looking for: *latitude and longitude* | ||
The parsed object will have the following attributes: | ||
- type: "GGA" | ||
- time: The time given as a JavaScript Date object | ||
- lat: The latitude | ||
- lon: The longitude | ||
- alt: The altitude | ||
- quality: Fix quality (either invalid, fix or diff) | ||
- satelites: Number of satellites being tracked | ||
- hdop: Horizontal [dilution of precision](https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)) | ||
- geoidal: Height of geoid (mean sea level) | ||
- age: time in seconds since last DGPS update | ||
- stationID: DGPS station ID number | ||
- valid: Indicates if the checksum is okay | ||
RMC - NMEAs own version of essential GPS data | ||
--- | ||
Similar to GGA but gives also delivers the velocity | ||
The parsed object will have the following attributes: | ||
- type: "RMC" | ||
- time: The time given as a JavaScript Date object | ||
- status: Status active or void | ||
- lat: The latitude | ||
- lon: The longitude | ||
- speed: Speed over the ground in km/h | ||
- track: Track angle in degrees | ||
- variation: Magnetic Variation | ||
- faa: The FAA mode, introduced with NMEA 2.3 | ||
- valid: Indicates if the checksum is okay | ||
GSA - Active satellites | ||
--- | ||
The parsed object will have the following attributes: | ||
- type: "GSA" | ||
- mode: Auto selection of 2D or 3D fix (either auto or manual) | ||
- fix: The selected fix mode (either 2D or 3D) | ||
- satellites: Array of satellite IDs | ||
- pdop: Position [dilution of precision](https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)) | ||
- vdop: Vertical [dilution of precision](https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)) | ||
- hdop: Horizontal [dilution of precision](https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)) | ||
- valid: Indicates if the checksum is okay | ||
GLL - Geographic Position - Latitude/Longitude | ||
--- | ||
The parsed object will have the following attributes: | ||
- type: "GLL" | ||
- lat: The latitude | ||
- lon: The longitude | ||
- status: Status active or void | ||
- time: The time given as a JavaScript Date object | ||
- valid: Indicates if the checksum is okay | ||
GSV - List of Satellites in view | ||
--- | ||
GSV messages are paginated. `msgNumber` indicates the current page and `msgsTotal` is the total number of pages. | ||
The parsed object will have the following attributes: | ||
- type: "GSV" | ||
- msgNumber: Current page | ||
- msgsTotal: Number of pages | ||
- satellites: Array of satellite objects with the following attributes: | ||
- prn: Satellite PRN number | ||
- elevation: Elevation in degrees | ||
- azimuth: Azimuth in degrees | ||
- snr: Signal to Noise Ratio (higher is better) | ||
- valid: Indicates if the checksum is okay | ||
VTG - vector track and speed over ground | ||
--- | ||
The parsed object will have the following attributes: | ||
- type: "VTG" | ||
- track: Track in degrees | ||
- speed: Speed over ground in km/h | ||
- faa: The FAA mode, introduced with NMEA 2.3 | ||
- valid: Indicates if the checksum is okay | ||
ZDA - UTC day, month, and year, and local time zone offset | ||
--- | ||
The parsed object will have the following attributes: | ||
- type: "ZDA" | ||
- time: The time given as a JavaScript Date object | ||
GPS State | ||
=== | ||
If the streaming API is not needed, but a solid state of the system, the `gps.state` object can be used. It has the following properties: | ||
- time: Current time | ||
- lat: Latitude | ||
- lon: Longitude | ||
- alt: Altitude | ||
- satsActive: Array of active satellites | ||
- speed: Speed over ground in km/h | ||
- track: Track in degrees | ||
- satsVisible: Array of all visible satellites | ||
Adding new protocols is a matter of minutes. If you need a protocol which isn't implemented, I'm happy to see a pull request or a new ticket. | ||
Troubleshooting | ||
=== | ||
If you don't get valid position information after turning on the receiver, chances are high you simply have to wait as it takes some [time to first fix](https://en.wikipedia.org/wiki/Time_to_first_fix). | ||
Functions | ||
=== | ||
GPS.js comes with a few static functions, which help by working with geo-coordinates. | ||
GPS.Parse(line) | ||
--- | ||
Parses a single line and returns the resulting object, in case the callback system isn't needed/wanted | ||
GPS.Distance(latFrom, lonFrom, latTo, lonTo) | ||
--- | ||
Calculates the distance between two geo-coordinates using Haversine formula | ||
GPS.Heading(latFrom, lonFrom, latTo, lonTo) | ||
--- | ||
Calculates the angle from one coordinate to another. Heading is represented as windrose coordinates (N=0, E=90, S=189, W=270). The result can be used as the argument of [angles](https://github.com/infusion/Angles.js) `compass()` method: | ||
```javascript | ||
var angles = require('angles'); | ||
console.log(angles.compass(GPS.Heading(50, 10, 51, 9))); // will return x ∈ { N, S, E, W, NE, ... } | ||
``` | ||
Using GPS.js with the browser | ||
=== | ||
The use cases should be rare to parse NMEA directly inside the browser, but it works too. | ||
```html | ||
<script src="gps.js"></script> | ||
<script> | ||
var gps = new GPS; | ||
gps.update('...'); | ||
</script> | ||
``` | ||
Testing | ||
=== | ||
If you plan to enhance the library, make sure you add test cases and all the previous tests are passing. You can test the library with | ||
``` | ||
npm test | ||
``` | ||
Copyright and licensing | ||
=== | ||
Copyright (c) 2016, Robert Eisele (robert@xarg.org) | ||
Dual licensed under the MIT or GPL Version 2 licenses. | ||
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
310815
0
24
1348
1
326
7
90
1
2
4
- Removedserialport@~1.2.2
- Removedasync@0.1.18(transitive)
- Removedbindings@1.1.1(transitive)
- Removedoptimist@0.3.7(transitive)
- Removedserialport@1.2.5(transitive)
- Removedsf@0.1.6(transitive)
- Removedwordwrap@0.0.3(transitive)