From 94f58cd552a9b122816996c21047e34687f7157c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E5=BF=97=E8=B6=85?= Date: Mon, 16 Oct 2023 12:09:14 +0800 Subject: [PATCH] init commit --- .github/workflows/node.js.yml | 33 +++ .github/workflows/npm-publish.yml | 51 +++++ .gitignore | 2 + LICENSE | 20 ++ README.md | 45 ++++ bin/parse-nmea | 8 + codecs/APB.js | 58 +++++ codecs/BWC.js | 42 ++++ codecs/DBT.js | 45 ++++ codecs/GGA.js | 72 ++++++ codecs/GLL.js | 52 +++++ codecs/GRMT.js | 48 ++++ codecs/GSA.js | 45 ++++ codecs/GSV.js | 23 ++ codecs/HDG.js | 36 +++ codecs/HDM.js | 34 +++ codecs/HDT.js | 34 +++ codecs/MWV.js | 44 ++++ codecs/RDID.js | 29 +++ codecs/RMB.js | 51 +++++ codecs/RMC.js | 46 ++++ codecs/ROT.js | 35 +++ codecs/RSA.js | 28 +++ codecs/TXT.js | 25 +++ codecs/VTG.js | 70 ++++++ codecs/VWR.js | 39 ++++ codecs/ZDA.js | 45 ++++ helpers.js | 356 ++++++++++++++++++++++++++++++ liner.js | 49 ++++ nmea.js | 124 +++++++++++ package.json | 25 +++ test.js | 31 +++ test/APBtest.js | 10 + test/BWCtest.js | 10 + test/DBTtest.js | 23 ++ test/GGAtest.js | 44 ++++ test/GLLtest.js | 29 +++ test/GRMTtest.js | 9 + test/GSAtest.js | 9 + test/GSVtest.js | 9 + test/HDMtest.js | 19 ++ test/HDTtest.js | 19 ++ test/MWVtest.js | 27 +++ test/RDIDtest.js | 10 + test/RMCtest.js | 9 + test/ROTtest.js | 18 ++ test/VTGtest.js | 24 ++ test/helperstest.js | 22 ++ test/nmeaTest.js | 18 ++ util/parse.js | 14 ++ 50 files changed, 1968 insertions(+) create mode 100644 .github/workflows/node.js.yml create mode 100644 .github/workflows/npm-publish.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bin/parse-nmea create mode 100644 codecs/APB.js create mode 100644 codecs/BWC.js create mode 100644 codecs/DBT.js create mode 100644 codecs/GGA.js create mode 100644 codecs/GLL.js create mode 100644 codecs/GRMT.js create mode 100644 codecs/GSA.js create mode 100644 codecs/GSV.js create mode 100644 codecs/HDG.js create mode 100644 codecs/HDM.js create mode 100644 codecs/HDT.js create mode 100644 codecs/MWV.js create mode 100644 codecs/RDID.js create mode 100644 codecs/RMB.js create mode 100644 codecs/RMC.js create mode 100644 codecs/ROT.js create mode 100644 codecs/RSA.js create mode 100644 codecs/TXT.js create mode 100644 codecs/VTG.js create mode 100644 codecs/VWR.js create mode 100644 codecs/ZDA.js create mode 100644 helpers.js create mode 100644 liner.js create mode 100644 nmea.js create mode 100644 package.json create mode 100644 test.js create mode 100644 test/APBtest.js create mode 100644 test/BWCtest.js create mode 100644 test/DBTtest.js create mode 100644 test/GGAtest.js create mode 100644 test/GLLtest.js create mode 100644 test/GRMTtest.js create mode 100644 test/GSAtest.js create mode 100644 test/GSVtest.js create mode 100644 test/HDMtest.js create mode 100644 test/HDTtest.js create mode 100644 test/MWVtest.js create mode 100644 test/RDIDtest.js create mode 100644 test/RMCtest.js create mode 100644 test/ROTtest.js create mode 100644 test/VTGtest.js create mode 100644 test/helperstest.js create mode 100644 test/nmeaTest.js create mode 100644 util/parse.js diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..b9f7798 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,33 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [10.x, 12.x, 14.x, 15.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm ci + - run: npm run build --if-present + - run: npm test diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..d8a98d5 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,51 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages + +name: Node.js Package + +on: + release: + types: [created] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + - run: npm install + - run: npm ci + - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + - run: npm install + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.npm_token }} + +# publish-gpr: +# needs: build +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# - uses: actions/setup-node@v1 +# with: +# node-version: 12 +# registry-url: https://npm.pkg.github.com/ +# - run: npm ci +# - run: npm publish +# env: +# NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91dfed8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +node_modules \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6308e93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..46693d0 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +A NMEA-0183 GPS Protocol parser +=============================== + +An example using the node-serialport library to read a stream of messages +from a GlobalSat BU-353 USB GPS receiver: + +```` +var serialport = require('serialport'); +var nmea = require('nmea'); + +var port = new serialport.SerialPort('/dev/cu.usbserial', { + baudrate: 4800, + parser: serialport.parsers.readline('\r\n')}); + +port.on('data', function(line) { + console.log(nmea.parse(line)); +}); + +// { type: 'active-satellites', +// selectionMode: 'A', +// mode: 1, +// satellites: [ 29, 18, 21 ], +// PDOP: '', +// HDOP: '', +// VDOP: '', +// talker_id: 'GP' } +// { type: 'satellite-list-partial', +// numMsgs: 3, +// msgNum: 1, +// satsInView: 11, +// satellites: +// [ { id: '18', elevationDeg: 7, azimuthTrue: 214, SNRdB: 43 }, +// { id: '21', elevationDeg: 5, azimuthTrue: 114, SNRdB: 34 }, +// { id: '26', elevationDeg: 71, azimuthTrue: 234, SNRdB: 0 } ], +// talker_id: 'GP' } + +```` + +To add custom codecs +==================== +```` +var MyCustom = require('./MyCustom.js'); +nmea.traditionalDecoders['MyCustomr'] = MyCustom.decode; +```` + diff --git a/bin/parse-nmea b/bin/parse-nmea new file mode 100644 index 0000000..03e97cc --- /dev/null +++ b/bin/parse-nmea @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +var Liner = require('../liner.js'); + +process.stdin.resume(); +process.stdin.setEncoding('utf8'); + +process.stdin.pipe(new Liner()).pipe(require('../nmea.js').createDefaultTransformer()).pipe(require('JSONStream').stringify(false)).pipe(process.stdout);; \ No newline at end of file diff --git a/codecs/APB.js b/codecs/APB.js new file mode 100644 index 0000000..d3ba068 --- /dev/null +++ b/codecs/APB.js @@ -0,0 +1,58 @@ +exports.ID = 'APB'; +exports.TYPE = 'autopilot-b'; + +exports.decode = function(fields) { + /* + === APB - Autopilot Sentence "B" === + + 13 15 + ------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 9 10 11 12| 14| + | | | | | | | | | | | | | | | + $--APB,A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1. Status + V = LORAN-C Blink or SNR warning + V = general warning flag or other navigation systems when a reliable + fix is not available + 2. Status + V = Loran-C Cycle Lock warning flag + A = OK or not used + 3. Cross Track Error Magnitude + 4. Direction to steer, L or R + 5. Cross Track Units, N = Nautical Miles + 6. Status + A = Arrival Circle Entered + 7. Status + A = Perpendicular passed at waypoint + 8. Bearing origin to destination + 9. M = Magnetic, T = True + 10. Destination Waypoint ID + 11. Bearing, present position to Destination + 12. M = Magnetic, T = True + 13. Heading to steer to destination waypoint + 14. M = Magnetic, T = True + 15. Checksum + */ + return { + sentence: exports.ID, + type: exports.TYPE, + status1 : fields[1], + status2 : fields[2], + xteMagn : +fields[3], + steerDir : fields[4], + xteUnit : fields[5], + arrivalCircleStatus : fields[6], + arrivalPerpendicularStatus : fields[7], + bearingOrig2Dest : +fields[8], + bearingOrig2DestType : fields[9], + waypoint : fields[10], + bearing2Dest : +fields[11], + bearingDestType : fields[12], + heading2steer : +fields[13], + headingDestType : fields[14] + } +} \ No newline at end of file diff --git a/codecs/BWC.js b/codecs/BWC.js new file mode 100644 index 0000000..95ee565 --- /dev/null +++ b/codecs/BWC.js @@ -0,0 +1,42 @@ +/* + === BWC - Bearing & Distance to Waypoint - Great Circle === + ------------------------------------------------------------------------------ + 12 + 1 2 3 4 5 6 7 8 9 10 11| 13 14 + | | | | | | | | | | | | | | + $--BEC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c,m,*hh + ------------------------------------------------------------------------------ + Field Number: + 1. UTCTime + 2. Waypoint Latitude + 3. N = North, S = South + 4. Waypoint Longitude + 5. E = East, W = West + 6. Bearing, True + 7. T = True + 8. Bearing, Magnetic + 9. M = Magnetic + 10. Nautical Miles + 11. N = Nautical Miles + 12. Waypoint ID + 13. FAA mode indicator (NMEA 2.3 and later, optional) + 14. Checksum + */ + +exports.ID = 'BWC'; +exports.TYPE = '2waypoint'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + lat: +fields[2], + latPole: fields[3], + lon: +fields[4], + lonPole: fields[5], + bearingtrue: +fields[6], + bearingmag: +fields[8], + distance: +fields[10], + id: fields[12] + } +} \ No newline at end of file diff --git a/codecs/DBT.js b/codecs/DBT.js new file mode 100644 index 0000000..31c9c04 --- /dev/null +++ b/codecs/DBT.js @@ -0,0 +1,45 @@ +var helpers = require("../helpers.js") + +/* + === DBT - Depth below transducer === + + ------------------------------------------------------------------------------ + *******1 2 3 4 5 6 7 + *******| | | | | | | + $--DBT,x.x,f,x.x,M,x.x,F*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1. Depth, feet + 2. f = feet + 3. Depth, meters + 4. M = meters + 5. Depth, Fathoms + 6. F = Fathoms + 7. Checksum + */ + +exports.TYPE = 'depth-transducer'; +exports.ID = 'DBT'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + depthMeters: +fields[3], + depthFeet: +fields[1] + } +} + +exports.encode = function (talker, msg) { + var result = ['$' + talker + exports.ID]; + result.push(helpers.encodeFixed(msg.depthFeet,2)); + result.push('f'); + result.push(helpers.encodeFixed(msg.depthMeters, 2)); + result.push('M'); + result.push(helpers.encodeFixed(msg.depthFathoms, 2)); + result.push('F'); + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +} \ No newline at end of file diff --git a/codecs/GGA.js b/codecs/GGA.js new file mode 100644 index 0000000..9276a23 --- /dev/null +++ b/codecs/GGA.js @@ -0,0 +1,72 @@ +var helpers = require("../helpers.js") + +exports.TYPE = 'fix'; +exports.ID = 'GGA'; + +/* + 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 - fix not available, + 1 - GPS fix, + 2 - Differential GPS fix + 7) Number of satellites in view, 00 - 12 + 8) Horizontal Dilution of precision + 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 +*/ +var FIX_TYPE = ['none', 'fix', 'delta','pps','rtk','frtk','estimated','manual','simulation']; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + timestamp: fields[1], + lat: fields[2], + latPole: fields[3], + lon: fields[4], + lonPole: fields[5], + fixType: FIX_TYPE[+fields[6]], + numSat: +fields[7], + horDilution: +fields[8], + alt: +fields[9], + altUnit: fields[10], + geoidalSep: +fields[11], + geoidalSepUnit: fields[12], + differentialAge: +fields[13], + differentialRefStn: fields[14] + }; +} + +exports.encode = function (talker, msg) { + var result = ['$' + talker + exports.ID]; + result.push(helpers.encodeTime(msg.timestamp)); + result.push(helpers.encodeDegrees(msg.lat)); + result.push(msg.latPole); + result.push(helpers.encodeDegrees(msg.lon)); + result.push(msg.lonPole); + result.push(FIX_TYPE.indexOf(msg.fixType)); + result.push(helpers.encodeValue(msg.numSat)); + result.push(helpers.encodeFixed(msg.horDilution, 1)); + result.push(helpers.encodeAltitude(msg.alt)); + result.push(helpers.encodeGeoidalSeperation(msg.geoidalSep)); + result.push(helpers.encodeFixed(msg.differentialAge, 2)); + result.push(msg.differentialRefStn); + + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +} \ No newline at end of file diff --git a/codecs/GLL.js b/codecs/GLL.js new file mode 100644 index 0000000..8061f67 --- /dev/null +++ b/codecs/GLL.js @@ -0,0 +1,52 @@ +var helpers = require("../helpers.js") + +/* +=== GLL - Geographic Position - Latitude/Longitude === + +------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 + | | | | | | | | + $--GLL,llll.ll,a,yyyyy.yy,a,hhmmss.ss,a,m,*hh +------------------------------------------------------------------------------ + +Field Number: + +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 + */ + +exports.TYPE = 'geo-position'; +exports.ID = 'GLL'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: 'geo-position', + timestamp: fields[5], + lat: fields[1], + latPole: fields[2], + lon: fields[3], + lonPole: fields[4], + timestamp: fields[5], + status: fields[6] == 'A' ? 'valid' : 'invalid' + }; + } + +exports.encode = function (talker, msg) { + var result = ['$' + talker + exports.ID]; + result.push(helpers.encodeDegrees(msg.lat)); + result.push(msg.latPole); + result.push(helpers.encodeDegrees(msg.lon)); + result.push(msg.lonPole); + result.push(helpers.encodeTime(msg.timestamp)); + result.push('A'); + result.push('D'); + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +} \ No newline at end of file diff --git a/codecs/GRMT.js b/codecs/GRMT.js new file mode 100644 index 0000000..59823f5 --- /dev/null +++ b/codecs/GRMT.js @@ -0,0 +1,48 @@ +exports.ID = 'GRMT'; +exports.TYPE = 'sensor-information'; + +exports.decode = function(fields) { + /* + === PGRMT - Garmin Proprietary Sensor Status Information === + + The Garmin Proprietary sentence $PGRMT gives information concerning the status of a GPS sensor. This + sentence is transmitted once per minute regardless of the selected baud rate. + + ------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 9 + | | | | | | | | | + $PGRMT,GPS19x-HVS Software Version 2.20,P,P,R,R,P,C,15,R*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1. Product, model and software version (variable length field, e.g., “GPS 10 SW VER 2.01 BT VER 1.27 764”) + 2. ROM checksum test, P = pass, F = fail + 3. Receiver failure discrete, P = pass, F = fail + 4. Stored data lost, R = retained, L = lost + 5. Real time clock lost, R = retained, L = lost + 6. Oscillator drift discrete, P = pass, F = excessive drift detected + 7. Data collection discrete, C = collecting, null if not collecting + 8. GPS sensor temperature in degrees C + 9. GPS sensor configuration data, R = retained, L = lost + + Note: Some sensors have been seen to not provide all information above, in some cases just the product + model during boot. Example: + + $PGRMT,GPS19x-HVS Software Version 2.20,,,,,,,,*6F + + */ + return { + sentence: exports.ID, + type: exports.TYPE, + product : fields[1], + rom_checksum : fields[2], + receiver_failure : fields[3], + stored_data_lost : fields[4], + rtc_lost: fields[5], + oscillator_drift: fields[6], + data_collection: fields[7], + sensor_temperature: fields[8], + sensor_configuration: fields[9] + } +} diff --git a/codecs/GSA.js b/codecs/GSA.js new file mode 100644 index 0000000..817c066 --- /dev/null +++ b/codecs/GSA.js @@ -0,0 +1,45 @@ +/* + === GSA - GPS DOP and active satellites === + + This is one of the sentences commonly emitted by GPS units. + + ------------------------------------------------------------------------------ + 1 2 3 4 14 15 16 17 18 + | | | | | | | | | + $--GSA,a,a,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x.x,x.x,x.x*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1) Selection mode + 2) Mode + 3) ID of 1st satellite used for fix + 4) ID of 2nd satellite used for fix + ... + 14) ID of 12th satellite used for fix + 15) PDOP in meters + 16) HDOP in meters + 17) VDOP in meters + 18) Checksum +*/ +exports.TYPE = 'active-satellites'; +exports.ID = 'GSA'; + +exports.decode = function(fields) { + // $GPGSA,A,3,12,05,25,29,,,,,,,,,9.4,7.6,5.6 + var sats = []; + for (var i=1; i < 13; i++) { + if (fields[i+2]) sats.push(+fields[i+2]); + }; + return { + sentence: exports.ID, + type: exports.TYPE, + selectionMode: fields[1], + mode: +fields[2], + satellites: sats, + PDOP: +fields[15], + HDOP: +fields[16], + VDOP: +fields[17], + systemId: +fields[18] + }; +} \ No newline at end of file diff --git a/codecs/GSV.js b/codecs/GSV.js new file mode 100644 index 0000000..f855b46 --- /dev/null +++ b/codecs/GSV.js @@ -0,0 +1,23 @@ +exports.ID = 'GSV'; +exports.TYPE = 'satellite-list-partial'; + +exports.decode = function(fields) { + // $GPGSV,3,1,12, 05,58,322,36, 02,55,032,, 26,50,173,, 04,31,085, + var numRecords = (fields.length - 4) / 4, + sats = []; + for (var i=0; i < numRecords; i++) { + var offset = i * 4 + 4; + sats.push({id: fields[offset], + elevationDeg: +fields[offset+1], + azimuthTrue: +fields[offset+2], + SNRdB: +fields[offset+3]}); + }; + return { + sentence: exports.ID, + type: exports.TYPE, + numMsgs: +fields[1], + msgNum: +fields[2], + satsInView: +fields[3], + satellites: sats + }; +} \ No newline at end of file diff --git a/codecs/HDG.js b/codecs/HDG.js new file mode 100644 index 0000000..0a78e2e --- /dev/null +++ b/codecs/HDG.js @@ -0,0 +1,36 @@ +var helpers = require("../helpers.js") + +/* + === HDG - Magnetic heading, deviation, variation === + + ------------------------------------------------------------------------------ + 1 2 3 4 5 6 + | | | | | | +$--HDG,x.x,x.x,a,x.x,a*hh + ------------------------------------------------------------------------------ + + Field Number: + +1) Magnetic Sensor heading in degrees +2) Magnetic Deviation, degrees +3) Magnetic Deviation direction, E = Easterly, W = Westerly +4) Magnetic Variation degrees +5) Magnetic Variation direction, E = Easterly, W = Westerly +6) Checksum + + */ +exports.TYPE = 'heading-deviation-variation'; +exports.ID = 'HDG'; + +exports.decode = function (fields) { + console.log(fields); + return { + sentence: exports.ID, + type: 'heading-deviation-variation', + heading: +fields[1], + deviation: +fields[2], + deviationDirection: fields[3], + variation: +fields[4], + variationDirection: fields[5] + } +}; \ No newline at end of file diff --git a/codecs/HDM.js b/codecs/HDM.js new file mode 100644 index 0000000..aee05f9 --- /dev/null +++ b/codecs/HDM.js @@ -0,0 +1,34 @@ +var helpers = require("../helpers.js") +/* + === HDM - Heading - Magnetic === + + ------------------------------------------------------------------------------ + 1 2 3 + | | | + $--HDM,x.x,M*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1) Heading Degrees, magnetic + 2) M = Magnetic + 3) Checksum + */ +exports.TYPE = 'heading-info-magnetic'; +exports.ID = 'HDM'; + +exports.decode = function (fields) { + return { + sentence: exports.ID, + type: 'heading-info-magnetic', + heading: +fields[1] + } +}; + +exports.encode = function (talker, msg) { + var result = ['$' + talker + exports.ID]; + result.push(helpers.encodeFixed(msg.heading, 1)); + result.push('M'); + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +}; \ No newline at end of file diff --git a/codecs/HDT.js b/codecs/HDT.js new file mode 100644 index 0000000..138ec75 --- /dev/null +++ b/codecs/HDT.js @@ -0,0 +1,34 @@ +var helpers = require("../helpers.js") +/* + === HDT - Heading - True === + + ------------------------------------------------------------------------------ + 1 2 3 + | | | + $--HDT,x.x,T*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1) Heading Degrees, true + 2) T = True + 3) Checksum + */ +exports.TYPE = 'heading-info'; +exports.ID = 'HDT'; + +exports.decode = function (fields) { + return { + sentence: exports.ID, + type: 'heading-info', + heading: +fields[1] + } +}; + +exports.encode = function (talker, msg) { + var result = ['$' + talker + exports.ID]; + result.push(helpers.encodeFixed(msg.heading, 1)); + result.push('T'); + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +}; \ No newline at end of file diff --git a/codecs/MWV.js b/codecs/MWV.js new file mode 100644 index 0000000..59dcff9 --- /dev/null +++ b/codecs/MWV.js @@ -0,0 +1,44 @@ +var helpers = require("../helpers.js") +/* + === MWV - Wind Speed and Angle === + + ------------------------------------------------------------------------------ + *******1 2 3 4 5 + *******| | | | | + $--MWV,x.x,a,x.x,a*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1. Wind Angle, 0 to 360 degrees + 2. Reference, R = Relative, T = True + 3. Wind Speed + 4. Wind Speed Units, K/M/N + 5. Status, A = Data Valid + 6. Checksum + */ +exports.TYPE = 'wind'; +exports.ID = 'MWV'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + angle: +fields[1], + reference: fields[2], + speed: +fields[3], + units: fields[4], + status: fields[5] + } +} + +exports.encode = function(talker, msg) { + var result = ['$' + talker + exports.ID]; + result.push(helpers.encodeDegrees(msg.angle)); + result.push(msg.reference); + result.push(helpers.encodeFixed(msg.speed, 2)); + result.push(msg.units); + result.push(typeof msg.status === undefined ? 'A' : msg.status); + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +} \ No newline at end of file diff --git a/codecs/RDID.js b/codecs/RDID.js new file mode 100644 index 0000000..d802eab --- /dev/null +++ b/codecs/RDID.js @@ -0,0 +1,29 @@ +exports.ID = 'RDID'; +exports.TYPE = 'gyro'; + +exports.decode = function(fields) { + /* + === PRDID - RDI Proprietary Heading, Pitch, Roll === + + + ------------------------------------------------------------------------------ + 1 2 3 4 + | | | | + $PRDID,-2.06,4.81,37.62*6D + ------------------------------------------------------------------------------ + + Field Number: + + 1. Roll + 2. Pitch + 3. Heading + 4. Checksum + */ + return { + sentence: exports.ID, + type: exports.TYPE, + roll : +fields[1], + pitch : +fields[2], + heading : +fields[3], + } +} diff --git a/codecs/RMB.js b/codecs/RMB.js new file mode 100644 index 0000000..37a7030 --- /dev/null +++ b/codecs/RMB.js @@ -0,0 +1,51 @@ +/* +=== RMB Recommended Minimum Navigation Information === + +To be sent by a navigation receiver when a destination waypoint is active. + +------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + | | | | | | | | | | | | | | +$--RMB,A,x.x,a,c--c,c--c,llll.ll,a,yyyyy.yy,a,x.x,x.x,x.x,A*hh +------------------------------------------------------------------------------ + +Field Number: + + 1) Status, V = Navigation receiver warning + 2) Cross Track error - nautical miles + 3) Direction to Steer, Left or Right + 4) FROM Waypoint ID + 5) TO Waypoint ID + 6) Destination Waypoint Latitude + 7) N or S + 8) Destination Waypoint Longitude + 9) E or W +10) Range to destination in nautical miles +11) Bearing to destination in degrees True +12) Destination closing velocity in knots +13) Arrival Status, A = Arrival Circle Entered +14) Checksum + + */ +exports.TYPE = 'nav-info-waypoint'; +exports.ID = 'RMB'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + status: fields[1] == 'V' ? 'warning' : 'valid', + crossTrackError: +fields[2], + steer: fields[3], + fromWaypoint: fields[4], + toWaypoint: fields[5], + lat: fields[6], + latPole: fields[7], + lon: fields[8], + lonPole: fields[9], + range: +fields[10], + bearing: +fields[11], + vmg: +fields[12], + arrived: fields[13] == 'A' ? true : false + }; +} \ No newline at end of file diff --git a/codecs/RMC.js b/codecs/RMC.js new file mode 100644 index 0000000..4e7fcea --- /dev/null +++ b/codecs/RMC.js @@ -0,0 +1,46 @@ +/* + === RMC - Recommended Minimum Navigation Information === + + This is one of the sentences commonly emitted by GPS units. + + ------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 9 10 11 12 + | | | | | | | | | | | | + $--RMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,xxxx,x.x,a*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1) Time (UTC) + 2) Status, V = Navigation receiver warning + 3) Latitude + 4) N or S + 5) Longitude + 6) E or W + 7) Speed over ground, knots + 8) Track made good, degrees true + 9) Date, ddmmyy + 10) Magnetic Variation, degrees + 11) E or W + 12) Checksum +*/ +exports.TYPE = 'nav-info'; +exports.ID = 'RMC'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + timestamp: fields[1], + status: fields[2] == 'V' ? 'warning' : 'valid', + lat: fields[3], + latPole: fields[4], + lon: fields[5], + lonPole: fields[6], + speedKnots: +fields[7], + trackTrue: +fields[8], + date: fields[9], + variation: +fields[10], + variationPole: fields[11] + }; +} \ No newline at end of file diff --git a/codecs/ROT.js b/codecs/ROT.js new file mode 100644 index 0000000..5fd6887 --- /dev/null +++ b/codecs/ROT.js @@ -0,0 +1,35 @@ +var helpers = require("../helpers.js") +/* + === ROT - Rate Of Turn === + + ------------------------------------------------------------------------------ + 1 2 3 + | | | + $--ROT,x.x,A*hh + ------------------------------------------------------------------------------ + + Field Number: + + 1. Rate Of Turn, degrees per minute, "-" means bow turns to port + 2. Status, "A" means data is valid + 3. Checksum + */ +exports.TYPE = 'rate-of-turn'; +exports.ID = 'ROT'; + +exports.decode = function (fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + rateOfTurn: +fields[1], + valid: fields[2] === "A", + } +}; + +exports.encode = function (talker, msg) { + var result = ['$' + talker + exports.ID]; + result.push(helpers.encodeFixed(msg.rateOfTurn, 2)); + result.push('A'); + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +} diff --git a/codecs/RSA.js b/codecs/RSA.js new file mode 100644 index 0000000..3fcdfd9 --- /dev/null +++ b/codecs/RSA.js @@ -0,0 +1,28 @@ +/* + + === RSA - Rudder Angle === + + ------------------------------------------------------------------------------ + 1 2 3 + | | | + $--RSA,x.x,A,,*0B + ------------------------------------------------------------------------------ + + Field Number: + + 1. Rudder Angle + 2. Always A + 3. Checksum + + */ + +exports.TYPE = 'rudder'; +exports.ID = 'RSA'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + angle: +fields[1] + } +} \ No newline at end of file diff --git a/codecs/TXT.js b/codecs/TXT.js new file mode 100644 index 0000000..65a0271 --- /dev/null +++ b/codecs/TXT.js @@ -0,0 +1,25 @@ +/* + ZDA product info + 1 2 3 4 5 + | | | | | + $GPTXT,xx,yy,zz,info*hh + 1) The total number of statements in the current message, 01 to 99 + 2) Statement number, 01 to 99 + 3) Text identifier + 4) Text information + 5) Checksum + */ + +exports.ID = 'TXT'; +exports.TYPE = 'product-info'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + xx: fields[1], + yy: fields[2], + zz: fields[3], + info: fields[4] + }; +} \ No newline at end of file diff --git a/codecs/VTG.js b/codecs/VTG.js new file mode 100644 index 0000000..e51b559 --- /dev/null +++ b/codecs/VTG.js @@ -0,0 +1,70 @@ +var helpers = require("../helpers.js") +/* + === VTG - Track made good and Ground speed === + + ------------------------------------------------------------------------------ + 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 + ------------------------------------------------------------------------------ + + Field Number: + + 1. Track Degrees + 2. T = True + 3. Track Degrees + 4. M = Magnetic + 5. Speed Knots + 6. N = Knots + 7. Speed Kilometers Per Hour + 8. K = Kilometers Per Hour + 9. FAA mode indicator (NMEA 2.3 and later) + 10. Checksum=== VTG - Track made good and Ground speed === + + ------------------------------------------------------------------------------ + 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 + ------------------------------------------------------------------------------ + + Field Number: + + 1. Track Degrees + 2. T = True + 3. Track Degrees + 4. M = Magnetic + 5. Speed Knots + 6. N = Knots + 7. Speed Kilometers Per Hour + 8. K = Kilometers Per Hour + 9. FAA mode indicator (NMEA 2.3 and later) + 10. Checksum + */ +exports.TYPE = 'track-info'; +exports.ID = 'VTG'; + +exports.decode = function (fields) { + return { + sentence: exports.ID, + type: 'track-info', + trackTrue: +fields[1], + trackMagnetic: +fields[3], + speedKnots: +fields[5], + speedKmph: +fields[7] + } +}; + +exports.encode = function (talker, msg) { + var result = ['$' + talker + exports.ID]; + result.push(helpers.encodeDegrees(msg.trackTrue)); + result.push('T'); + result.push(helpers.encodeDegrees(msg.trackMagnetic)); + result.push('M'); + result.push(helpers.encodeFixed(msg.speedKnots, 2)); + result.push('N'); + result.push(''); + result.push(''); + result.push('A'); + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +} \ No newline at end of file diff --git a/codecs/VWR.js b/codecs/VWR.js new file mode 100644 index 0000000..c0a2375 --- /dev/null +++ b/codecs/VWR.js @@ -0,0 +1,39 @@ +var helpers = require("../helpers.js") +/* +=== VWR Relative Wind Speed and Angle === + +Note: This is no longer a sentence that is recommended by the NMEA 0183 Standard Committee +for new designs, however it does exist in a lot of equipment. + +------------------------------------------------------------------------------ + 1 2 3 4 5 6 7 8 9 + | | | | | | | | | +$--VWR,x.x,a,x.x,N,x.x,M,x.x,K*hh +------------------------------------------------------------------------------ + +Field Number: + +1) Wind direction magnitude in degrees +2) Wind direction Left/Right of bow +3) Speed +4) N +5) Speed +6) M = Meters Per Second +7) Speed +8) K = Kilometers Per Hour +9) Checksum + */ +exports.TYPE = 'wind-relative'; +exports.ID = 'VWR'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + angle: +fields[1], + direction: fields[2], + speedKnots: +fields[3], + speedMs: +fields[5], + speedKmph: +fields[7] + } +} \ No newline at end of file diff --git a/codecs/ZDA.js b/codecs/ZDA.js new file mode 100644 index 0000000..5c98e77 --- /dev/null +++ b/codecs/ZDA.js @@ -0,0 +1,45 @@ +var helpers = require("../helpers.js") + +/* + ZDA Time & Date – UTC, Day, Month, Year and Local Time Zone + 1 2 3 4 5 6 7 + | | | | | | | + $--ZDA,hhmmss.ss,xx,xx,xxxx,xx,xx*hh + 1) Local zone minutes description, same sign as local hours + 2) Local zone description, 00 to +/- 13 hours + 3) Year + 4) Month, 01 to 12 + 5) Day, 01 to 31 + 6) Time (UTC) + 7) Checksum + */ + +exports.ID = 'ZDA'; +exports.TYPE = 'time-zone'; + +exports.decode = function(fields) { + return { + sentence: exports.ID, + type: exports.TYPE, + timestamp: fields[1], + day: fields[2], + month: fields[3], + year: fields[4], + ltzh: fields[5], + ltzn: fields[6] + }; +} + +exports.encode = function (talker, msg) { + var result = ['$' + talker + exports.ID]; + var { date } = msg + result.push(helpers.padLeft(date.getHours().toString(), 2, '0') + helpers.padLeft(date.getMinutes().toString(), 2, '0') + helpers.padLeft(date.getSeconds().toString(), 2, '0') + '.000'); + result.push(helpers.padLeft(date.getDate().toString(), 2, '0')); + result.push(helpers.padLeft((date.getMonth() + 1).toString(), 2, '0')); + result.push(date.getFullYear().toString()); + result.push('00'); + result.push('00'); + + var resultMsg = result.join(','); + return resultMsg + helpers.computeChecksum(resultMsg); +} diff --git a/helpers.js b/helpers.js new file mode 100644 index 0000000..a639370 --- /dev/null +++ b/helpers.js @@ -0,0 +1,356 @@ +//Copied from from https://github.com/nherment/node-nmea/blob/master/lib/Helper.js + +var m_hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + +exports.toHexString = function(v) { + var lsn; + var msn; + + msn = (v >> 4) & 0x0f; + lsn = (v >> 0) & 0x0f; + return m_hex[msn] + m_hex[lsn]; +}; + +exports.padLeft = function(s, len, ch) { + while(s.length < len) { + s = ch + s; + } + return s; +}; + +// verify the checksum +exports.verifyChecksum = function(sentence, checksum) { + var q; + var c1; + var c2; + var i; + + // skip the $ + i = 1; + + // init to first character + c1 = sentence.charCodeAt(i); + + // process rest of characters, zero delimited + for( i = 2; i < sentence.length; ++i) { + c1 = c1 ^ sentence.charCodeAt(i); + } + + // checksum is a 2 digit hex value + c2 = parseInt(checksum, 16); + + // should be equal + return ((c1 & 0xff) === c2); +}; + +// generate a checksum for a sentence (no trailing *xx) +exports.computeChecksum = function(sentence) { + var c1; + var i; + + // skip the $ + i = 1; + + // init to first character var count; + + c1 = sentence.charCodeAt(i); + + // process rest of characters, zero delimited + for( i = 2; i < sentence.length; ++i) { + c1 = c1 ^ sentence.charCodeAt(i); + } + + return '*' + exports.toHexString(c1); +}; + +// ========================================= +// field encoders +// ========================================= + +// encode latitude +// input: latitude in decimal degrees +// output: latitude in nmea format +// ddmm.mmm +exports.encodeLatitude = function(lat) { + var d; + var m; + var f; + var h; + var s; + var t; + if(lat === undefined) { + return ''; + } + + if(lat < 0) { + h = 'S'; + lat = -lat; + } else { + h = 'N'; + } + // get integer degrees + d = Math.floor(lat); + // degrees are always 2 digits + s = d.toString(); + if(s.length < 2) { + s = '0' + s; + } + // get fractional degrees + f = lat - d; + // convert to fractional minutes + m = (f * 60.0); + // format the fixed point fractional minutes + t = m.toFixed(3); + if(m < 10) { + // add leading 0 + t = '0' + t; + } + + s = s + t + ',' + h; + return s; +}; + +// encode longitude +// input: longitude in decimal degrees +// output: longitude in nmea format +// dddmm.mmm +exports.encodeLongitude = function(lon) { + var d; + var m; + var f; + var h; + var s; + var t; + + if(lon === undefined) { + return ''; + } + + if(lon < 0) { + h = 'W'; + lon = -lon; + } else { + h = 'E'; + } + + // get integer degrees + d = Math.floor(lon); + // degrees are always 3 digits + s = d.toString(); + while(s.length < 3) { + s = '0' + s; + } + + // get fractional degrees + f = lon - d; + // convert to fractional minutes and round up to the specified precision + m = (f * 60.0); + // minutes are always 6 characters = mm.mmm + t = m.toFixed(3); + if(m < 10) { + // add leading 0 + t = '0' + t; + } + s = s + t + ',' + h; + return s; +}; + +// 1 decimal, always meters +exports.encodeAltitude = function(alt) { + if(alt === undefined) { + return ','; + } + return alt.toFixed(1) + ',M'; +}; + +// 1 decimal, always meters +exports.encodeGeoidalSeperation = function(geoidalSep) { + if(geoidalSep === undefined) { + return ','; + } + return geoidalSep.toFixed(1) + ',M'; +}; + +// magnetic variation +exports.encodeMagVar = function(v) { + var a; + var s; + if(v === undefined) { + return ','; + } + a = Math.abs(v); + s = (v < 0) ? (a.toFixed(1) + ',E') : (a.toFixed(1) + ',W'); + return exports.padLeft(s, 7, '0'); +}; + +// degrees +exports.encodeDegrees = function(d) { + if(d === undefined) { + return ''; + } + return exports.padLeft(d.toFixed(2), 6, '0'); +}; + +exports.encodeDate = function(d) { + var yr; + var mn; + var dy; + + if(d === undefined) { + return ''; + } + yr = d.getUTCFullYear(); + mn = d.getUTCMonth() + 1; + dy = d.getUTCDate(); + return exports.padLeft(dy.toString(), 2, '0') + exports.padLeft(mn.toString(), 2, '0') + yr.toString().substr(2); +}; + +exports.encodeTime = function(d) { + var h; + var m; + var s; + + if(d === undefined) { + return ''; + } + h = d.getUTCHours(); + m = d.getUTCMinutes(); + s = d.getUTCSeconds(); + return exports.padLeft(h.toString(), 2, '0') + exports.padLeft(m.toString(), 2, '0') + exports.padLeft(s.toString(), 2, '0'); +}; + +exports.encodeKnots = function(k) { + if(k === undefined) { + return ''; + } + return exports.padLeft(k.toFixed(1), 5, '0'); +}; + +exports.encodeValue = function(v) { + if(v === undefined) { + return ''; + } + return v.toString(); +}; + +exports.encodeFixed = function(v, f) { + if(v === undefined) { + return ''; + } + return v.toFixed(f); +}; + +// ========================================= +// field traditionalDecoders +// ========================================= + +// separate number and units +exports.parseAltitude = function(alt, units) { + var scale = 1.0; + if(units === 'F') { + scale = 0.3048; + } + return parseFloat(alt) * scale; +}; + +// separate degrees value and quadrant (E/W) +exports.parseDegrees = function(deg, quadrant) { + var q = (quadrant === 'E') ? -1.0 : 1.0; + + return parseFloat(deg) * q; +}; + +// fields can be empty so have to wrap the global parseFloat +exports.parseFloatX = function(f) { + if(f === '') { + return 0.0; + } + return parseFloat(f); +}; + +// decode latitude +// input : latitude in nmea format +// first two digits are degress +// rest of digits are decimal minutes +// output : latitude in decimal degrees +exports.parseLatitude = function(lat, hemi) { + var h = (hemi === 'N') ? 1.0 : -1.0; + var a; + var dg; + var mn; + var l; + a = lat.split('.'); + if(a[0].length === 4) { + // two digits of degrees + dg = lat.substring(0, 2); + mn = lat.substring(2); + } else if(a[0].length === 3) { + // 1 digit of degrees (in case no leading zero) + dg = lat.substring(0, 1); + mn = lat.substring(1); + } else { + // no degrees, just minutes (nonstandard but a buggy unit might do this) + dg = '0'; + mn = lat; + } + // latitude is usually precise to 5-8 digits + return ((parseFloat(dg) + (parseFloat(mn) / 60.0)) * h).toFixed(8); +}; + +// decode longitude +// first three digits are degress +// rest of digits are decimal minutes +exports.parseLongitude = function(lon, hemi) { + var h; + var a; + var dg; + var mn; + h = (hemi === 'E') ? 1.0 : -1.0; + a = lon.split('.'); + if(a[0].length === 5) { + // three digits of degrees + dg = lon.substring(0, 3); + mn = lon.substring(3); + } else if(a[0].length === 4) { + // 2 digits of degrees (in case no leading zero) + dg = lon.substring(0, 2); + mn = lon.substring(2); + } else if(a[0].length === 3) { + // 1 digit of degrees (in case no leading zero) + dg = lon.substring(0, 1); + mn = lon.substring(1); + } else { + // no degrees, just minutes (nonstandard but a buggy unit might do this) + dg = '0'; + mn = lon; + } + // longitude is usually precise to 5-8 digits + return ((parseFloat(dg) + (parseFloat(mn) / 60.0)) * h).toFixed(8); +}; + +// fields can be empty so have to wrap the global parseInt +exports.parseIntX = function(i) { + if(i === '') { + return 0; + } + return parseInt(i, 10); +}; + +exports.parseDateTime = function(date, time) { + var h = parseInt(time.slice(0, 2), 10); + var m = parseInt(time.slice(2, 4), 10); + var s = parseInt(time.slice(4, 6), 10); + var D = parseInt(date.slice(0, 2), 10); + var M = parseInt(date.slice(2, 4), 10) - 1; // UTC = 0..11 + var Y = parseInt(date.slice(4, 6), 10); + // hack : GPRMC date doesn't specify century. GPS came out in 1973 + // so if year is less than 73 its 2000, otherwise 1900 + if (Y < 73) { + Y = Y + 2000; + } + else { + Y = Y + 1900; + } + + return new Date(Date.UTC(Y, M, D, h, m, s)); +}; diff --git a/liner.js b/liner.js new file mode 100644 index 0000000..4c58530 --- /dev/null +++ b/liner.js @@ -0,0 +1,49 @@ +var Transform = require('stream').Transform; + +function Liner() { + Transform.call(this, { objectMode: true }); +} + +Liner.prototype = { + _transform: function (chunk, encoding, done) { + var data = chunk.toString() + if (this._lastLineData) data = this._lastLineData + data + + var lines = data.split('\n') + this._lastLineData = lines.splice(lines.length - 1, 1)[0] + + lines.forEach(this.push.bind(this)) + done() + }, + _flush: function (done) { + if (this._lastLineData) this.push(this._lastLineData) + this._lastLineData = null + done() + } +} + +extend(Transform, Liner); + +function extend(base, sub) { + // Avoid instantiating the base class just to setup inheritance + // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create + // for a polyfill + // Also, do a recursive merge of two prototypes, so we don't overwrite + // the existing prototype, but still maintain the inheritance chain + // Thanks to @ccnokes + var origProto = sub.prototype; + sub.prototype = Object.create(base.prototype); + for (var key in origProto) { + sub.prototype[key] = origProto[key]; + } + // Remember the constructor property was set wrong, let's fix it + sub.prototype.constructor = sub; + // In ECMAScript5+ (all modern browsers), you can make the constructor property + // non-enumerable if you define it like this instead + Object.defineProperty(sub.prototype, 'constructor', { + enumerable: false, + value: sub + }); +} + +module.exports = Liner; \ No newline at end of file diff --git a/nmea.js b/nmea.js new file mode 100644 index 0000000..e211900 --- /dev/null +++ b/nmea.js @@ -0,0 +1,124 @@ +// A NMEA-0183 parser based on the format given here: http://www.tronico.fi/OH6NT/docs/NMEA0183.pdf + +var MWV = require('./codecs/MWV.js'); +var VTG = require('./codecs/VTG.js'); +var DBT = require('./codecs/DBT.js'); +var GLL = require('./codecs/GLL.js'); +var BWC = require('./codecs/BWC.js'); +var GSV = require('./codecs/GSV.js'); +var GSA = require('./codecs/GSA.js'); +var GGA = require('./codecs/GGA.js'); +var RMB = require('./codecs/RMB.js'); +var RMC = require('./codecs/RMC.js'); +var RSA = require('./codecs/RSA.js'); +var APB = require('./codecs/APB.js'); +var HDG = require('./codecs/HDG.js'); +var HDT = require('./codecs/HDT.js'); +var HDM = require('./codecs/HDM.js'); +var RDID = require('./codecs/RDID.js'); +var GRMT = require('./codecs/GRMT.js'); +var VWR = require('./codecs/VWR.js'); +var ROT = require('./codecs/ROT.js'); +var ZDA = require('./codecs/ZDA.js'); +var TXT = require('./codecs/TXT.js'); + +// export helpers +module.exports.Helpers= require('./helpers.js'); + +var validLine = function (line) { + // check that the line passes checksum validation + // checksum is the XOR of all characters between $ and * in the message. + // checksum reference is provided as a hex value after the * in the message. + var checkVal = 0; + var parts = line.split('*'); + for (var i = 1; i < parts[0].length; i++) { + checkVal = checkVal ^ parts[0].charCodeAt(i); + } + ; + return checkVal == parseInt(parts[1], 16); +}; + +exports.traditionalDecoders = { + GGA: GGA.decode, + RMB: RMB.decode, + RMC: RMC.decode, + RSA: RSA.decode, + APB: APB.decode, + GSA: GSA.decode, + GSV: GSV.decode, + BWC: BWC.decode, + DBT: DBT.decode, + MWV: MWV.decode, + VTG: VTG.decode, + GLL: GLL.decode, + HDG: HDG.decode, + HDT: HDT.decode, + HDM: HDM.decode, + RDID: RDID.decode, + GRMT: GRMT.decode, + VWR: VWR.decode, + ROT: ROT.decode, + ZDA: ZDA.decode, + TXT: TXT.decode, +}; + +exports.encoders = new Object(); + +exports.encoders[MWV.TYPE] = MWV; +exports.encoders[VTG.TYPE] = VTG; +exports.encoders[DBT.TYPE] = DBT; +exports.encoders[GLL.TYPE] = GLL; +exports.encoders[HDT.TYPE] = HDT; +exports.encoders[GGA.TYPE] = GGA; +exports.encoders[HDM.TYPE] = HDM; +exports.encoders[ROT.TYPE] = ROT; +exports.encoders[ZDA.TYPE] = ZDA; +exports.encoders[TXT.TYPE] = TXT; + +exports.parse = function (line) { + if (validLine(line)) { + var fields = line.split('*')[0].split(','), + talker_id, + msg_fmt; + if (fields[0].charAt(1) == 'P') { + talker_id = 'P'; // Proprietary + msg_fmt = fields[0].substr(2); + } else { + talker_id = fields[0].substr(1, 2); + msg_fmt = fields[0].substr(3); + } + var parser = exports.traditionalDecoders[msg_fmt]; + if (parser) { + var val = parser(fields); + val.talker_id = talker_id; + return val; + } else { + throw Error("Error in parsing:" + line); + } + } else { + throw Error("Invalid line:" + line); + } +}; + +exports.encode = function (talker, msg) { + if (typeof msg === 'undefined') { + throw new Error("Can not encode undefined, did you forget msg parameter?"); + } + var encoder = exports.encoders[msg.type]; + if (encoder) { + return encoder.encode(talker, msg); + } else { + throw Error("No encoder for type:" + msg.type); + } +} + +exports.createDefaultTransformer = function (options) { + var stream = require('through')(function (data) { + try { + stream.queue(exports.parse(data)); + } catch (e) { + } + }); + return stream; +}; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..47b11f7 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "nmea", + "description": "A parser for the NMEA 0183 GPS Receiver protocol", + "version": "0.1.2", + "author": "James Penn ", + "license": "MIT", + "main": "nmea", + "keywords": [ + "gps", + "nmea" + ], + "repository": "git://github.com/jamesp/node-nmea", + "devDependencies": { + "line-reader": "0.2", + "mocha": "^8.1.1", + "should": "~2.0.2" + }, + "dependencies": { + "JSONStream": "0.7", + "through": ">=2.2.7 <3" + }, + "scripts": { + "test": "mocha" + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..6683f45 --- /dev/null +++ b/test.js @@ -0,0 +1,31 @@ +var nmea = require('./nmea') + +var s = [ +"$GPGSA,A,3,27,08,11,10,26,21,18,16,07,20,,,1.60,0.97,1.27*0F", +"$GPGSV,3,1,12,29,75,266,39,05,48,047,,26,43,108,,15,35,157,*78", +"$GPGSV,3,2,12,21,30,292,,18,21,234,,02,18,093,,25,13,215,*7F", +"$GPGSV,3,3,12,30,11,308,,16,,333,,12,,191,,07,-4,033,*62", +"$GPRMC,085542.023,V,,,,,,,041211,,,N*45", +"$IIRMC,101639,A,4924.407,N,00108.467,W,06.2,177,230720,01,W,A*04", +"$GPGGA,085543.023,,,,,0,00,,,M,0.0,M,,0000*58", +"$IIBWC,160947,6008.160,N,02454.290,E,162.4,T,154.3,M,001.050,N,DEST*1C", +"$IIAPB,A,A,0.001,L,N,V,V,154.3,M,DEST,154.3,M,154.2,M*19", +"$APHDG,175.6,,,,*5D", +"$APHDG,132.2,2.0,W,3.9,E*40", +"$GPHDT,274.07,T*03", +"$IIHDM,201.5,M*24", +"$PRDID,-4.44,2.12,154.25*56", +"$PGRMT,GPS19x-HVS Software Version 2.20,,,,,,,,*6F", +"$IIVWR,045.0,L,12.6,N,6.5,M,23.3,K*52", +"$ECRMB,A,0.060,L,,Waypoint 110,4921.975,N,00109.838,W,2.63,199.8,5.8,V,D*65", +"$ECRMB,A,0.001,R,001,002,5431.307,N,00941.537,E,1.488,31.551,0.104,V*32", +"$APRSA,1.15,A,,*0B", +"$APRSA,1.10,A,,*0E", +"$--ROT,-2.53,A*3F", +"$GNZDA,080246.000,07,05,2021,00,00*43", +"$GPTXT,01,01,01,ANTENNA OPEN*25", +]; + +for (var i=0; i < s.length; i++) { + console.log(nmea.parse(s[i])); +}; diff --git a/test/APBtest.js b/test/APBtest.js new file mode 100644 index 0000000..04c102f --- /dev/null +++ b/test/APBtest.js @@ -0,0 +1,10 @@ +var should = require('should'); + +describe('GGA ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*3C"); + msg.should.have.property('type', 'autopilot-b'); + msg.should.have.property('sentence', 'APB'); + }); +}); + diff --git a/test/BWCtest.js b/test/BWCtest.js new file mode 100644 index 0000000..89b2332 --- /dev/null +++ b/test/BWCtest.js @@ -0,0 +1,10 @@ +var should = require('should'); + +describe('BWC ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21"); + msg.should.have.property('type', '2waypoint'); + msg.should.have.property('sentence', 'BWC'); + }); +}); + diff --git a/test/DBTtest.js b/test/DBTtest.js new file mode 100644 index 0000000..99aa6f6 --- /dev/null +++ b/test/DBTtest.js @@ -0,0 +1,23 @@ +var should = require('should'); + +describe('DBT parsing', function () { + it('parses feet and meters', function () { + var msg = require("../nmea.js").parse("$IIDBT,036.41,f,011.10,M,005.99,F*25"); + msg.should.have.property('sentence', 'DBT'); + msg.should.have.property('type', 'depth-transducer'); + msg.should.have.property('depthFeet', 36.41); + msg.should.have.property('depthMeters', 11.10); + }); +}); + +describe('DBT encoding', function () { + it('encodes ok', function () { + var nmeaMsg = require("../nmea.js").encode('II', { + type: 'depth-transducer', + depthFeet: 36.41, + depthFathoms: 5.99, + depthMeters: 11.10 + }); + nmeaMsg.should.equal("$IIDBT,36.41,f,11.10,M,5.99,F*25"); + }); +}); \ No newline at end of file diff --git a/test/GGAtest.js b/test/GGAtest.js new file mode 100644 index 0000000..772b006 --- /dev/null +++ b/test/GGAtest.js @@ -0,0 +1,44 @@ +var should = require('should'); + +describe('GGA ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$IIGGA,123519,4807.04,N,1131.00,E,1,8,0.9,545.9,M,46.9,M,,*52"); + msg.should.have.property('type', 'fix'); + msg.should.have.property('sentence', 'GGA'); + msg.should.have.property('talker_id', 'II'); + msg.should.have.property('timestamp', '123519'); + msg.should.have.property('lat', '4807.04'); + msg.should.have.property('latPole', 'N'); + msg.should.have.property('lon', '1131.00'); + msg.should.have.property('lonPole', 'E'); + msg.should.have.property('fixType', 'fix'); + msg.should.have.property('numSat', 8); + msg.should.have.property('horDilution', 0.9); + msg.should.have.property('alt', 545.9); + msg.should.have.property('altUnit', 'M'); + msg.should.have.property('geoidalSep', 46.9); + msg.should.have.property('geoidalSepUnit', 'M'); + }); +}); + + +describe('GGA', function () { + it('encodes ok', function () { + var nmeaMsg = require("../nmea.js").encode('II', { + type: 'fix', + timestamp: new Date(Date.UTC(2013, 1, 1, 12, 35, 19)), + lat: 4807.04, + latPole: 'N', + lon: 1131.00, + lonPole: 'E', + fixType: 'fix', + numSat: 8, + horDilution: 0.9, + alt: 545.9, + altUnit: 'M', + geoidalSep: 46.9, + geoidalSepUnit: 'M' + }); + nmeaMsg.should.equal("$IIGGA,123519,4807.04,N,1131.00,E,1,8,0.9,545.9,M,46.9,M,,*52"); + }); +}); \ No newline at end of file diff --git a/test/GLLtest.js b/test/GLLtest.js new file mode 100644 index 0000000..dba9957 --- /dev/null +++ b/test/GLLtest.js @@ -0,0 +1,29 @@ +var should = require('should'); + +describe('GLL ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$GPGLL,6005.068,N,02332.341,E,095601,A,D*42"); + msg.should.have.property('type', 'geo-position'); + msg.should.have.property('sentence', 'GLL'); + msg.should.have.property('lat', '6005.068'); + msg.should.have.property('latPole', 'N'); + msg.should.have.property('lon', '02332.341'); + msg.should.have.property('lonPole', 'E'); + msg.should.have.property('status', 'valid'); + }); +}); + +describe('GLL', function () { + it('encodes ok', function () { + var nmeaMsg = require("../nmea.js").encode('II', { + type: 'geo-position', + lat: 6005.06, + latPole: 'N', + lon: 2332.34, + lonPole: 'E', + timestamp: new Date(2013, 4, 1, 21,17 - new Date().getTimezoneOffset(),22), + status: 'valid' + }); + nmeaMsg.should.equal("$IIGLL,6005.06,N,2332.34,E,211722,A,D*62"); + }); +}); \ No newline at end of file diff --git a/test/GRMTtest.js b/test/GRMTtest.js new file mode 100644 index 0000000..717ca23 --- /dev/null +++ b/test/GRMTtest.js @@ -0,0 +1,9 @@ +var should = require('should'); + +describe('GRMT ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$PGRMT,GPS19x-HVS Software Version 2.20,,,,,,,,*6F"); + msg.should.have.property('type', 'sensor-information'); + msg.should.have.property('sentence', 'GRMT'); + }); +}); \ No newline at end of file diff --git a/test/GSAtest.js b/test/GSAtest.js new file mode 100644 index 0000000..6d36f27 --- /dev/null +++ b/test/GSAtest.js @@ -0,0 +1,9 @@ +var should = require('should'); + +describe('GSV ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$GPGSA,A,3,12,05,25,29,,,,,,,,,9.4,7.6,5.6*37"); + msg.should.have.property('type', 'active-satellites'); + msg.should.have.property('sentence', 'GSA'); + }); +}); \ No newline at end of file diff --git a/test/GSVtest.js b/test/GSVtest.js new file mode 100644 index 0000000..b61a7d5 --- /dev/null +++ b/test/GSVtest.js @@ -0,0 +1,9 @@ +var should = require('should'); + +describe('GSV ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74 $GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*2D"); + msg.should.have.property('type', 'satellite-list-partial'); + msg.should.have.property('sentence', 'GSV'); + }); +}); \ No newline at end of file diff --git a/test/HDMtest.js b/test/HDMtest.js new file mode 100644 index 0000000..79cfb72 --- /dev/null +++ b/test/HDMtest.js @@ -0,0 +1,19 @@ +var should = require('should'); + +describe('HDM parsing', function () { + it('parse heading', function () { + var msg = require("../nmea.js").parse("$IIHDM,201.5,M*24"); + msg.should.have.property('sentence', 'HDM'); + msg.should.have.property('heading', 201.5); + }); +}); + +describe('HDM encoding', function () { + it('encodes ok', function () { + var nmeaMsg = require("../nmea.js").encode('II', { + type: 'heading-info-magnetic', + heading: 201.5 + }); + nmeaMsg.should.equal("$IIHDM,201.5,M*24"); + }); +}); \ No newline at end of file diff --git a/test/HDTtest.js b/test/HDTtest.js new file mode 100644 index 0000000..24148a5 --- /dev/null +++ b/test/HDTtest.js @@ -0,0 +1,19 @@ +var should = require('should'); + +describe('HDT parsing', function () { + it('parse heading', function () { + var msg = require("../nmea.js").parse("$IIHDT,234.2,T*25"); + msg.should.have.property('sentence', 'HDT'); + msg.should.have.property('heading', 234.2); + }); +}); + +describe('HDT encoding', function () { + it('encodes ok', function () { + var nmeaMsg = require("../nmea.js").encode('II', { + type: 'heading-info', + heading: 234.2 + }); + nmeaMsg.should.equal("$IIHDT,234.2,T*25"); + }); +}); \ No newline at end of file diff --git a/test/MWVtest.js b/test/MWVtest.js new file mode 100644 index 0000000..b436d97 --- /dev/null +++ b/test/MWVtest.js @@ -0,0 +1,27 @@ +var should = require('should'); + +describe('MWV parsing', function () { + it('parses ok', function () { + var msg = require("../nmea.js").parse("$IIMWV,017,R,02.91,N,A*2F"); + msg.should.have.property('sentence', 'MWV'); + msg.should.have.property('type', 'wind'); + msg.should.have.property('angle', 17); + msg.should.have.property('reference', 'R'); + msg.should.have.property('speed', 2.91); + msg.should.have.property('units', 'N'); + msg.should.have.property('status', 'A'); + }); +}); + +describe('MWV encoding', function () { + it('parses ok', function () { + var nmeaMsg = require("../nmea.js").encode('XX', { + type: 'wind', + angle: 17, + reference: 'R', + speed: 2.91, + units: 'N', + status: 'A'}); + nmeaMsg.should.equal("$XXMWV,017.00,R,2.91,N,A*31"); + }); +}); diff --git a/test/RDIDtest.js b/test/RDIDtest.js new file mode 100644 index 0000000..398d771 --- /dev/null +++ b/test/RDIDtest.js @@ -0,0 +1,10 @@ +var should = require('should'); + +describe('RDID ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$PRDID,-1.31,7.81,47.31*68"); + msg.should.have.property('type', 'gyro'); + msg.should.have.property('sentence', 'RDID'); + }); +}); + diff --git a/test/RMCtest.js b/test/RMCtest.js new file mode 100644 index 0000000..f56443e --- /dev/null +++ b/test/RMCtest.js @@ -0,0 +1,9 @@ +var should = require('should'); + +describe('RMC ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A"); + msg.should.have.property('type', 'nav-info'); + msg.should.have.property('sentence', 'RMC'); + }); +}); \ No newline at end of file diff --git a/test/ROTtest.js b/test/ROTtest.js new file mode 100644 index 0000000..50416aa --- /dev/null +++ b/test/ROTtest.js @@ -0,0 +1,18 @@ +var should = require('should'); + +describe('ROT ', function () { + it('parses', function () { + var msg = require("../nmea.js").parse("$--ROT,-2.53,A*3F"); + msg.should.have.property('type', 'rate-of-turn'); + msg.should.have.property('sentence', 'ROT'); + msg.should.have.property('rateOfTurn', -2.53); + }); + + it('encodes', function () { + var msg = require("../nmea.js").encode('--', { + type: 'rate-of-turn', + rateOfTurn: -2.53452 + }); + msg.should.equal('$--ROT,-2.53,A*3F'); + }); +}); diff --git a/test/VTGtest.js b/test/VTGtest.js new file mode 100644 index 0000000..e44afab --- /dev/null +++ b/test/VTGtest.js @@ -0,0 +1,24 @@ +var should = require('should'); + +describe('VTG parsing', function () { + it('parses ok', function () { + var msg = require("../nmea.js").parse("$IIVTG,210.43,T,210.43,M,5.65,N,,,A*67"); + msg.should.have.property('sentence', 'VTG'); + msg.should.have.property('type', 'track-info'); + msg.should.have.property('trackTrue', 210.43); + msg.should.have.property('trackMagnetic', 210.43); + msg.should.have.property('speedKnots', 5.65); + }); +}); + +describe('VTG encoding', function () { + it('encodes ok', function () { + var nmeaMsg = require("../nmea.js").encode('XX', { + type: 'track-info', + trackTrue: 210.43, + trackMagnetic: 209.43, + speedKnots: 2.91 + }); + nmeaMsg.should.equal("$XXVTG,210.43,T,209.43,M,2.91,N,,,A*63"); + }); +}); diff --git a/test/helperstest.js b/test/helperstest.js new file mode 100644 index 0000000..38488e9 --- /dev/null +++ b/test/helperstest.js @@ -0,0 +1,22 @@ +var should = require('should'); + +describe('helpers ', function () { + + it('padLeft', function () { + var msg = require("../helpers.js").padLeft("abc", 5, " "); + msg.should.equal(" abc"); + }); + + it('parseDateTime', function () { + // Input = 3rd of April of 2005 + var dt = require("../helpers.js").parseDateTime("030405", "112233"); + (+dt.getUTCDate()).should.equal(3); + (+dt.getUTCMonth() + 1).should.equal(4); + (+dt.getUTCFullYear()).should.equal(2005); + (+dt.getUTCHours()).should.equal(11); + (+dt.getUTCMinutes()).should.equal(22); + (+dt.getUTCSeconds()).should.equal(33); + }); + +}); + diff --git a/test/nmeaTest.js b/test/nmeaTest.js new file mode 100644 index 0000000..e4f4936 --- /dev/null +++ b/test/nmeaTest.js @@ -0,0 +1,18 @@ +var should = require('should'); + + +describe('Encoding unknown', function () { + it('undefined throws error', function () { + var nmea = require("../nmea.js"); + (function(){ + nmea.encode(undefined); + }).should.throw("Can not encode undefined, did you forget msg parameter?"); + }); + + it('no type throws error', function () { + var nmea = require("../nmea.js"); + (function(){ + nmea.encode('II', {type:"foo"}); + }).should.throw("No encoder for type:foo"); + }); +}); diff --git a/util/parse.js b/util/parse.js new file mode 100644 index 0000000..00c7547 --- /dev/null +++ b/util/parse.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +var lineReader = require('line-reader'); +var nmea = require('../nmea.js'); + +lineReader.eachLine(process.argv[2], function(line, last) { + var sentence = nmea.parse(line); + if (sentence !== undefined) { + console.log(sentence); + } else { + console.error("Parse error:" + line); + } + return !last; +}); \ No newline at end of file