From deb0f32d1b1395f8c7f1a0a2df6faa4d200f338b Mon Sep 17 00:00:00 2001 From: Robert Eisele Date: Fri, 4 Oct 2024 19:41:45 +0200 Subject: [PATCH] Release v0.7 --- .gitignore | 89 +-- .travis.yml | 12 - LICENSE | 2 +- README.md | 72 ++- bower.json | 31 - dist/gps.js | 951 +++++++++++++++++++++++++++++ dist/gps.min.js | 23 + dist/gps.mjs | 948 +++++++++++++++++++++++++++++ examples/confluence.js | 14 +- examples/dashboard/dashboard.html | 916 ++++++++++++++-------------- examples/dashboard/server.js | 12 +- examples/fileRead.js | 6 +- examples/fileWrite.js | 6 +- examples/json-stream.js | 14 +- examples/maps/maps.html | 254 ++++---- examples/maps/server.js | 10 +- examples/serial.js | 6 +- examples/set-date.js | 8 +- examples/simple.js | 4 +- examples/state.js | 6 +- gps.js | 965 ------------------------------ gps.min.js | 21 - package.json | 53 +- src/gps.js | 951 +++++++++++++++++++++++++++++ tests/functions.js | 16 +- tests/parser.js | 235 ++++++-- tests/partial.js | 60 +- 27 files changed, 3800 insertions(+), 1885 deletions(-) delete mode 100644 .travis.yml delete mode 100644 bower.json create mode 100644 dist/gps.js create mode 100644 dist/gps.min.js create mode 100644 dist/gps.mjs delete mode 100644 gps.js delete mode 100644 gps.min.js create mode 100644 src/gps.js diff --git a/.gitignore b/.gitignore index f0aeaf5..9daa824 100644 --- a/.gitignore +++ b/.gitignore @@ -1,87 +1,2 @@ - -# Created by https://www.gitignore.io/api/node -# Edit at https://www.gitignore.io/?templates=node - -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# next.js build output -.next - -# nuxt.js build output -.nuxt - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# End of https://www.gitignore.io/api/node +.DS_Store +node_modules diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0526b45..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: node_js -node_js: - - "stable" -script: npm test -env: - - CXX=g++-4.8 -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 diff --git a/LICENSE b/LICENSE index 49057d3..8741015 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Robert Eisele +Copyright (c) 2025 Robert Eisele Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 58ff05f..514febc 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,13 @@ ![GPS.js](https://github.com/infusion/GPS.js/blob/master/res/logo.png?raw=true "Javascript GPS Parser") [![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) 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 -=== +## 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. @@ -28,8 +27,8 @@ gps.update("$GPGGA,224900.000,4832.3762,N,00903.5393,E,1,04,7.8,498.6,M,48.0,M,, It's also possible to add event-listeners only on one of the following protocols, by stating `gps.on('GGA', ...)` for example. -State -=== +## 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: @@ -39,16 +38,16 @@ gps.on('data', () => { }); ``` -Installation -=== +## Installation + Installing GPS.js is as easy as cloning this repo or use the following command: ``` npm install gps ``` -Find the serial device -=== +## 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 multiple USB ports on your computer and use them randomly, you have to lookup the path/device again. @@ -64,8 +63,8 @@ obs.serial.enable('/dev/ttyS1', () => { }); ``` -Examples -=== +## Examples + GPS.js comes with some examples, like drawing the current latitude 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. @@ -142,8 +141,8 @@ On systems without a RTC - like Raspberry PI - you need to update the time yours node set-date ``` -Available Methods -=== +## Available Methods + update(line) --- @@ -161,8 +160,8 @@ off(event) --- Removes an event listener -Implemented Protocols -=== +## Implemented Protocols + GGA - Fix information --- @@ -287,8 +286,8 @@ The parsed object will have the following attributes: - heightError: Height 1 sigma error, in meters - valid: Indicates if the checksum is okay -GPS State -=== +## 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 @@ -303,12 +302,12 @@ If the streaming API is not needed, but a solid state of the system, the `gps.st 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 -=== +## 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 -=== +## Functions + GPS.js comes with a few static functions, which helps working with geo-coordinates. @@ -334,27 +333,36 @@ console.log(angles.compass(GPS.Heading(50, 10, 51, 9))); // will return x ∈ { ``` -Using GPS.js with the browser -=== +## Using GPS.js with the browser + The use cases should be rare to parse NMEA directly inside the browser, but it works too. ```html - + ``` -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 +## Building the library + +After cloning the Git repository run: ``` -npm test +npm install +npm run build ``` -Copyright and licensing -=== -Copyright (c) 2016-2022, [Robert Eisele](https://www.xarg.org/) -Dual licensed under the MIT or GPL Version 2 licenses. +## Run a test + +Testing the source against the shipped test suite is as easy as + +``` +npm run test +``` + +## Copyright and licensing + +Copyright (c) 2025, [Robert Eisele](https://raw.org/) +Licensed under the MIT license. diff --git a/bower.json b/bower.json deleted file mode 100644 index 190ce69..0000000 --- a/bower.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "gps", - "main": "gps.js", - "version": "0.6.1", - "homepage": "https://github.com/infusion/GPS.js", - "description": "A GPS NMEA parser library", - "keywords": [ - "nmea", "nmea", "gps", "serial", "parser", "distance", "geo", "location", "rmc", "gga", "gll", "gsa", "vtg", "gva" - ], - "moduleType": [ - "amd", - "globals", - "node" - ], - "authors": [ - "Robert Eisele (http://www.xarg.org/)" - ], - "license": [ - "MIT", - "GPL" - ], - "repository": { - "type": "git", - "url": "git://github.com/infusion/GPS.js.git" - }, - "ignore": [ - "tests", - ".travis.yml", - "package.json" - ] -} diff --git a/dist/gps.js b/dist/gps.js new file mode 100644 index 0000000..c8ea054 --- /dev/null +++ b/dist/gps.js @@ -0,0 +1,951 @@ +'use strict'; + +var D2R = Math.PI / 180; + +var collectSats = {}; +var collectActiveSats = {}; +var lastSeenSat = {}; + +function updateState(state, data) { + + // TODO: can we really use RMC time here or is it the time of fix? + if (data['type'] === 'RMC' || data['type'] === 'GGA' || data['type'] === 'GLL' || data['type'] === 'GNS') { + state['time'] = data['time']; + state['lat'] = data['lat']; + state['lon'] = data['lon']; + } + + if (data['type'] === 'HDT') { + state['heading'] = data['heading']; + state['trueNorth'] = data['trueNorth']; + } + + 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') { + + var systemId = data['systemId']; + collectActiveSats[systemId] = data['satellites']; + var satsActive = []; + for (var s in collectActiveSats) { + satsActive.push(...collectActiveSats[s]); + } + + state['satsActive'] = satsActive; + state['fix'] = data['fix']; + state['hdop'] = data['hdop']; + state['pdop'] = data['pdop']; + state['vdop'] = data['vdop']; + } + + if (data['type'] === 'GSV') { + + var now = new Date().getTime(); + + var sats = data['satellites']; + for (var i = 0; i < sats.length; i++) { + var key = sats[i].key; + lastSeenSat[key] = now; + collectSats[key] = sats[i]; + } + + var ret = []; + for (var key in collectSats) { + if (now - lastSeenSat[key] < 3000) // Sats are visible for 3 seconds + ret.push(collectSats[key]) + } + state['satsVisible'] = ret; + } +} + +/** + * + * @param {String} time + * @param {String=} date + * @returns {Date} + */ +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(Number(year), Number(month), Number(day)); + } else { + // If we need to parse older GPRMC data, we should hack something like + // year < 73 ? 2000+year : 1900+year + // Since GPS appeared in 1973 + ret.setUTCFullYear(Number('20' + year), Number(month), Number(day)); + } + } + + ret.setUTCHours(Number(time.slice(0, 2))); + ret.setUTCMinutes(Number(time.slice(2, 4))); + ret.setUTCSeconds(Number(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(Number(ms)); + + return ret; +} + +function parseCoord(coord, dir) { + + // Latitude can go from 0 to 90; longitude can go from -180 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 parseSystemId(systemId) { + switch (systemId) { + case 0: + return "QZSS"; + case 1: + return "GPS"; + case 2: + return "GLONASS"; + case 3: + return "Galileo"; + case 4: + return "BeiDou" + default: + return "unknown" + } +} + +function parseSystem(str) { + var satellite = str.slice(1, 3); + switch (satellite) { + case "GP": + return "GPS"; + case "GQ": + return "QZSS"; + case "GL": + return "GLONASS"; + case "GA": + return "Galileo"; + case "GB": + return "BeiDou" + default: + return satellite; + } +} + + +function parseGSAMode(mode) { + + switch (mode) { + case 'M': + return 'manual'; + case 'A': + return 'automatic'; + case '': + return null; + } + throw new Error('INVALID GSA MODE: ' + mode); +} + +function parseGGAFix(fix) { + + if (fix === '') return null; + + switch (parseInt(fix, 10)) { + 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 (real time kinematic) RTK fix + case 5: + return 'rtk-float'; // valid (real time kinematic) RTK float + case 6: + return 'estimated'; // dead reckoning + case 7: + return 'manual'; + case 8: + return 'simulated'; + } + throw new Error('INVALID GGA FIX: ' + fix); +} + +function parseGSAFix(fix) { + + if (fix === '') return null; + + switch (parseInt(fix, 10)) { + case 1: + return null; + case 2: + return '2D'; + case 3: + return '3D'; + } + throw new Error('INVALID GSA FIX: ' + fix); +} + +function parseRMC_GLLStatus(status) { + + switch (status) { + case '': + return null; + case 'A': + return 'active'; + case 'V': + return 'void'; + } + throw new Error('INVALID RMC/GLL STATUS: ' + status); +} + +function parseFAA(faa) { + + // Only A and D will correspond to an Active and reliable Sentence + + switch (faa) { + case '': + return null; + case 'A': + return 'autonomous'; + case 'D': + return 'differential'; + case 'E': + return 'estimated'; // dead reckoning + case 'M': + return 'manual input'; + case 'S': + return 'simulated'; + case 'N': + return 'not valid'; + case 'P': + return 'precise'; + case 'R': + return 'rtk'; // valid (real time kinematic) RTK fix + case 'F': + return 'rtk-float'; // valid (real time kinematic) RTK float + } + throw new Error('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 new Error('Unknown unit: ' + unit); +} + +/** + * + * @constructor + */ +function GPS() { + + if (!(this instanceof GPS)) { + return new GPS; + } + + this['events'] = {}; + this['state'] = { 'errors': 0, 'processed': 0 }; +} + +GPS.prototype['events'] = null; +GPS.prototype['state'] = null; + +GPS['mod'] = { + // Global Positioning System Fix Data + 'GGA': function (str, gga) { + + if (gga.length !== 16 && gga.length !== 14) { + throw new Error('Invalid GGA length: ' + str); + } + + /* + 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 + + 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]), + 'satellites': parseNumber(gga[7]), + 'hdop': parseNumber(gga[8]), // dilution + 'geoidal': parseDist(gga[11], gga[12]), // aboveGeoid + 'age': gga[13] === undefined ? null : parseNumber(gga[13]), // dgps time since update + 'stationID': gga[14] === undefined ? null : parseNumber(gga[14]) // dgpsReference?? + }; + }, + // GPS DOP and active satellites + 'GSA': function (str, gsa) { + + if (gsa.length !== 19 && gsa.length !== 20) { + throw new Error('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) = systemID NMEA 4.10 + 18 = Checksum + */ + + var sats = []; + for (var i = 3; i < 15; 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]), + 'systemId': gsa.length > 19 ? parseNumber(gsa[18]) : null, + 'system': gsa.length > 19 ? parseSystemId(parseNumber(gsa[18])) : 'unknown' + }; + }, + // Recommended Minimum data for gps + 'RMC': function (str, rmc) { + + if (rmc.length !== 13 && rmc.length !== 14 && rmc.length !== 15) { + throw new Error('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) + (13) = NMEA 4.10 introduced nav status + 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]), // heading + 'variation': parseRMCVariation(rmc[10], rmc[11]), + 'faa': rmc.length > 13 ? parseFAA(rmc[12]) : null, + 'navStatus': rmc.length > 14 ? rmc[13] : null + }; + }, + // Track info + 'VTG': function (str, vtg) { + + if (vtg.length !== 10 && vtg.length !== 11) { + throw new Error('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 + ------------------------------------------------------------------------------ + + 1 = Track made good (degrees true) + 2 = Fixed text 'T' indicates that track made good is relative to true north + 3 = optional: Track made good (degrees magnetic) + 4 = optional: M: track made good is relative to magnetic north + 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, + 'trackMagetic': null, + 'speed': null, + 'faa': null + }; + } + + if (vtg[2] !== 'T') { + throw new Error('Invalid VTG track mode: ' + str); + } + + if (vtg[8] !== 'K' || vtg[6] !== 'N') { + throw new Error('Invalid VTG speed tag: ' + str); + } + + return { + 'track': parseNumber(vtg[1]), // heading + 'trackMagnetic': vtg[3] === '' ? null : parseNumber(vtg[3]), // heading uncorrected to magnetic north + 'speed': parseKnots(vtg[5]), + 'faa': vtg.length === 11 ? parseFAA(vtg[9]) : null + }; + }, + // satellites in view + 'GSV': function (str, gsv) { + + if (gsv.length % 4 === 0) { + // = 1 -> normal package + // = 2 -> NMEA 4.10 extension + // = 3 -> BeiDou extension? + throw new Error('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 + repeat [ + 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) + ] + N+1 = signalID NMEA 4.10 + N+2 = Checksum + */ + + var sats = []; + var satellite = str.slice(1, 3); + + for (var i = 4; i < gsv.length - 3; i += 4) { + + var prn = parseNumber(gsv[i]); + var snr = parseNumber(gsv[i + 3]); + /* + Plot satellites in Radar chart with north on top + by linear map elevation from 0° to 90° into r to 0 + + centerX + cos(azimuth - 90) * (1 - elevation / 90) * radius + centerY + sin(azimuth - 90) * (1 - elevation / 90) * radius + */ + 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, + 'system': parseSystem(str), + 'key': satellite + prn + }); + } + + return { + 'msgNumber': parseNumber(gsv[2]), + 'msgsTotal': parseNumber(gsv[1]), + 'satsInView': parseNumber(gsv[3]), + 'satellites': sats, + 'signalId': gsv.length % 4 === 2 ? parseNumber(gsv[gsv.length - 2]) : null,// NMEA 4.10 addition + 'system': parseSystem(str) + }; + }, + // Geographic Position - Latitude/Longitude + 'GLL': function (str, gll) { + + if (gll.length !== 9 && gll.length !== 8) { + throw new Error('Invalid GLL length: ' + str); + } + + /* + ------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 + | | | | | | | | + $--GLL,llll.ll,a,yyyyy.yy,a,hhmmss.ss,a,m,*hh + ------------------------------------------------------------------------------ + + 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]), + 'faa': gll.length === 9 ? parseFAA(gll[7]) : null + }; + }, + // 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 + }; + }, + 'GST': function (str, gst) { + + if (gst.length !== 10) { + throw new Error('Invalid GST length: ' + str); + } + + /* + 1 = Time (UTC) + 2 = RMS value of the pseudorange residuals; includes carrier phase residuals during periods of RTK (float) and RTK (fixed) processing + 3 = Error ellipse semi-major axis 1 sigma error, in meters + 4 = Error ellipse semi-minor axis 1 sigma error, in meters + 5 = Error ellipse orientation, degrees from true north + 6 = Latitude 1 sigma error, in meters + 7 = Longitude 1 sigma error, in meters + 8 = Height 1 sigma error, in meters + 9 = Checksum + */ + + return { + 'time': parseTime(gst[1]), + 'rms': parseNumber(gst[2]), + 'ellipseMajor': parseNumber(gst[3]), + 'ellipseMinor': parseNumber(gst[4]), + 'ellipseOrientation': parseNumber(gst[5]), + 'latitudeError': parseNumber(gst[6]), + 'longitudeError': parseNumber(gst[7]), + 'heightError': parseNumber(gst[8]) + }; + }, + + // add HDT + 'HDT': function (str, hdt) { + + if (hdt.length !== 4) { + throw new Error('Invalid HDT length: ' + str); + } + + /* + ------------------------------------------------------------------------------ + 1 2 3 + | | | + $--HDT,hhh.hhh,T*XX + ------------------------------------------------------------------------------ + + 1. Heading in degrees + 2. T: indicates heading relative to True North + 3. Checksum + */ + + return { + 'heading': parseFloat(hdt[1]), + 'trueNorth': hdt[2] === 'T' + }; + }, + + 'GRS': function (str, grs) { + + if (grs.length !== 18) { + throw new Error('Invalid GRS length: ' + str); + } + + var res = []; + for (var i = 3; i <= 14; i++) { + var tmp = parseNumber(grs[i]); + if (tmp !== null) + res.push(tmp); + } + + return { + 'time': parseTime(grs[1]), + 'mode': parseNumber(grs[2]), + 'res': res + }; + }, + 'GBS': function (str, gbs) { + + if (gbs.length !== 10 && gbs.length !== 12) { + throw new Error('Invalid GBS length: ' + str); + } + + /* + 0 1 2 3 4 5 6 7 8 + | | | | | | | | | + $--GBS,hhmmss.ss,x.x,x.x,x.x,x.x,x.x,x.x,x.x*hh + + 1. UTC time of the GGA or GNS fix associated with this sentence + 2. Expected error in latitude (meters) + 3. Expected error in longitude (meters) + 4. Expected error in altitude (meters) + 5. PRN (id) of most likely failed satellite + 6. Probability of missed detection for most likely failed satellite + 7. Estimate of bias in meters on most likely failed satellite + 8. Standard deviation of bias estimate + -- + 9. systemID (NMEA 4.10) + 10. signalID (NMEA 4.10) + */ + + return { + 'time': parseTime(gbs[1]), + 'errLat': parseNumber(gbs[2]), + 'errLon': parseNumber(gbs[3]), + 'errAlt': parseNumber(gbs[4]), + 'failedSat': parseNumber(gbs[5]), + 'probFailedSat': parseNumber(gbs[6]), + 'biasFailedSat': parseNumber(gbs[7]), + 'stdFailedSat': parseNumber(gbs[8]), + 'systemId': gbs.length === 12 ? parseNumber(gbs[9]) : null, + 'signalId': gbs.length === 12 ? parseNumber(gbs[10]) : null + }; + }, + 'GNS': function (str, gns) { + + if (gns.length !== 14 && gns.length !== 15) { + throw new Error('Invalid GNS length: ' + str); + } + + return { + 'time': parseTime(gns[1]), + 'lat': parseCoord(gns[2], gns[3]), + 'lon': parseCoord(gns[4], gns[5]), + 'mode': gns[6], + 'satsUsed': parseNumber(gns[7]), + 'hdop': parseNumber(gns[8]), + 'alt': parseNumber(gns[9]), + 'sep': parseNumber(gns[10]), + 'diffAge': parseNumber(gns[11]), + 'diffStation': parseNumber(gns[12]), + 'navStatus': gns.length === 15 ? gns[13] : null // NMEA 4.10 + }; + } +}; + +GPS['Parse'] = function (line) { + + if (typeof line !== 'string') + return false; + + var nmea = line.split(','); + + var last = nmea.pop(); + + // HDT is 2 items length + if (nmea.length < 2 || 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 y = sdlon * clat2; + var x = clat1 * slat2 - slat1 * clat2 * cdlon; + + var head = Math.atan2(y, x) * 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['TotalDistance'] = function (path) { + + if (path.length < 2) + return 0; + + var len = 0; + for (var i = 0; i < path.length - 1; i++) { + var c = path[i]; + var n = path[i + 1]; + len += GPS['Distance'](c['lat'], c['lon'], n['lat'], n['lon']); + } + return len; +}; + +GPS.prototype['update'] = function (line) { + + var parsed = GPS['Parse'](line); + + this['state']['processed']++; + + if (parsed === false) { + this['state']['errors']++; + return false; + } + + updateState(this['state'], parsed); + + this['emit']('data', parsed); + this['emit'](parsed.type, 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) === '$') { + try { + this['update'](line); + } catch (err) { + this['partial'] = ""; + throw new Error(err); + } + } + 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; +}; + +GPS.prototype['emit'] = function (ev, data) { + if (this['events'][ev] !== undefined) { + this['events'][ev].call(this, data); + } +}; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = GPS; +module.exports = GPS; diff --git a/dist/gps.min.js b/dist/gps.min.js new file mode 100644 index 0000000..3139be7 --- /dev/null +++ b/dist/gps.min.js @@ -0,0 +1,23 @@ +/* +GPS.js v0.7.0 10/4/2024 +https://raw.org/article/using-gps-with-node-js-and-javascript/ + +Copyright (c) 2024, Robert Eisele (https://raw.org/) +Licensed under the MIT license. +*/ +'use strict';(function(A){function k(b,a){if(""===b)return null;var c=new Date;if(a){var e=a.slice(4),f=a.slice(2,4)-1;a=a.slice(0,2);4===e.length?c.setUTCFullYear(Number(e),Number(f),Number(a)):c.setUTCFullYear(Number("20"+e),Number(f),Number(a))}c.setUTCHours(Number(b.slice(0,2)));c.setUTCMinutes(Number(b.slice(2,4)));c.setUTCSeconds(Number(b.slice(4,6)));b=b.slice(7);e=b.length;f=0;0!==e&&(f=parseFloat(b)*Math.pow(10,3-e));c.setUTCMilliseconds(Number(f));return c}function l(b,a){if(""===b)return null; +var c=1;switch(a){case "S":c=-1;case "N":var e=2;break;case "W":c=-1;case "E":e=3}return c*(parseFloat(b.slice(0,e))+parseFloat(b.slice(e))/60)}function d(b){return""===b?null:parseFloat(b)}function t(b){return""===b?null:1.852*parseFloat(b)}function B(b){switch(b){case 0:return"QZSS";case 1:return"GPS";case 2:return"GLONASS";case 3:return"Galileo";case 4:return"BeiDou";default:return"unknown"}}function u(b){b=b.slice(1,3);switch(b){case "GP":return"GPS";case "GQ":return"QZSS";case "GL":return"GLONASS"; +case "GA":return"Galileo";case "GB":return"BeiDou";default:return b}}function C(b){if(""===b)return null;switch(parseInt(b,10)){case 0:return null;case 1:return"fix";case 2:return"dgps-fix";case 3:return"pps-fix";case 4:return"rtk";case 5:return"rtk-float";case 6:return"estimated";case 7:return"manual";case 8:return"simulated"}throw Error("INVALID GGA FIX: "+b);}function D(b){if(""===b)return null;switch(parseInt(b,10)){case 1:return null;case 2:return"2D";case 3:return"3D"}throw Error("INVALID GSA FIX: "+ +b);}function v(b){switch(b){case "":return null;case "A":return"active";case "V":return"void"}throw Error("INVALID RMC/GLL STATUS: "+b);}function p(b){switch(b){case "":return null;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";case "R":return"rtk";case "F":return"rtk-float"}throw Error("INVALID FAA MODE: "+b);}function w(b,a){if("M"===a||""===a)return d(b); +throw Error("Unknown unit: "+a);}function g(){if(!(this instanceof g))return new g;this.events={};this.state={errors:0,processed:0}}var m=Math.PI/180,q={},r={},x={};g.prototype.events=null;g.prototype.state=null;g.mod={GGA:function(b,a){if(16!==a.length&&14!==a.length)throw Error("Invalid GGA length: "+b);return{time:k(a[1]),lat:l(a[2],a[3]),lon:l(a[4],a[5]),alt:w(a[9],a[10]),quality:C(a[6]),satellites:d(a[7]),hdop:d(a[8]),geoidal:w(a[11],a[12]),age:void 0===a[13]?null:d(a[13]),stationID:void 0=== +a[14]?null:d(a[14])}},GSA:function(b,a){if(19!==a.length&&20!==a.length)throw Error("Invalid GSA length: "+b);b=[];for(var c=3;15>c;c++)""!==a[c]&&b.push(parseInt(a[c],10));a:{c=a[1];switch(c){case "M":c="manual";break a;case "A":c="automatic";break a;case "":c=null;break a}throw Error("INVALID GSA MODE: "+c);}return{mode:c,fix:D(a[2]),satellites:b,pdop:d(a[15]),hdop:d(a[16]),vdop:d(a[17]),systemId:19=c;c++){var e=d(a[c]);null!==e&&b.push(e)}return{time:k(a[1]),mode:d(a[2]),res:b}},GBS:function(b,a){if(10!==a.length&&12!==a.length)throw Error("Invalid GBS length: "+ +b);return{time:k(a[1]),errLat:d(a[2]),errLon:d(a[3]),errAlt:d(a[4]),failedSat:d(a[5]),probFailedSat:d(a[6]),biasFailedSat:d(a[7]),stdFailedSat:d(a[8]),systemId:12===a.length?d(a[9]):null,signalId:12===a.length?d(a[10]):null}},GNS:function(b,a){if(14!==a.length&&15!==a.length)throw Error("Invalid GNS length: "+b);return{time:k(a[1]),lat:l(a[2],a[3]),lon:l(a[4],a[5]),mode:a[6],satsUsed:d(a[7]),hdop:d(a[8]),alt:d(a[9]),sep:d(a[10]),diffAge:d(a[11]),diffStation:d(a[12]),navStatus:15===a.length?a[13]: +null}}};g.Parse=function(b){if("string"!==typeof b)return!1;var a=b.split(","),c=a.pop();if(2>a.length||"$"!==b.charAt(0)||-1===c.indexOf("*"))return!1;c=c.split("*");a.push(c[0]);a.push(c[1]);a[0]=a[0].slice(3);if(void 0!==g.mod[a[0]]){c=this.mod[a[0]](b,a);c.raw=b;for(var e=0,f=1;fb.length)return 0;for(var a=0,c=0;ce-x[h]&&c.push(q[h]);a.satsVisible=c}this.emit("data",b);this.emit(b.type,b);return!0};g.prototype.partial="";g.prototype.updatePartial=function(b){for(this.partial+=b;;){b=this.partial.indexOf("\r\n");if(-1===b)break;var a=this.partial.slice(0,b);if("$"===a.charAt(0))try{this.update(a)}catch(c){throw this.partial="",Error(c);}this.partial=this.partial.slice(b+2)}};g.prototype.on=function(b,a){return void 0===this.events[b]? +(this.events[b]=a,this):null};g.prototype.off=function(b){void 0!==this.events[b]&&(this.events[b]=void 0);return this};g.prototype.emit=function(b,a){void 0!==this.events[b]&&this.events[b].call(this,a)};A.GPS=g})(this); diff --git a/dist/gps.mjs b/dist/gps.mjs new file mode 100644 index 0000000..6c5fbe5 --- /dev/null +++ b/dist/gps.mjs @@ -0,0 +1,948 @@ +'use strict'; + +var D2R = Math.PI / 180; + +var collectSats = {}; +var collectActiveSats = {}; +var lastSeenSat = {}; + +function updateState(state, data) { + + // TODO: can we really use RMC time here or is it the time of fix? + if (data['type'] === 'RMC' || data['type'] === 'GGA' || data['type'] === 'GLL' || data['type'] === 'GNS') { + state['time'] = data['time']; + state['lat'] = data['lat']; + state['lon'] = data['lon']; + } + + if (data['type'] === 'HDT') { + state['heading'] = data['heading']; + state['trueNorth'] = data['trueNorth']; + } + + 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') { + + var systemId = data['systemId']; + collectActiveSats[systemId] = data['satellites']; + var satsActive = []; + for (var s in collectActiveSats) { + satsActive.push(...collectActiveSats[s]); + } + + state['satsActive'] = satsActive; + state['fix'] = data['fix']; + state['hdop'] = data['hdop']; + state['pdop'] = data['pdop']; + state['vdop'] = data['vdop']; + } + + if (data['type'] === 'GSV') { + + var now = new Date().getTime(); + + var sats = data['satellites']; + for (var i = 0; i < sats.length; i++) { + var key = sats[i].key; + lastSeenSat[key] = now; + collectSats[key] = sats[i]; + } + + var ret = []; + for (var key in collectSats) { + if (now - lastSeenSat[key] < 3000) // Sats are visible for 3 seconds + ret.push(collectSats[key]) + } + state['satsVisible'] = ret; + } +} + +/** + * + * @param {String} time + * @param {String=} date + * @returns {Date} + */ +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(Number(year), Number(month), Number(day)); + } else { + // If we need to parse older GPRMC data, we should hack something like + // year < 73 ? 2000+year : 1900+year + // Since GPS appeared in 1973 + ret.setUTCFullYear(Number('20' + year), Number(month), Number(day)); + } + } + + ret.setUTCHours(Number(time.slice(0, 2))); + ret.setUTCMinutes(Number(time.slice(2, 4))); + ret.setUTCSeconds(Number(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(Number(ms)); + + return ret; +} + +function parseCoord(coord, dir) { + + // Latitude can go from 0 to 90; longitude can go from -180 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 parseSystemId(systemId) { + switch (systemId) { + case 0: + return "QZSS"; + case 1: + return "GPS"; + case 2: + return "GLONASS"; + case 3: + return "Galileo"; + case 4: + return "BeiDou" + default: + return "unknown" + } +} + +function parseSystem(str) { + var satellite = str.slice(1, 3); + switch (satellite) { + case "GP": + return "GPS"; + case "GQ": + return "QZSS"; + case "GL": + return "GLONASS"; + case "GA": + return "Galileo"; + case "GB": + return "BeiDou" + default: + return satellite; + } +} + + +function parseGSAMode(mode) { + + switch (mode) { + case 'M': + return 'manual'; + case 'A': + return 'automatic'; + case '': + return null; + } + throw new Error('INVALID GSA MODE: ' + mode); +} + +function parseGGAFix(fix) { + + if (fix === '') return null; + + switch (parseInt(fix, 10)) { + 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 (real time kinematic) RTK fix + case 5: + return 'rtk-float'; // valid (real time kinematic) RTK float + case 6: + return 'estimated'; // dead reckoning + case 7: + return 'manual'; + case 8: + return 'simulated'; + } + throw new Error('INVALID GGA FIX: ' + fix); +} + +function parseGSAFix(fix) { + + if (fix === '') return null; + + switch (parseInt(fix, 10)) { + case 1: + return null; + case 2: + return '2D'; + case 3: + return '3D'; + } + throw new Error('INVALID GSA FIX: ' + fix); +} + +function parseRMC_GLLStatus(status) { + + switch (status) { + case '': + return null; + case 'A': + return 'active'; + case 'V': + return 'void'; + } + throw new Error('INVALID RMC/GLL STATUS: ' + status); +} + +function parseFAA(faa) { + + // Only A and D will correspond to an Active and reliable Sentence + + switch (faa) { + case '': + return null; + case 'A': + return 'autonomous'; + case 'D': + return 'differential'; + case 'E': + return 'estimated'; // dead reckoning + case 'M': + return 'manual input'; + case 'S': + return 'simulated'; + case 'N': + return 'not valid'; + case 'P': + return 'precise'; + case 'R': + return 'rtk'; // valid (real time kinematic) RTK fix + case 'F': + return 'rtk-float'; // valid (real time kinematic) RTK float + } + throw new Error('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 new Error('Unknown unit: ' + unit); +} + +/** + * + * @constructor + */ +function GPS() { + + if (!(this instanceof GPS)) { + return new GPS; + } + + this['events'] = {}; + this['state'] = { 'errors': 0, 'processed': 0 }; +} + +GPS.prototype['events'] = null; +GPS.prototype['state'] = null; + +GPS['mod'] = { + // Global Positioning System Fix Data + 'GGA': function (str, gga) { + + if (gga.length !== 16 && gga.length !== 14) { + throw new Error('Invalid GGA length: ' + str); + } + + /* + 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 + + 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]), + 'satellites': parseNumber(gga[7]), + 'hdop': parseNumber(gga[8]), // dilution + 'geoidal': parseDist(gga[11], gga[12]), // aboveGeoid + 'age': gga[13] === undefined ? null : parseNumber(gga[13]), // dgps time since update + 'stationID': gga[14] === undefined ? null : parseNumber(gga[14]) // dgpsReference?? + }; + }, + // GPS DOP and active satellites + 'GSA': function (str, gsa) { + + if (gsa.length !== 19 && gsa.length !== 20) { + throw new Error('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) = systemID NMEA 4.10 + 18 = Checksum + */ + + var sats = []; + for (var i = 3; i < 15; 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]), + 'systemId': gsa.length > 19 ? parseNumber(gsa[18]) : null, + 'system': gsa.length > 19 ? parseSystemId(parseNumber(gsa[18])) : 'unknown' + }; + }, + // Recommended Minimum data for gps + 'RMC': function (str, rmc) { + + if (rmc.length !== 13 && rmc.length !== 14 && rmc.length !== 15) { + throw new Error('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) + (13) = NMEA 4.10 introduced nav status + 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]), // heading + 'variation': parseRMCVariation(rmc[10], rmc[11]), + 'faa': rmc.length > 13 ? parseFAA(rmc[12]) : null, + 'navStatus': rmc.length > 14 ? rmc[13] : null + }; + }, + // Track info + 'VTG': function (str, vtg) { + + if (vtg.length !== 10 && vtg.length !== 11) { + throw new Error('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 + ------------------------------------------------------------------------------ + + 1 = Track made good (degrees true) + 2 = Fixed text 'T' indicates that track made good is relative to true north + 3 = optional: Track made good (degrees magnetic) + 4 = optional: M: track made good is relative to magnetic north + 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, + 'trackMagetic': null, + 'speed': null, + 'faa': null + }; + } + + if (vtg[2] !== 'T') { + throw new Error('Invalid VTG track mode: ' + str); + } + + if (vtg[8] !== 'K' || vtg[6] !== 'N') { + throw new Error('Invalid VTG speed tag: ' + str); + } + + return { + 'track': parseNumber(vtg[1]), // heading + 'trackMagnetic': vtg[3] === '' ? null : parseNumber(vtg[3]), // heading uncorrected to magnetic north + 'speed': parseKnots(vtg[5]), + 'faa': vtg.length === 11 ? parseFAA(vtg[9]) : null + }; + }, + // satellites in view + 'GSV': function (str, gsv) { + + if (gsv.length % 4 === 0) { + // = 1 -> normal package + // = 2 -> NMEA 4.10 extension + // = 3 -> BeiDou extension? + throw new Error('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 + repeat [ + 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) + ] + N+1 = signalID NMEA 4.10 + N+2 = Checksum + */ + + var sats = []; + var satellite = str.slice(1, 3); + + for (var i = 4; i < gsv.length - 3; i += 4) { + + var prn = parseNumber(gsv[i]); + var snr = parseNumber(gsv[i + 3]); + /* + Plot satellites in Radar chart with north on top + by linear map elevation from 0° to 90° into r to 0 + + centerX + cos(azimuth - 90) * (1 - elevation / 90) * radius + centerY + sin(azimuth - 90) * (1 - elevation / 90) * radius + */ + 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, + 'system': parseSystem(str), + 'key': satellite + prn + }); + } + + return { + 'msgNumber': parseNumber(gsv[2]), + 'msgsTotal': parseNumber(gsv[1]), + 'satsInView': parseNumber(gsv[3]), + 'satellites': sats, + 'signalId': gsv.length % 4 === 2 ? parseNumber(gsv[gsv.length - 2]) : null,// NMEA 4.10 addition + 'system': parseSystem(str) + }; + }, + // Geographic Position - Latitude/Longitude + 'GLL': function (str, gll) { + + if (gll.length !== 9 && gll.length !== 8) { + throw new Error('Invalid GLL length: ' + str); + } + + /* + ------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 + | | | | | | | | + $--GLL,llll.ll,a,yyyyy.yy,a,hhmmss.ss,a,m,*hh + ------------------------------------------------------------------------------ + + 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]), + 'faa': gll.length === 9 ? parseFAA(gll[7]) : null + }; + }, + // 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 + }; + }, + 'GST': function (str, gst) { + + if (gst.length !== 10) { + throw new Error('Invalid GST length: ' + str); + } + + /* + 1 = Time (UTC) + 2 = RMS value of the pseudorange residuals; includes carrier phase residuals during periods of RTK (float) and RTK (fixed) processing + 3 = Error ellipse semi-major axis 1 sigma error, in meters + 4 = Error ellipse semi-minor axis 1 sigma error, in meters + 5 = Error ellipse orientation, degrees from true north + 6 = Latitude 1 sigma error, in meters + 7 = Longitude 1 sigma error, in meters + 8 = Height 1 sigma error, in meters + 9 = Checksum + */ + + return { + 'time': parseTime(gst[1]), + 'rms': parseNumber(gst[2]), + 'ellipseMajor': parseNumber(gst[3]), + 'ellipseMinor': parseNumber(gst[4]), + 'ellipseOrientation': parseNumber(gst[5]), + 'latitudeError': parseNumber(gst[6]), + 'longitudeError': parseNumber(gst[7]), + 'heightError': parseNumber(gst[8]) + }; + }, + + // add HDT + 'HDT': function (str, hdt) { + + if (hdt.length !== 4) { + throw new Error('Invalid HDT length: ' + str); + } + + /* + ------------------------------------------------------------------------------ + 1 2 3 + | | | + $--HDT,hhh.hhh,T*XX + ------------------------------------------------------------------------------ + + 1. Heading in degrees + 2. T: indicates heading relative to True North + 3. Checksum + */ + + return { + 'heading': parseFloat(hdt[1]), + 'trueNorth': hdt[2] === 'T' + }; + }, + + 'GRS': function (str, grs) { + + if (grs.length !== 18) { + throw new Error('Invalid GRS length: ' + str); + } + + var res = []; + for (var i = 3; i <= 14; i++) { + var tmp = parseNumber(grs[i]); + if (tmp !== null) + res.push(tmp); + } + + return { + 'time': parseTime(grs[1]), + 'mode': parseNumber(grs[2]), + 'res': res + }; + }, + 'GBS': function (str, gbs) { + + if (gbs.length !== 10 && gbs.length !== 12) { + throw new Error('Invalid GBS length: ' + str); + } + + /* + 0 1 2 3 4 5 6 7 8 + | | | | | | | | | + $--GBS,hhmmss.ss,x.x,x.x,x.x,x.x,x.x,x.x,x.x*hh + + 1. UTC time of the GGA or GNS fix associated with this sentence + 2. Expected error in latitude (meters) + 3. Expected error in longitude (meters) + 4. Expected error in altitude (meters) + 5. PRN (id) of most likely failed satellite + 6. Probability of missed detection for most likely failed satellite + 7. Estimate of bias in meters on most likely failed satellite + 8. Standard deviation of bias estimate + -- + 9. systemID (NMEA 4.10) + 10. signalID (NMEA 4.10) + */ + + return { + 'time': parseTime(gbs[1]), + 'errLat': parseNumber(gbs[2]), + 'errLon': parseNumber(gbs[3]), + 'errAlt': parseNumber(gbs[4]), + 'failedSat': parseNumber(gbs[5]), + 'probFailedSat': parseNumber(gbs[6]), + 'biasFailedSat': parseNumber(gbs[7]), + 'stdFailedSat': parseNumber(gbs[8]), + 'systemId': gbs.length === 12 ? parseNumber(gbs[9]) : null, + 'signalId': gbs.length === 12 ? parseNumber(gbs[10]) : null + }; + }, + 'GNS': function (str, gns) { + + if (gns.length !== 14 && gns.length !== 15) { + throw new Error('Invalid GNS length: ' + str); + } + + return { + 'time': parseTime(gns[1]), + 'lat': parseCoord(gns[2], gns[3]), + 'lon': parseCoord(gns[4], gns[5]), + 'mode': gns[6], + 'satsUsed': parseNumber(gns[7]), + 'hdop': parseNumber(gns[8]), + 'alt': parseNumber(gns[9]), + 'sep': parseNumber(gns[10]), + 'diffAge': parseNumber(gns[11]), + 'diffStation': parseNumber(gns[12]), + 'navStatus': gns.length === 15 ? gns[13] : null // NMEA 4.10 + }; + } +}; + +GPS['Parse'] = function (line) { + + if (typeof line !== 'string') + return false; + + var nmea = line.split(','); + + var last = nmea.pop(); + + // HDT is 2 items length + if (nmea.length < 2 || 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 y = sdlon * clat2; + var x = clat1 * slat2 - slat1 * clat2 * cdlon; + + var head = Math.atan2(y, x) * 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['TotalDistance'] = function (path) { + + if (path.length < 2) + return 0; + + var len = 0; + for (var i = 0; i < path.length - 1; i++) { + var c = path[i]; + var n = path[i + 1]; + len += GPS['Distance'](c['lat'], c['lon'], n['lat'], n['lon']); + } + return len; +}; + +GPS.prototype['update'] = function (line) { + + var parsed = GPS['Parse'](line); + + this['state']['processed']++; + + if (parsed === false) { + this['state']['errors']++; + return false; + } + + updateState(this['state'], parsed); + + this['emit']('data', parsed); + this['emit'](parsed.type, 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) === '$') { + try { + this['update'](line); + } catch (err) { + this['partial'] = ""; + throw new Error(err); + } + } + 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; +}; + +GPS.prototype['emit'] = function (ev, data) { + if (this['events'][ev] !== undefined) { + this['events'][ev].call(this, data); + } +}; +export { + GPS as default +}; diff --git a/examples/confluence.js b/examples/confluence.js index 867bf94..cd35cae 100644 --- a/examples/confluence.js +++ b/examples/confluence.js @@ -20,10 +20,10 @@ port.pipe(parser); var Angles = require('angles'); -var GPS = require('../gps.js'); +var GPS = require('gps'); var gps = new GPS; -gps.on('data', function(data) { +gps.on('data', function (data) { var lat1 = gps.state.lat; var lon1 = gps.state.lon; @@ -36,13 +36,13 @@ gps.on('data', function(data) { var head = GPS.Heading(lat1, lon1, lat2, lon2); var rose = Angles.compass(head); - console.log("\033[2J\033[;H" + - "You are at (" + lat1 + ", " + lon1 + "),\n" + - "The closest confluence point (" + lat2 + ", " + lon2 + ") is in " + dist + " km.\n" + - "You have to go " + head + "° " + rose); + console.log("\033[2J\033[;H" + + "You are at (" + lat1 + ", " + lon1 + "),\n" + + "The closest confluence point (" + lat2 + ", " + lon2 + ") is in " + dist + " km.\n" + + "You have to go " + head + "° " + rose); }); -parser.on('data', function(data) { +parser.on('data', function (data) { gps.update(data); }); diff --git a/examples/dashboard/dashboard.html b/examples/dashboard/dashboard.html index 3afc913..a9fc634 100644 --- a/examples/dashboard/dashboard.html +++ b/examples/dashboard/dashboard.html @@ -1,458 +1,462 @@ - - Simple Map - - - - - - - - - - - - - -

Satellites

- -

Information

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Date
Latitude
Longitude
Altitude
Speed
Status
PDOP
VDOP
HDOP
Satellites in Use
Satellites in View
- - - + + + Simple Map + + + + + + + + + + + + + + +

Satellites

+ +

Information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Date
Latitude
Longitude
Altitude
Speed
Status
PDOP
VDOP
HDOP
Satellites in Use
Satellites in View
+ + + + \ No newline at end of file diff --git a/examples/dashboard/server.js b/examples/dashboard/server.js index 6ba5b44..205ddce 100644 --- a/examples/dashboard/server.js +++ b/examples/dashboard/server.js @@ -21,20 +21,20 @@ const port = new SerialPort(file, { port.pipe(parser); -app.get('/', function(req, res) { +app.get('/', function (req, res) { res.sendFile(__dirname + '/dashboard.html'); }); -var GPS = require('../../gps.js'); +var GPS = require('gps'); var gps = new GPS; gps.state.bearing = 0; -var prev = {lat: null, lon: null}; +var prev = { lat: null, lon: null }; -http.listen(3000, function() { +http.listen(3000, function () { console.log('listening on *:3000'); - gps.on('data', function() { + gps.on('data', function () { if (prev.lat !== null && prev.lon !== null) { gps.state.bearing = GPS.Heading(prev.lat, prev.lon, gps.state.lat, gps.state.lon); } @@ -44,7 +44,7 @@ http.listen(3000, function() { ; }); - parser.on('data', function(data) { + parser.on('data', function (data) { gps.update(data); }); }); diff --git a/examples/fileRead.js b/examples/fileRead.js index ff95f88..909274e 100644 --- a/examples/fileRead.js +++ b/examples/fileRead.js @@ -3,19 +3,19 @@ var fs = require('fs'); var rs = fs.createReadStream('gps.dump'); var byline = require('byline'); -var GPS = require('../gps.js'); +var GPS = require('gps'); var gps = new GPS; var stream = byline(rs); // This filters all GGA packages from the dump -gps.on('GGA', function(gga) { +gps.on('GGA', function (gga) { console.log('Lat: ' + gga.lat); console.log('Lon: ' + gga.lon); }); -stream.on('data', function(data) { +stream.on('data', function (data) { gps.update(data.toString()); }); diff --git a/examples/fileWrite.js b/examples/fileWrite.js index e5f23ee..93cbee6 100644 --- a/examples/fileWrite.js +++ b/examples/fileWrite.js @@ -20,13 +20,13 @@ port.pipe(parser); var fs = require('fs'); var ws = fs.createWriteStream('gps.dump'); -var GPS = require('../gps.js'); +var GPS = require('gps'); var gps = new GPS; -gps.on('data', function(data) { +gps.on('data', function (data) { ws.write(data.raw + '\n'); }); -parser.on('data', function(data) { +parser.on('data', function (data) { gps.update(data); }); diff --git a/examples/json-stream.js b/examples/json-stream.js index 732b8e6..269f1a8 100644 --- a/examples/json-stream.js +++ b/examples/json-stream.js @@ -3,18 +3,18 @@ var Transform = require('stream').Transform; -var GPS = require('../gps.js'); +var GPS = require('gps'); process.stdin.resume(); process.stdin.setEncoding('utf8'); function Process() { - Transform.call(this, {objectMode: true}); + Transform.call(this, { objectMode: true }); } Process.prototype = { _line: "", - _transform: function(chunk, encoding, done) { + _transform: function (chunk, encoding, done) { var data = this._line + chunk.toString(); @@ -23,7 +23,7 @@ Process.prototype = { var self = this; - lines.forEach(function(x) { + lines.forEach(function (x) { var tmp = GPS.parse(x); if (tmp !== false) { @@ -33,7 +33,7 @@ Process.prototype = { done(); }, - _flush: function(done) { + _flush: function (done) { if (this._line) { var tmp = GPS.parse(this._line); @@ -53,5 +53,5 @@ for (var key in origProto) { Process.prototype.constructor = Process; process.stdin - .pipe(new Process) - .pipe(process.stdout); + .pipe(new Process) + .pipe(process.stdout); diff --git a/examples/maps/maps.html b/examples/maps/maps.html index 660f01b..76c338e 100644 --- a/examples/maps/maps.html +++ b/examples/maps/maps.html @@ -1,129 +1,135 @@ - - Simple Map - - - - - -
- - - - + + - - - - - + gstate.lat = state.position.pos[0]; + gstate.lng = state.position.pos[1]; + + var path = getErrorEllipse(state.position.pos, state.position.cov); + + ellipse.setPaths(path); + map.setCenter(gstate); + marker.setPosition(gstate); + }); + } + + + + + + + + \ No newline at end of file diff --git a/examples/maps/server.js b/examples/maps/server.js index 47b9cfb..6ec4aad 100644 --- a/examples/maps/server.js +++ b/examples/maps/server.js @@ -25,7 +25,7 @@ const port = new SerialPort(file, { port.pipe(parser); -var GPS = require('../../gps.js'); +var GPS = require('gps'); var gps = new GPS; // Simple Kalman Filter set up @@ -41,7 +41,7 @@ var u = $V([0, 0]); var filter = new Kalman($V([0, 0]), $M([[1, 0], [0, 1]])); -gps.on('data', function(data) { +gps.on('data', function (data) { if (data.lat && data.lon) { @@ -65,15 +65,15 @@ gps.on('data', function(data) { io.emit('position', gps.state); }); -app.get('/', function(req, res) { +app.get('/', function (req, res) { res.sendFile(__dirname + '/maps.html'); }); -http.listen(3000, function() { +http.listen(3000, function () { console.log('listening on *:3000'); }); -parser.on('data', function(data) { +parser.on('data', function (data) { gps.update(data); }); diff --git a/examples/serial.js b/examples/serial.js index 8323847..7d179c6 100644 --- a/examples/serial.js +++ b/examples/serial.js @@ -24,13 +24,13 @@ const port = new SerialPort(file, { port.pipe(parser); -var GPS = require('../gps.js'); +var GPS = require('gps'); var gps = new GPS; -gps.on('data', function(data) { +gps.on('data', function (data) { console.log(data); }); -parser.on('data', function(data) { +parser.on('data', function (data) { gps.update(data); }); diff --git a/examples/set-date.js b/examples/set-date.js index 70ad29f..8cd9654 100644 --- a/examples/set-date.js +++ b/examples/set-date.js @@ -19,15 +19,15 @@ const port = new SerialPort(file, { port.pipe(parser); -var GPS = require('../gps.js'); +var GPS = require('gps'); var gps = new GPS; -gps.on('data', function(data) { +gps.on('data', function (data) { if (!data.time) return; - exec('date -s "' + data.time.toString() + '"', function(error, stdout, stderr) { + exec('date -s "' + data.time.toString() + '"', function (error, stdout, stderr) { if (error) throw error; // Clock should be set now, exit console.log("Set time to " + data.time.toString()); @@ -35,7 +35,7 @@ gps.on('data', function(data) { }); }); -parser.on('data', function(data) { +parser.on('data', function (data) { gps.update(data); }); diff --git a/examples/simple.js b/examples/simple.js index 8c9d66b..7c4bc3d 100644 --- a/examples/simple.js +++ b/examples/simple.js @@ -1,11 +1,11 @@ -var GPS = require('../gps.js'); +var GPS = require('gps'); var gps = new GPS; var sentence = '$GPGGA,224900.000,4832.3762,N,00903.5393,E,1,04,7.8,498.6,M,48.0,M,,0000*5E'; -gps.on('data', function(parsed) { +gps.on('data', function (parsed) { console.log(parsed); }); diff --git a/examples/state.js b/examples/state.js index 991253f..b275b32 100644 --- a/examples/state.js +++ b/examples/state.js @@ -18,13 +18,13 @@ const port = new SerialPort(file, { port.pipe(parser); -var GPS = require('../gps.js'); +var GPS = require('gps'); var gps = new GPS; -gps.on('data', function(data) { +gps.on('data', function (data) { console.log(gps.state); }); -parser.on('data', function(data) { +parser.on('data', function (data) { gps.update(data); }); diff --git a/gps.js b/gps.js deleted file mode 100644 index 90cb3b0..0000000 --- a/gps.js +++ /dev/null @@ -1,965 +0,0 @@ -/** - * @license GPS.js v0.6.1 26/01/2016 - * - * Copyright (c) 2016, Robert Eisele (robert@xarg.org) - * Dual licensed under the MIT or GPL Version 2 licenses. - **/ - - -(function(root) { - - 'use strict'; - - var D2R = Math.PI / 180; - - var collectSats = {}; - var collectActiveSats = {}; - var lastSeenSat = {}; - - function updateState(state, data) { - - // TODO: can we really use RMC time here or is it the time of fix? - if (data['type'] === 'RMC' || data['type'] === 'GGA' || data['type'] === 'GLL' || data['type'] === 'GNS') { - state['time'] = data['time']; - state['lat'] = data['lat']; - state['lon'] = data['lon']; - } - - if (data['type'] === 'HDT') { - state['heading'] = data['heading']; - state['trueNorth'] = data['trueNorth']; - } - - 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') { - - var systemId = data['systemId']; - collectActiveSats[systemId] = data['satellites']; - var satsActive = []; - for (var s in collectActiveSats) { - satsActive.push(...collectActiveSats[s]); - } - - state['satsActive'] = satsActive; - state['fix'] = data['fix']; - state['hdop'] = data['hdop']; - state['pdop'] = data['pdop']; - state['vdop'] = data['vdop']; - } - - if (data['type'] === 'GSV') { - - var now = new Date().getTime(); - - var sats = data['satellites']; - for (var i = 0; i < sats.length; i++) { - var key = sats[i].key; - lastSeenSat[key] = now; - collectSats[key] = sats[i]; - } - - var ret = []; - for (var key in collectSats) { - if (now - lastSeenSat[key] < 3000) // Sats are visible for 3 seconds - ret.push(collectSats[key]) - } - state['satsVisible'] = ret; - } - } - - /** - * - * @param {String} time - * @param {String=} date - * @returns {Date} - */ - 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(Number(year), Number(month), Number(day)); - } else { - // If we need to parse older GPRMC data, we should hack something like - // year < 73 ? 2000+year : 1900+year - // Since GPS appeared in 1973 - ret.setUTCFullYear(Number('20' + year), Number(month), Number(day)); - } - } - - ret.setUTCHours(Number(time.slice(0, 2))); - ret.setUTCMinutes(Number(time.slice(2, 4))); - ret.setUTCSeconds(Number(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(Number(ms)); - - return ret; - } - - function parseCoord(coord, dir) { - - // Latitude can go from 0 to 90; longitude can go from -180 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 parseSystemId(systemId) { - switch (systemId) { - case 0: - return "QZSS"; - case 1: - return "GPS"; - case 2: - return "GLONASS"; - case 3: - return "Galileo"; - case 4: - return "BeiDou" - default: - return "unknown" - } - } - - function parseSystem(str) { - var satellite = str.slice(1, 3); - switch (satellite) { - case "GP": - return "GPS"; - case "GQ": - return "QZSS"; - case "GL": - return "GLONASS"; - case "GA": - return "Galileo"; - case "GB": - return "BeiDou" - default: - return satellite; - } - } - - - function parseGSAMode(mode) { - - switch (mode) { - case 'M': - return 'manual'; - case 'A': - return 'automatic'; - case '': - return null; - } - throw new Error('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 (real time kinematic) RTK fix - case '5': - return 'rtk-float'; // valid (real time kinematic) RTK float - case '6': - return 'estimated'; // dead reckoning - case '7': - return 'manual'; - case '8': - return 'simulated'; - } - throw new Error('INVALID GGA FIX: ' + fix); - } - - function parseGSAFix(fix) { - - switch (fix) { - case '': - case '1': - return null; - case '2': - return '2D'; - case '3': - return '3D'; - } - throw new Error('INVALID GSA FIX: ' + fix); - } - - function parseRMC_GLLStatus(status) { - - switch (status) { - case '': - return null; - case 'A': - return 'active'; - case 'V': - return 'void'; - } - throw new Error('INVALID RMC/GLL STATUS: ' + status); - } - - function parseFAA(faa) { - - // Only A and D will correspond to an Active and reliable Sentence - - switch (faa) { - case '': - return null; - case 'A': - return 'autonomous'; - case 'D': - return 'differential'; - case 'E': - return 'estimated'; // dead reckoning - case 'M': - return 'manual input'; - case 'S': - return 'simulated'; - case 'N': - return 'not valid'; - case 'P': - return 'precise'; - case 'R': - return 'rtk'; // valid (real time kinematic) RTK fix - case 'F': - return 'rtk-float'; // valid (real time kinematic) RTK float - } - throw new Error('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 new Error('Unknown unit: ' + unit); - } - - /** - * - * @constructor - */ - function GPS() { - - if (!(this instanceof GPS)) { - return new GPS; - } - - this['events'] = {}; - this['state'] = { 'errors': 0, 'processed': 0 }; - } - - GPS.prototype['events'] = null; - GPS.prototype['state'] = null; - - GPS['mod'] = { - // Global Positioning System Fix Data - 'GGA': function(str, gga) { - - if (gga.length !== 16 && gga.length !== 14) { - throw new Error('Invalid GGA length: ' + str); - } - - /* - 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 - - 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]), - 'satellites': parseNumber(gga[7]), - 'hdop': parseNumber(gga[8]), // dilution - 'geoidal': parseDist(gga[11], gga[12]), // aboveGeoid - 'age': gga[13] === undefined ? null : parseNumber(gga[13]), // dgps time since update - 'stationID': gga[14] === undefined ? null : parseNumber(gga[14]) // dgpsReference?? - }; - }, - // GPS DOP and active satellites - 'GSA': function(str, gsa) { - - if (gsa.length !== 19 && gsa.length !== 20) { - throw new Error('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) = systemID NMEA 4.10 - 18 = Checksum - */ - - var sats = []; - for (var i = 3; i < 15; 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]), - 'systemId': gsa.length > 19 ? parseNumber(gsa[18]) : null, - 'system': gsa.length > 19 ? parseSystemId(parseNumber(gsa[18])) : 'unknown' - }; - }, - // Recommended Minimum data for gps - 'RMC': function(str, rmc) { - - if (rmc.length !== 13 && rmc.length !== 14 && rmc.length !== 15) { - throw new Error('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) - (13) = NMEA 4.10 introduced nav status - 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]), // heading - 'variation': parseRMCVariation(rmc[10], rmc[11]), - 'faa': rmc.length > 13 ? parseFAA(rmc[12]) : null, - 'navStatus': rmc.length > 14 ? rmc[13] : null - }; - }, - // Track info - 'VTG': function(str, vtg) { - - if (vtg.length !== 10 && vtg.length !== 11) { - throw new Error('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 - ------------------------------------------------------------------------------ - - 1 = Track made good (degrees true) - 2 = Fixed text 'T' indicates that track made good is relative to true north - 3 = optional: Track made good (degrees magnetic) - 4 = optional: M: track made good is relative to magnetic north - 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, - 'trackMagetic': null, - 'speed': null, - 'faa': null - }; - } - - if (vtg[2] !== 'T') { - throw new Error('Invalid VTG track mode: ' + str); - } - - if (vtg[8] !== 'K' || vtg[6] !== 'N') { - throw new Error('Invalid VTG speed tag: ' + str); - } - - return { - 'track': parseNumber(vtg[1]), // heading - 'trackMagnetic': vtg[3] === '' ? null : parseNumber(vtg[3]), // heading uncorrected to magnetic north - 'speed': parseKnots(vtg[5]), - 'faa': vtg.length === 11 ? parseFAA(vtg[9]) : null - }; - }, - // satellites in view - 'GSV': function(str, gsv) { - - if (gsv.length % 4 === 0) { - // = 1 -> normal package - // = 2 -> NMEA 4.10 extension - // = 3 -> BeiDou extension? - throw new Error('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 - repeat [ - 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) - ] - N+1 = signalID NMEA 4.10 - N+2 = Checksum - */ - - var sats = []; - var satellite = str.slice(1, 3); - - for (var i = 4; i < gsv.length - 3; i += 4) { - - var prn = parseNumber(gsv[i]); - var snr = parseNumber(gsv[i + 3]); - /* - Plot satellites in Radar chart with north on top - by linear map elevation from 0° to 90° into r to 0 - - centerX + cos(azimuth - 90) * (1 - elevation / 90) * radius - centerY + sin(azimuth - 90) * (1 - elevation / 90) * radius - */ - 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, - 'system': parseSystem(str), - 'key': satellite + prn - }); - } - - return { - 'msgNumber': parseNumber(gsv[2]), - 'msgsTotal': parseNumber(gsv[1]), - 'satsInView': parseNumber(gsv[3]), - 'satellites': sats, - 'signalId': gsv.length % 4 === 2 ? parseNumber(gsv[gsv.length - 2]) : null,// NMEA 4.10 addition - 'system': parseSystem(str) - }; - }, - // Geographic Position - Latitude/Longitude - 'GLL': function(str, gll) { - - if (gll.length !== 9 && gll.length !== 8) { - throw new Error('Invalid GLL length: ' + str); - } - - /* - ------------------------------------------------------------------------------ - 1 2 3 4 5 6 7 8 - | | | | | | | | - $--GLL,llll.ll,a,yyyyy.yy,a,hhmmss.ss,a,m,*hh - ------------------------------------------------------------------------------ - - 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]), - 'faa': gll.length === 9 ? parseFAA(gll[7]) : null - }; - }, - // 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 - }; - }, - 'GST': function(str, gst) { - - if (gst.length !== 10) { - throw new Error('Invalid GST length: ' + str); - } - - /* - 1 = Time (UTC) - 2 = RMS value of the pseudorange residuals; includes carrier phase residuals during periods of RTK (float) and RTK (fixed) processing - 3 = Error ellipse semi-major axis 1 sigma error, in meters - 4 = Error ellipse semi-minor axis 1 sigma error, in meters - 5 = Error ellipse orientation, degrees from true north - 6 = Latitude 1 sigma error, in meters - 7 = Longitude 1 sigma error, in meters - 8 = Height 1 sigma error, in meters - 9 = Checksum - */ - - return { - 'time': parseTime(gst[1]), - 'rms': parseNumber(gst[2]), - 'ellipseMajor': parseNumber(gst[3]), - 'ellipseMinor': parseNumber(gst[4]), - 'ellipseOrientation': parseNumber(gst[5]), - 'latitudeError': parseNumber(gst[6]), - 'longitudeError': parseNumber(gst[7]), - 'heightError': parseNumber(gst[8]) - }; - }, - - // add HDT - 'HDT': function(str, hdt) { - - if (hdt.length !== 4) { - throw new Error('Invalid HDT length: ' + str); - } - - /* - ------------------------------------------------------------------------------ - 1 2 3 - | | | - $--HDT,hhh.hhh,T*XX - ------------------------------------------------------------------------------ - - 1. Heading in degrees - 2. T: indicates heading relative to True North - 3. Checksum - */ - - return { - 'heading': parseFloat(hdt[1]), - 'trueNorth': hdt[2] === 'T' - }; - }, - - 'GRS': function(str, grs) { - - if (grs.length !== 18) { - throw new Error('Invalid GRS length: ' + str); - } - - var res = []; - for (var i = 3; i <= 14; i++) { - var tmp = parseNumber(grs[i]); - if (tmp !== null) - res.push(tmp); - } - - return { - 'time': parseTime(grs[1]), - 'mode': parseNumber(grs[2]), - 'res': res - }; - }, - 'GBS': function(str, gbs) { - - if (gbs.length !== 10 && gbs.length !== 12) { - throw new Error('Invalid GBS length: ' + str); - } - - /* - 0 1 2 3 4 5 6 7 8 - | | | | | | | | | - $--GBS,hhmmss.ss,x.x,x.x,x.x,x.x,x.x,x.x,x.x*hh - - 1. UTC time of the GGA or GNS fix associated with this sentence - 2. Expected error in latitude (meters) - 3. Expected error in longitude (meters) - 4. Expected error in altitude (meters) - 5. PRN (id) of most likely failed satellite - 6. Probability of missed detection for most likely failed satellite - 7. Estimate of bias in meters on most likely failed satellite - 8. Standard deviation of bias estimate - -- - 9. systemID (NMEA 4.10) - 10. signalID (NMEA 4.10) - */ - - return { - 'time': parseTime(gbs[1]), - 'errLat': parseNumber(gbs[2]), - 'errLon': parseNumber(gbs[3]), - 'errAlt': parseNumber(gbs[4]), - 'failedSat': parseNumber(gbs[5]), - 'probFailedSat': parseNumber(gbs[6]), - 'biasFailedSat': parseNumber(gbs[7]), - 'stdFailedSat': parseNumber(gbs[8]), - 'systemId': gbs.length === 12 ? parseNumber(gbs[9]) : null, - 'signalId': gbs.length === 12 ? parseNumber(gbs[10]) : null - }; - }, - 'GNS': function(str, gns) { - - if (gns.length !== 14 && gns.length !== 15) { - throw new Error('Invalid GNS length: ' + str); - } - - return { - 'time': parseTime(gns[1]), - 'lat': parseCoord(gns[2], gns[3]), - 'lon': parseCoord(gns[4], gns[5]), - 'mode': gns[6], - 'satsUsed': parseNumber(gns[7]), - 'hdop': parseNumber(gns[8]), - 'alt': parseNumber(gns[9]), - 'sep': parseNumber(gns[10]), - 'diffAge': parseNumber(gns[11]), - 'diffStation': parseNumber(gns[12]), - 'navStatus': gns.length === 15 ? gns[13] : null // NMEA 4.10 - }; - } - }; - - GPS['Parse'] = function(line) { - - if (typeof line !== 'string') - return false; - - var nmea = line.split(','); - - var last = nmea.pop(); - - // HDT is 2 items length - if (nmea.length < 2 || 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 y = sdlon * clat2; - var x = clat1 * slat2 - slat1 * clat2 * cdlon; - - var head = Math.atan2(y, x) * 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['TotalDistance'] = function(path) { - - if (path.length < 2) - return 0; - - var len = 0; - for (var i = 0; i < path.length - 1; i++) { - var c = path[i]; - var n = path[i + 1]; - len += GPS['Distance'](c['lat'], c['lon'], n['lat'], n['lon']); - } - return len; - }; - - GPS.prototype['update'] = function(line) { - - var parsed = GPS['Parse'](line); - - this['state']['processed']++; - - if (parsed === false) { - this['state']['errors']++; - return false; - } - - updateState(this['state'], parsed); - - this['emit']('data', parsed); - this['emit'](parsed.type, 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) === '$') { - try { - this['update'](line); - } catch (err) { - this['partial'] = ""; - throw new Error(err); - } - } - 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; - }; - - GPS.prototype['emit'] = function(ev, data) { - if (this['events'][ev] !== undefined) { - this['events'][ev].call(this, data); - } - }; - - if (typeof exports === 'object') { - Object.defineProperty(GPS, "__esModule", { 'value': true }); - GPS['default'] = GPS; - GPS['GPS'] = GPS; - module['exports'] = GPS; - } else { - root['GPS'] = GPS; - } - -})(this); diff --git a/gps.min.js b/gps.min.js deleted file mode 100644 index 47cc124..0000000 --- a/gps.min.js +++ /dev/null @@ -1,21 +0,0 @@ -/* -GPS.js v0.6.1 26/01/2016 - -Copyright (c) 2016, Robert Eisele (robert@xarg.org) -Dual licensed under the MIT or GPL Version 2 licenses. -*/ -(function(x){function k(b,a){if(""===b)return null;var d=new Date;if(a){var c=a.slice(4),f=a.slice(2,4)-1,h=a.slice(0,2);4===c.length?d.setUTCFullYear(Number(c),Number(f),Number(h)):d.setUTCFullYear(Number("20"+c),Number(f),Number(h))}d.setUTCHours(Number(b.slice(0,2)));d.setUTCMinutes(Number(b.slice(2,4)));d.setUTCSeconds(Number(b.slice(4,6)));c=b.slice(7);f=c.length;h=0;0!==f&&(h=parseFloat(c)*Math.pow(10,3-f));d.setUTCMilliseconds(Number(h));return d}function l(b,a){if(""===b)return null;var d= -1;switch(a){case "S":d=-1;case "N":var c=2;break;case "W":d=-1;case "E":c=3}return d*(parseFloat(b.slice(0,c))+parseFloat(b.slice(c))/60)}function e(b){return""===b?null:parseFloat(b)}function q(b){return""===b?null:1.852*parseFloat(b)}function y(b){if(""===b)return null;switch(parseInt(b,10)){case 0:return null;case 1:return"fix";case 2:return"dgps-fix";case 3:return"pps-fix";case 4:return"rtk";case 5:return"rtk-float";case 6:return"estimated";case 7:return"manual";case 8:return"simulated"}throw Error("INVALID GGA FIX: "+ -b);}function r(b){switch(b){case "A":return"active";case "V":return"void";case "":return null}throw Error("INVALID RMC/GLL STATUS: "+b);}function n(b){switch(b){case "":return null;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";case "R":return"rtk";case "F":return"rtk-float"}throw Error("INVALID FAA MODE: "+b);}function t(b,a){if("M"===a||""===a)return e(b); -throw Error("Unknown unit: "+a);}function g(){if(!(this instanceof g))return new g;this.events={};this.state={errors:0,processed:0}}var m=Math.PI/180,p={},u={};g.prototype.events=null;g.prototype.state=null;g.mod={GGA:function(b,a){if(16!==a.length&&14!==a.length)throw Error("Invalid GGA length: "+b);return{time:k(a[1]),lat:l(a[2],a[3]),lon:l(a[4],a[5]),alt:t(a[9],a[10]),quality:y(a[6]),satellites:e(a[7]),hdop:e(a[8]),geoidal:t(a[11],a[12]),age:void 0===a[13]?null:e(a[13]),stationID:void 0===a[14]? -null:e(a[14])}},GSA:function(b,a){if(19!==a.length&&20!==a.length)throw Error("Invalid GSA length: "+b);for(var d=[],c=3;15>c;c++)""!==a[c]&&d.push(parseInt(a[c],10));a:{c=a[1];switch(c){case "M":c="manual";break a;case "A":c="automatic";break a;case "":c=null;break a}throw Error("INVALID GSA MODE: "+c);}a:{var f=a[2];switch(f){case "1":case "":f=null;break a;case "2":f="2D";break a;case "3":f="3D";break a}throw Error("INVALID GSA FIX: "+f);}return{mode:c,fix:f,satellites:d,pdop:e(a[15]),hdop:e(a[16]), -vdop:e(a[17]),systemId:19=c;c++){var f=e(a[c]);null!==f&&d.push(f)}return{time:k(a[1]),mode:e(a[2]),res:d}},GBS:function(b,a){if(10!==a.length&& -12!==a.length)throw Error("Invalid GBS length: "+b);return{time:k(a[1]),errLat:e(a[2]),errLon:e(a[3]),errAlt:e(a[4]),failedSat:e(a[5]),probFailedSat:e(a[6]),biasFailedSat:e(a[7]),stdFailedSat:e(a[8]),systemId:12===a.length?e(a[9]):null,signalId:12===a.length?e(a[10]):null}},GNS:function(b,a){if(14!==a.length&&15!==a.length)throw Error("Invalid GNS length: "+b);return{time:k(a[1]),lat:l(a[2],a[3]),lon:l(a[4],a[5]),mode:a[6],satsUsed:e(a[7]),hdop:e(a[8]),alt:e(a[9]),sep:e(a[10]),diffAge:e(a[11]),diffStation:e(a[12]), -navStatus:15===a.length?a[13]:null}}};g.Parse=function(b){if("string"!==typeof b)return!1;var a=b.split(","),d=a.pop();if(2>a.length||"$"!==b.charAt(0)||-1===d.indexOf("*"))return!1;d=d.split("*");a.push(d[0]);a.push(d[1]);a[0]=a[0].slice(3);if(void 0!==g.mod[a[0]]){d=this.mod[a[0]](b,a);d.raw=b;for(var c=0,f=1;fb.length)return 0;for(var a=0,d=0;dd-u[h]&& -c.push(p[h]);a.satsVisible=c}this.emit("data",b);this.emit(b.type,b);return!0};g.prototype.partial="";g.prototype.updatePartial=function(b){for(this.partial+=b;;){b=this.partial.indexOf("\r\n");if(-1===b)break;var a=this.partial.slice(0,b);if("$"===a.charAt(0))try{this.update(a)}catch(d){throw this.partial="",Error(d);}this.partial=this.partial.slice(b+2)}};g.prototype.on=function(b,a){return void 0===this.events[b]?(this.events[b]=a,this):null};g.prototype.off=function(b){void 0!==this.events[b]&& -(this.events[b]=void 0);return this};g.prototype.emit=function(b,a){void 0!==this.events[b]&&this.events[b].call(this,a)};"object"===typeof exports?(Object.defineProperty(g,"__esModule",{value:!0}),g["default"]=g,g.GPS=g,module.exports=g):x.GPS=g})(this); \ No newline at end of file diff --git a/package.json b/package.json index ec33a63..207d7ba 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "gps", - "title": "gps.js", - "version": "0.6.1", - "homepage": "https://www.xarg.org/2016/07/using-gps-with-node-js-and-javascript/", - "bugs": "https://github.com/infusion/GPS.js/issues", + "title": "GPS.js", + "version": "0.7.0", + "homepage": "https://raw.org/article/using-gps-with-node-js-and-javascript/", + "bugs": "https://github.com/rawify/GPS.js/issues", "description": "A GPS NMEA parser library", "keywords": [ "nmea", @@ -23,35 +23,54 @@ "gva", "hdt" ], - "author": "Robert Eisele (http://www.xarg.org/)", - "main": "gps.js", - "types": "gps.d.ts", "private": false, + "main": "./dist/gps.js", + "module": "./dist/gps.mjs", + "types": "./gps.d.ts", + "browser": "./dist/gps.min.js", + "unpkg": "./dist/gps.min.js", "readmeFilename": "README.md", - "directories": { - "example": "examples", - "test": "tests" + "exports": { + ".": { + "types": "./gps.d.ts", + "require": "./dist/gps.js", + "import": "./dist/gps.mjs" + } }, - "license": "MIT OR GPL-2.0", "repository": { "type": "git", - "url": "git://github.com/infusion/GPS.js.git" + "url": "git@github.com:rawify/GPS.js.git" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + }, + "author": { + "name": "Robert Eisele", + "email": "robert@raw.org", + "url": "https://raw.org/" }, + "license": "MIT", "engines": { "node": "*" }, + "directories": { + "example": "examples", + "test": "tests" + }, "scripts": { + "build": "crude-build GPS", "test": "mocha tests/*.js" }, "devDependencies": { "angles": "^0.2.4", "byline": "^5.0.0", - "chai": "^4.3.4", - "express": "^4.17.2", - "mocha": "^9.1.4", - "serialport": "^9.2.8", - "socket.io": "^4.4.1", + "crude-build": "^0.0.1", + "express": "^4.21.0", "kalman": "0.0.2", + "mocha": "^10.7.3", + "serialport": "^12.0.0", + "socket.io": "^4.8.0", "sylvester": "0.0.21" } } diff --git a/src/gps.js b/src/gps.js new file mode 100644 index 0000000..e30974d --- /dev/null +++ b/src/gps.js @@ -0,0 +1,951 @@ +/** + * @license GPS.js v0.7.0 10/4/2024 + * https://raw.org/article/using-gps-with-node-js-and-javascript/ + * + * Copyright (c) 2024, Robert Eisele (https://raw.org/) + * Licensed under the MIT license. + **/ + +var D2R = Math.PI / 180; + +var collectSats = {}; +var collectActiveSats = {}; +var lastSeenSat = {}; + +function updateState(state, data) { + + // TODO: can we really use RMC time here or is it the time of fix? + if (data['type'] === 'RMC' || data['type'] === 'GGA' || data['type'] === 'GLL' || data['type'] === 'GNS') { + state['time'] = data['time']; + state['lat'] = data['lat']; + state['lon'] = data['lon']; + } + + if (data['type'] === 'HDT') { + state['heading'] = data['heading']; + state['trueNorth'] = data['trueNorth']; + } + + 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') { + + var systemId = data['systemId']; + collectActiveSats[systemId] = data['satellites']; + var satsActive = []; + for (var s in collectActiveSats) { + satsActive.push(...collectActiveSats[s]); + } + + state['satsActive'] = satsActive; + state['fix'] = data['fix']; + state['hdop'] = data['hdop']; + state['pdop'] = data['pdop']; + state['vdop'] = data['vdop']; + } + + if (data['type'] === 'GSV') { + + var now = new Date().getTime(); + + var sats = data['satellites']; + for (var i = 0; i < sats.length; i++) { + var key = sats[i].key; + lastSeenSat[key] = now; + collectSats[key] = sats[i]; + } + + var ret = []; + for (var key in collectSats) { + if (now - lastSeenSat[key] < 3000) // Sats are visible for 3 seconds + ret.push(collectSats[key]) + } + state['satsVisible'] = ret; + } +} + +/** + * + * @param {String} time + * @param {String=} date + * @returns {Date} + */ +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(Number(year), Number(month), Number(day)); + } else { + // If we need to parse older GPRMC data, we should hack something like + // year < 73 ? 2000+year : 1900+year + // Since GPS appeared in 1973 + ret.setUTCFullYear(Number('20' + year), Number(month), Number(day)); + } + } + + ret.setUTCHours(Number(time.slice(0, 2))); + ret.setUTCMinutes(Number(time.slice(2, 4))); + ret.setUTCSeconds(Number(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(Number(ms)); + + return ret; +} + +function parseCoord(coord, dir) { + + // Latitude can go from 0 to 90; longitude can go from -180 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 parseSystemId(systemId) { + switch (systemId) { + case 0: + return "QZSS"; + case 1: + return "GPS"; + case 2: + return "GLONASS"; + case 3: + return "Galileo"; + case 4: + return "BeiDou" + default: + return "unknown" + } +} + +function parseSystem(str) { + var satellite = str.slice(1, 3); + switch (satellite) { + case "GP": + return "GPS"; + case "GQ": + return "QZSS"; + case "GL": + return "GLONASS"; + case "GA": + return "Galileo"; + case "GB": + return "BeiDou" + default: + return satellite; + } +} + + +function parseGSAMode(mode) { + + switch (mode) { + case 'M': + return 'manual'; + case 'A': + return 'automatic'; + case '': + return null; + } + throw new Error('INVALID GSA MODE: ' + mode); +} + +function parseGGAFix(fix) { + + if (fix === '') return null; + + switch (parseInt(fix, 10)) { + 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 (real time kinematic) RTK fix + case 5: + return 'rtk-float'; // valid (real time kinematic) RTK float + case 6: + return 'estimated'; // dead reckoning + case 7: + return 'manual'; + case 8: + return 'simulated'; + } + throw new Error('INVALID GGA FIX: ' + fix); +} + +function parseGSAFix(fix) { + + if (fix === '') return null; + + switch (parseInt(fix, 10)) { + case 1: + return null; + case 2: + return '2D'; + case 3: + return '3D'; + } + throw new Error('INVALID GSA FIX: ' + fix); +} + +function parseRMC_GLLStatus(status) { + + switch (status) { + case '': + return null; + case 'A': + return 'active'; + case 'V': + return 'void'; + } + throw new Error('INVALID RMC/GLL STATUS: ' + status); +} + +function parseFAA(faa) { + + // Only A and D will correspond to an Active and reliable Sentence + + switch (faa) { + case '': + return null; + case 'A': + return 'autonomous'; + case 'D': + return 'differential'; + case 'E': + return 'estimated'; // dead reckoning + case 'M': + return 'manual input'; + case 'S': + return 'simulated'; + case 'N': + return 'not valid'; + case 'P': + return 'precise'; + case 'R': + return 'rtk'; // valid (real time kinematic) RTK fix + case 'F': + return 'rtk-float'; // valid (real time kinematic) RTK float + } + throw new Error('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 new Error('Unknown unit: ' + unit); +} + +/** + * + * @constructor + */ +function GPS() { + + if (!(this instanceof GPS)) { + return new GPS; + } + + this['events'] = {}; + this['state'] = { 'errors': 0, 'processed': 0 }; +} + +GPS.prototype['events'] = null; +GPS.prototype['state'] = null; + +GPS['mod'] = { + // Global Positioning System Fix Data + 'GGA': function (str, gga) { + + if (gga.length !== 16 && gga.length !== 14) { + throw new Error('Invalid GGA length: ' + str); + } + + /* + 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 + + 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]), + 'satellites': parseNumber(gga[7]), + 'hdop': parseNumber(gga[8]), // dilution + 'geoidal': parseDist(gga[11], gga[12]), // aboveGeoid + 'age': gga[13] === undefined ? null : parseNumber(gga[13]), // dgps time since update + 'stationID': gga[14] === undefined ? null : parseNumber(gga[14]) // dgpsReference?? + }; + }, + // GPS DOP and active satellites + 'GSA': function (str, gsa) { + + if (gsa.length !== 19 && gsa.length !== 20) { + throw new Error('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) = systemID NMEA 4.10 + 18 = Checksum + */ + + var sats = []; + for (var i = 3; i < 15; 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]), + 'systemId': gsa.length > 19 ? parseNumber(gsa[18]) : null, + 'system': gsa.length > 19 ? parseSystemId(parseNumber(gsa[18])) : 'unknown' + }; + }, + // Recommended Minimum data for gps + 'RMC': function (str, rmc) { + + if (rmc.length !== 13 && rmc.length !== 14 && rmc.length !== 15) { + throw new Error('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) + (13) = NMEA 4.10 introduced nav status + 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]), // heading + 'variation': parseRMCVariation(rmc[10], rmc[11]), + 'faa': rmc.length > 13 ? parseFAA(rmc[12]) : null, + 'navStatus': rmc.length > 14 ? rmc[13] : null + }; + }, + // Track info + 'VTG': function (str, vtg) { + + if (vtg.length !== 10 && vtg.length !== 11) { + throw new Error('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 + ------------------------------------------------------------------------------ + + 1 = Track made good (degrees true) + 2 = Fixed text 'T' indicates that track made good is relative to true north + 3 = optional: Track made good (degrees magnetic) + 4 = optional: M: track made good is relative to magnetic north + 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, + 'trackMagetic': null, + 'speed': null, + 'faa': null + }; + } + + if (vtg[2] !== 'T') { + throw new Error('Invalid VTG track mode: ' + str); + } + + if (vtg[8] !== 'K' || vtg[6] !== 'N') { + throw new Error('Invalid VTG speed tag: ' + str); + } + + return { + 'track': parseNumber(vtg[1]), // heading + 'trackMagnetic': vtg[3] === '' ? null : parseNumber(vtg[3]), // heading uncorrected to magnetic north + 'speed': parseKnots(vtg[5]), + 'faa': vtg.length === 11 ? parseFAA(vtg[9]) : null + }; + }, + // satellites in view + 'GSV': function (str, gsv) { + + if (gsv.length % 4 === 0) { + // = 1 -> normal package + // = 2 -> NMEA 4.10 extension + // = 3 -> BeiDou extension? + throw new Error('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 + repeat [ + 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) + ] + N+1 = signalID NMEA 4.10 + N+2 = Checksum + */ + + var sats = []; + var satellite = str.slice(1, 3); + + for (var i = 4; i < gsv.length - 3; i += 4) { + + var prn = parseNumber(gsv[i]); + var snr = parseNumber(gsv[i + 3]); + /* + Plot satellites in Radar chart with north on top + by linear map elevation from 0° to 90° into r to 0 + + centerX + cos(azimuth - 90) * (1 - elevation / 90) * radius + centerY + sin(azimuth - 90) * (1 - elevation / 90) * radius + */ + 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, + 'system': parseSystem(str), + 'key': satellite + prn + }); + } + + return { + 'msgNumber': parseNumber(gsv[2]), + 'msgsTotal': parseNumber(gsv[1]), + 'satsInView': parseNumber(gsv[3]), + 'satellites': sats, + 'signalId': gsv.length % 4 === 2 ? parseNumber(gsv[gsv.length - 2]) : null,// NMEA 4.10 addition + 'system': parseSystem(str) + }; + }, + // Geographic Position - Latitude/Longitude + 'GLL': function (str, gll) { + + if (gll.length !== 9 && gll.length !== 8) { + throw new Error('Invalid GLL length: ' + str); + } + + /* + ------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 + | | | | | | | | + $--GLL,llll.ll,a,yyyyy.yy,a,hhmmss.ss,a,m,*hh + ------------------------------------------------------------------------------ + + 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]), + 'faa': gll.length === 9 ? parseFAA(gll[7]) : null + }; + }, + // 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 + }; + }, + 'GST': function (str, gst) { + + if (gst.length !== 10) { + throw new Error('Invalid GST length: ' + str); + } + + /* + 1 = Time (UTC) + 2 = RMS value of the pseudorange residuals; includes carrier phase residuals during periods of RTK (float) and RTK (fixed) processing + 3 = Error ellipse semi-major axis 1 sigma error, in meters + 4 = Error ellipse semi-minor axis 1 sigma error, in meters + 5 = Error ellipse orientation, degrees from true north + 6 = Latitude 1 sigma error, in meters + 7 = Longitude 1 sigma error, in meters + 8 = Height 1 sigma error, in meters + 9 = Checksum + */ + + return { + 'time': parseTime(gst[1]), + 'rms': parseNumber(gst[2]), + 'ellipseMajor': parseNumber(gst[3]), + 'ellipseMinor': parseNumber(gst[4]), + 'ellipseOrientation': parseNumber(gst[5]), + 'latitudeError': parseNumber(gst[6]), + 'longitudeError': parseNumber(gst[7]), + 'heightError': parseNumber(gst[8]) + }; + }, + + // add HDT + 'HDT': function (str, hdt) { + + if (hdt.length !== 4) { + throw new Error('Invalid HDT length: ' + str); + } + + /* + ------------------------------------------------------------------------------ + 1 2 3 + | | | + $--HDT,hhh.hhh,T*XX + ------------------------------------------------------------------------------ + + 1. Heading in degrees + 2. T: indicates heading relative to True North + 3. Checksum + */ + + return { + 'heading': parseFloat(hdt[1]), + 'trueNorth': hdt[2] === 'T' + }; + }, + + 'GRS': function (str, grs) { + + if (grs.length !== 18) { + throw new Error('Invalid GRS length: ' + str); + } + + var res = []; + for (var i = 3; i <= 14; i++) { + var tmp = parseNumber(grs[i]); + if (tmp !== null) + res.push(tmp); + } + + return { + 'time': parseTime(grs[1]), + 'mode': parseNumber(grs[2]), + 'res': res + }; + }, + 'GBS': function (str, gbs) { + + if (gbs.length !== 10 && gbs.length !== 12) { + throw new Error('Invalid GBS length: ' + str); + } + + /* + 0 1 2 3 4 5 6 7 8 + | | | | | | | | | + $--GBS,hhmmss.ss,x.x,x.x,x.x,x.x,x.x,x.x,x.x*hh + + 1. UTC time of the GGA or GNS fix associated with this sentence + 2. Expected error in latitude (meters) + 3. Expected error in longitude (meters) + 4. Expected error in altitude (meters) + 5. PRN (id) of most likely failed satellite + 6. Probability of missed detection for most likely failed satellite + 7. Estimate of bias in meters on most likely failed satellite + 8. Standard deviation of bias estimate + -- + 9. systemID (NMEA 4.10) + 10. signalID (NMEA 4.10) + */ + + return { + 'time': parseTime(gbs[1]), + 'errLat': parseNumber(gbs[2]), + 'errLon': parseNumber(gbs[3]), + 'errAlt': parseNumber(gbs[4]), + 'failedSat': parseNumber(gbs[5]), + 'probFailedSat': parseNumber(gbs[6]), + 'biasFailedSat': parseNumber(gbs[7]), + 'stdFailedSat': parseNumber(gbs[8]), + 'systemId': gbs.length === 12 ? parseNumber(gbs[9]) : null, + 'signalId': gbs.length === 12 ? parseNumber(gbs[10]) : null + }; + }, + 'GNS': function (str, gns) { + + if (gns.length !== 14 && gns.length !== 15) { + throw new Error('Invalid GNS length: ' + str); + } + + return { + 'time': parseTime(gns[1]), + 'lat': parseCoord(gns[2], gns[3]), + 'lon': parseCoord(gns[4], gns[5]), + 'mode': gns[6], + 'satsUsed': parseNumber(gns[7]), + 'hdop': parseNumber(gns[8]), + 'alt': parseNumber(gns[9]), + 'sep': parseNumber(gns[10]), + 'diffAge': parseNumber(gns[11]), + 'diffStation': parseNumber(gns[12]), + 'navStatus': gns.length === 15 ? gns[13] : null // NMEA 4.10 + }; + } +}; + +GPS['Parse'] = function (line) { + + if (typeof line !== 'string') + return false; + + var nmea = line.split(','); + + var last = nmea.pop(); + + // HDT is 2 items length + if (nmea.length < 2 || 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 y = sdlon * clat2; + var x = clat1 * slat2 - slat1 * clat2 * cdlon; + + var head = Math.atan2(y, x) * 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['TotalDistance'] = function (path) { + + if (path.length < 2) + return 0; + + var len = 0; + for (var i = 0; i < path.length - 1; i++) { + var c = path[i]; + var n = path[i + 1]; + len += GPS['Distance'](c['lat'], c['lon'], n['lat'], n['lon']); + } + return len; +}; + +GPS.prototype['update'] = function (line) { + + var parsed = GPS['Parse'](line); + + this['state']['processed']++; + + if (parsed === false) { + this['state']['errors']++; + return false; + } + + updateState(this['state'], parsed); + + this['emit']('data', parsed); + this['emit'](parsed.type, 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) === '$') { + try { + this['update'](line); + } catch (err) { + this['partial'] = ""; + throw new Error(err); + } + } + 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; +}; + +GPS.prototype['emit'] = function (ev, data) { + if (this['events'][ev] !== undefined) { + this['events'][ev].call(this, data); + } +}; diff --git a/tests/functions.js b/tests/functions.js index 7b895b3..bd25e64 100644 --- a/tests/functions.js +++ b/tests/functions.js @@ -1,23 +1,21 @@ -var expect = require('chai').expect; -var GPS = require('../gps.js'); +const GPS = require('gps'); +const assert = require('assert'); -describe('GPS functions', function() { +describe('GPS functions', function () { - it('should measure distance', function() { + it('should measure distance', function () { var result = GPS.Distance(45.527517, -122.718766, 45.373373, -121.693604); - expect(result).to.deep.equal(81.80760861833895); - + assert.deepEqual(result, 81.80760861833895) }); - it('should measure heading', function() { + it('should measure heading', function () { var result = GPS.Heading(45.527517, -122.718766, 45.373373, -121.693604); - expect(result).to.deep.equal(101.73177498132071); - + assert.deepEqual(result, 101.73177498132071) }); }); \ No newline at end of file diff --git a/tests/parser.js b/tests/parser.js index 467e05e..4d01fc7 100644 --- a/tests/parser.js +++ b/tests/parser.js @@ -3,13 +3,13 @@ function _(x) { return x < 10 ? "0" + x : x; } -var today = new Date(); +let today = new Date(); today = today.getUTCFullYear() + '-' + _(today.getUTCMonth() + 1) + '-' + _(today.getUTCDate()); -var expect = require('chai').expect; -var GPS = require('../gps.js'); -var gps = new GPS; -var tests = { +const GPS = require('gps'); +const assert = require('assert'); +const gps = new GPS; +const tests = { 'foo': 'invalid', '$GPGSA,A,3,29,26,31,21,,,,,,,,,2.0,1.7,1.0*39': { 'fix': '3D', @@ -24,6 +24,7 @@ var tests = { 21 ], 'type': 'GSA', + "system": "unknown", "systemId": null, 'valid': true, 'vdop': 1 @@ -102,31 +103,40 @@ var tests = { 'msgsTotal': 3, "satsInView": 12, 'signalId': null, + "system": "GPS", 'satellites': [ { 'azimuth': 148, 'elevation': 17, + "key": "GP16", 'prn': 16, 'snr': 46, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 307, 'elevation': 61, + "key": "GP20", 'prn': 20, 'snr': 51, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 283, 'elevation': 36, + "key": "GP23", 'prn': 23, 'snr': 47, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 34, 'elevation': 6, + "key": "GP25", 'prn': 25, 'snr': 0, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" } ], 'type': 'GSV', @@ -159,6 +169,7 @@ var tests = { 'mode': 'automatic', 'pdop': 1.72, "systemId": null, + "system": "unknown", 'raw': '$GPGSA,A,3,10,07,05,02,29,04,08,13,,,,,1.72,1.03,1.38*0A', 'satellites': [ 10, @@ -179,32 +190,41 @@ var tests = { 'msgsTotal': 3, "satsInView": 11, 'signalId': null, + "system": "GPS", 'raw': '$GPGSV,3,1,11,10,63,137,17,07,61,098,15,05,59,290,20,08,54,157,30*70', 'satellites': [ { 'azimuth': 137, 'elevation': 63, + "key": "GP10", 'prn': 10, 'snr': 17, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 98, 'elevation': 61, + "key": "GP7", 'prn': 7, 'snr': 15, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 290, 'elevation': 59, + "key": "GP5", 'prn': 5, 'snr': 20, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 157, 'elevation': 54, + "key": "GP8", 'prn': 8, 'snr': 30, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" } ], 'type': 'GSV', @@ -253,6 +273,7 @@ var tests = { 25, 29 ], + "system": "unknown", 'type': 'GSA', 'valid': true, 'vdop': 5.6 @@ -267,29 +288,38 @@ var tests = { { 'azimuth': 111, 'elevation': 3, + "key": "GP3", 'prn': 3, 'snr': 0, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 270, 'elevation': 15, + "key": "GP4", 'prn': 4, 'snr': 0, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 10, 'elevation': 1, + "key": "GP6", 'prn': 6, 'snr': 0, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 292, 'elevation': 6, + "key": "GP13", 'prn': 13, 'snr': 0, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" } ], + "system": "GPS", 'type': 'GSV', 'valid': true }, @@ -303,30 +333,39 @@ var tests = { { 'azimuth': 170, 'elevation': 25, + "key": "GP14", 'prn': 14, 'snr': 0, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 208, 'elevation': 57, + "key": "GP16", 'prn': 16, 'snr': 39, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 296, 'elevation': 67, + "key": "GP18", 'prn': 18, 'snr': 40, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 246, 'elevation': 40, + "key": "GP19", 'prn': 19, 'snr': 0, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" } ], 'type': 'GSV', + "system": "GPS", 'valid': false }, '$GPGSV,3,2,11,02,39,223,16,13,28,070,17,26,23,252,,04,14,186,15*77': { @@ -339,30 +378,39 @@ var tests = { { 'azimuth': 223, 'elevation': 39, + "key": "GP2", 'prn': 2, 'snr': 16, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 70, 'elevation': 28, + "key": "GP13", 'prn': 13, 'snr': 17, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 252, 'elevation': 23, + "key": "GP26", 'prn': 26, 'snr': null, - 'status': 'in view' + 'status': 'in view', + "system": "GPS" }, { 'azimuth': 186, 'elevation': 14, + "key": "GP4", 'prn': 4, 'snr': 15, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" } ], 'type': 'GSV', + "system": "GPS", 'valid': true }, '$GPGSV,3,3,11,29,09,301,24,16,09,020,,36,,,*76': { @@ -375,24 +423,31 @@ var tests = { { 'azimuth': 301, 'elevation': 9, + "key": "GP29", 'prn': 29, 'snr': 24, - 'status': 'tracking' + 'status': 'tracking', + "system": "GPS" }, { 'azimuth': 20, 'elevation': 9, + "key": "GP16", 'prn': 16, 'snr': null, - 'status': 'in view' + 'status': 'in view', + "system": "GPS" }, { 'azimuth': null, 'elevation': null, + "key": "GP36", 'prn': 36, 'snr': null, - 'status': 'in view' + 'status': 'in view', + "system": "GPS" } ], 'type': 'GSV', + "system": "GPS", 'valid': true }, '$GPRMC,092750.000,A,5321.6802,N,00630.3372,W,0.02,31.66,280511,,,A*43': { @@ -559,31 +614,40 @@ var tests = { { "azimuth": 106, "elevation": 20, + "key": "GP2", "prn": 2, "snr": 26, - "status": "tracking" + "status": "tracking", + "system": "GPS" }, { "azimuth": 72, "elevation": 20, + "key": "GP6", "prn": 6, "snr": 18, - "status": "tracking" + "status": "tracking", + "system": "GPS" }, { "azimuth": 40, "elevation": 77, + "key": "GP12", "prn": 12, "snr": 37, - "status": "tracking" + "status": "tracking", + "system": "GPS" }, { "azimuth": 309, "elevation": 30, + "key": "GP14", "prn": 14, "snr": 25, - "status": "tracking" + "status": "tracking", + "system": "GPS" } ], "satsInView": 11, "signalId": 1, + "system": "GPS", "type": "GSV", "valid": true }, @@ -595,13 +659,16 @@ var tests = { { "azimuth": 27, "elevation": 11, + "key": "GA33", "prn": 33, "snr": null, - "status": "in view" + "status": "in view", + "system": "Galileo" } ], "satsInView": 9, "signalId": 7, + "system": "Galileo", "type": "GSV", "valid": true }, @@ -613,31 +680,40 @@ var tests = { { "azimuth": 103, "elevation": 22, + "key": "GP2", "prn": 2, "snr": null, - "status": "in view" + "status": "in view", + "system": "GPS" }, { "azimuth": 357, "elevation": 0, + "key": "GP3", "prn": 3, "snr": null, - "status": "in view" + "status": "in view", + "system": "GPS" }, { "azimuth": 68, "elevation": 21, + "key": "GP6", "prn": 6, "snr": 18, - "status": "tracking" + "status": "tracking", + "system": "GPS" }, { "azimuth": 46, "elevation": 73, + "key": "GP12", "prn": 12, "snr": 32, - "status": "tracking" + "status": "tracking", + "system": "GPS" } ], "satsInView": 12, "signalId": 6, + "system": "GPS", "type": "GSV", "valid": true }, @@ -649,32 +725,41 @@ var tests = { { "azimuth": 285, "elevation": 49, + "key": "GA2", "prn": 2, "snr": 30, - "status": "tracking" + "status": "tracking", + "system": "Galileo" }, { "azimuth": 221, "elevation": 22, + "key": "GA3", "prn": 3, "snr": 29, - "status": "tracking" + "status": "tracking", + "system": "Galileo" }, { "azimuth": 328, "elevation": 12, + "key": "GA7", "prn": 7, "snr": null, - "status": "in view" + "status": "in view", + "system": "Galileo" }, { "azimuth": 278, "elevation": 32, + "key": "GA8", "prn": 8, "snr": 35, - "status": "tracking" + "status": "tracking", + "system": "Galileo" } ], "satsInView": 11, "signalId": 7, "type": "GSV", + "system": "Galileo", "valid": true }, "$GBGSV,1,1,04,13,31,064,,21,12,255,,26,18,293,27,29,46,155,31,1*78": { @@ -685,30 +770,39 @@ var tests = { { "azimuth": 64, "elevation": 31, + "key": "GB13", "prn": 13, "snr": null, - "status": "in view" + "status": "in view", + "system": "BeiDou" }, { "azimuth": 255, "elevation": 12, + "key": "GB21", "prn": 21, "snr": null, - "status": "in view" + "status": "in view", + "system": "BeiDou" }, { "azimuth": 293, "elevation": 18, + "key": "GB26", "prn": 26, "snr": 27, - "status": "tracking" + "status": "tracking", + "system": "BeiDou" }, { "azimuth": 155, "elevation": 46, + "key": "GB29", "prn": 29, "snr": 31, - "status": "tracking" + "status": "tracking", + "system": "BeiDou" } ], "satsInView": 4, + "system": "BeiDou", "signalId": 1, "type": "GSV", "valid": true @@ -731,6 +825,7 @@ var tests = { "fix": "3D", "hdop": 0.84, "mode": "automatic", + "system": "GPS", "systemId": 1, "pdop": 1.55, "raw": "$GNGSA,A,3,25,29,31,26,16,21,,,,,,,1.55,0.84,1.30,1*00", @@ -861,6 +956,7 @@ var tests = { 15 ], "systemId": 1, + "system": "GPS", "type": "GSA", "valid": true, "vdop": 3.87 @@ -943,29 +1039,38 @@ var tests = { "satellites": [{ "azimuth": null, "elevation": null, + "key": "BD1", "prn": 1, "snr": 37, - "status": "tracking" + "status": "tracking", + "system": "BD" }, { "azimuth": null, "elevation": null, + "key": "BD2", "prn": 2, "snr": 38, - "status": "tracking" + "status": "tracking", + "system": "BD" }, { "azimuth": null, "elevation": null, + "key": "BD3", "prn": 3, "snr": 39, - "status": "tracking" + "status": "tracking", + "system": "BD" }, { "azimuth": null, "elevation": null, + "key": "BD5", "prn": 5, "snr": 37, - "status": "tracking" + "status": "tracking", + "system": "BD" }], "satsInView": 16, + "system": "BD", "signalId": null, "type": "GSV", "valid": true @@ -977,24 +1082,31 @@ var tests = { "satellites": [{ "azimuth": 329, "elevation": 46, + "key": "BD10", "prn": 10, "snr": 31, - "status": "tracking" + "status": "tracking", + "system": "BD" }, { "azimuth": 161, "elevation": 43, + "key": "BD8", "prn": 8, "snr": null, - "status": "in view" + "status": "in view", + "system": "BD" }, { "azimuth": 217, "elevation": 40, + "key": "BD9", "prn": 9, "snr": null, - "status": "in view" + "status": "in view", + "system": "BD" }], "satsInView": 3, "signalId": null, + "system": "BD", "type": "GSV", "valid": true }, @@ -1005,29 +1117,38 @@ var tests = { "satellites": [{ "azimuth": 305, "elevation": 18, + "key": "BD211", "prn": 211, "snr": 36, - "status": "tracking" + "status": "tracking", + "system": "BD" }, { "azimuth": 113, "elevation": 7, + "key": "BD205", "prn": 205, "snr": null, - "status": "in view" + "status": "in view", + "system": "BD" }, { "azimuth": 29, "elevation": 4, + "key": "BD206", "prn": 206, "snr": null, - "status": "in view" + "status": "in view", + "system": "BD" }, { "azimuth": 46, "elevation": 30, + "key": "BD209", "prn": 209, "snr": null, - "status": "in view" + "status": "in view", + "system": "BD" }], "satsInView": 6, + "system": "BD", "signalId": null, "type": "GSV", "valid": true @@ -1052,7 +1173,7 @@ describe('NMEA syntax', function () { (function (i) { it('Should pass ' + i, function () { - expect(collect[i]).to.deep.equal(tests[i]); + assert.deepEqual(collect[i], tests[i]); }); })(i); } diff --git a/tests/partial.js b/tests/partial.js index aee40c9..02a23ad 100644 --- a/tests/partial.js +++ b/tests/partial.js @@ -1,42 +1,42 @@ -var expect = require('chai').expect; -var GPS = require('../gps.js'); -var gps = new GPS; - -var res = [{ - 'lat': 48.539856666666665, - 'lon': 9.059166666666666, - 'speed': 4.22256, - 'status': 'active', - 'time': new Date('2016-01-26T23:49:19.000Z'), - 'track': 2.93, - 'raw': '$GPRMC,234919.000,A,4832.3914,N,00903.5500,E,2.28,2.93,260116,,*0D', - 'type': 'RMC', - 'faa': null, - "navStatus": null, - 'valid': true, - 'variation': null - }, { - 'speed': 4.22256, - 'track': 2.93, - 'trackMagnetic': null, - 'raw': '$GPVTG,2.93,T,,M,2.28,N,4.2,K*66', - 'type': 'VTG', - 'faa': null, - 'valid': true - } +const GPS = require('gps'); +const assert = require('assert'); +const gps = new GPS; + +const res = [{ + 'lat': 48.539856666666665, + 'lon': 9.059166666666666, + 'speed': 4.22256, + 'status': 'active', + 'time': new Date('2016-01-26T23:49:19.000Z'), + 'track': 2.93, + 'raw': '$GPRMC,234919.000,A,4832.3914,N,00903.5500,E,2.28,2.93,260116,,*0D', + 'type': 'RMC', + 'faa': null, + "navStatus": null, + 'valid': true, + 'variation': null +}, { + 'speed': 4.22256, + 'track': 2.93, + 'trackMagnetic': null, + 'raw': '$GPVTG,2.93,T,,M,2.28,N,4.2,K*66', + 'type': 'VTG', + 'faa': null, + 'valid': true +} ]; -describe('partial updates', function() { +describe('partial updates', function () { - it('should work async with partial updates', function(done) { + it('should work async with partial updates', function (done) { var K = 0; - gps.on('data', function(data) { + gps.on('data', function (data) { try { - expect(data).to.deep.equal(res[K++]); + assert.deepEqual(data, res[K++]); } catch (e) { done(e); return;