├── sentences ├── TODO │ ├── PNKEP12.js │ ├── XDRMast.js │ ├── XDRHeel.js │ ├── PNKEP11.js │ ├── PNKEP04.js │ ├── PNKEP05.js │ └── TODO.js ├── ROT.js ├── MTW.js ├── HDT.js ├── HDM.js ├── XDRBaro.js ├── XDRTemp.js ├── HDMC.js ├── MTA.js ├── DBS.js ├── MWVT.js ├── DBK.js ├── DBT.js ├── PNKEP02.js ├── PSILTBS.js ├── XDRNA.js ├── XTE.js ├── XTE-GC.js ├── VLW.js ├── RSA.js ├── MMB.js ├── PNKEP01.js ├── VPW.js ├── HDTC.js ├── PSILCD1.js ├── PNKEP03.js ├── VWT.js ├── DPT.js ├── DPT-surface.js ├── ZDA.js ├── HDG.js ├── MWVR.js ├── PNKEP99.js ├── VTG.js ├── VWR.js ├── GLL.js ├── MWD.js ├── VHW.js ├── RMB.js ├── APB.js ├── RMC.js └── GGA.js ├── .github ├── dependabot.yml └── workflows │ ├── require_pr_label.yml │ ├── publish.yml │ ├── test.yml │ └── release_on_tag.yml ├── package.json ├── .gitignore ├── test ├── testutil.js ├── GGA.js ├── MWV.js ├── nmea.js └── RMC.js ├── README.md ├── CHANGELOG.md ├── index.js ├── nmea.js └── LICENSE /sentences/TODO/PNKEP12.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Battery 2 4 | $PNKEP,12,xx.x,,,*hh 5 | |_voltage 6 | */ 7 | -------------------------------------------------------------------------------- /sentences/TODO/XDRMast.js: -------------------------------------------------------------------------------- 1 | /* 2 | Mast angle: 3 | $IIXDR,A,x.x,D,mastangle,*hh 4 | I_Measurement of the mast angle in degrees 5 | */ 6 | -------------------------------------------------------------------------------- /sentences/TODO/XDRHeel.js: -------------------------------------------------------------------------------- 1 | /* 2 | $IIXDR,A,2.5,D,Heel Angle*hh from http://www.cruisersforum.com/forums/f134/tactics-plugin-166909-20.html 3 | */ 4 | -------------------------------------------------------------------------------- /sentences/TODO/PNKEP11.js: -------------------------------------------------------------------------------- 1 | /* 2 | Battery 1 3 | $PNKEP,11,xx.x,x.xx,xx.x,xx.x*hh 4 | | | | |_battery level % 5 | | | |_used capacity 6 | | |_current 7 | |_voltage 8 | */ 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | ignored_updates: 8 | - match: 9 | dependency_name: "baconjs" 10 | version_requirement: "3.x" 11 | -------------------------------------------------------------------------------- /sentences/ROT.js: -------------------------------------------------------------------------------- 1 | // to verify 2 | 3 | const nmea = require('../nmea.js') 4 | module.exports = function (app) { 5 | return { 6 | sentence: 'ROT', 7 | title: 'ROT - Rate of Turn', 8 | keys: ['navigation.rateOfTurn'], 9 | f: function (rot) { 10 | var degm = rot * 3437.74677078493 11 | return nmea.toSentence(['$IIROT', degm.toFixed(2), 'A']) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/require_pr_label.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Labels 2 | on: 3 | pull_request: 4 | types: [opened, labeled, unlabeled, synchronize] 5 | jobs: 6 | label: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: mheap/github-action-required-labels@v1 10 | with: 11 | mode: exactly 12 | count: 1 13 | labels: "fix, feature, doc, chore, test, ignore, other, dependencies" -------------------------------------------------------------------------------- /sentences/MTW.js: -------------------------------------------------------------------------------- 1 | // NMEA0183 Encoder MTW $IIMTW,40.0,C*17 2 | const nmea = require('../nmea.js') 3 | module.exports = function (app) { 4 | return { 5 | sentence: 'MTW', 6 | title: 'MTW - Water Temperature', 7 | keys: ['environment.water.temperature'], 8 | f: function (temperature) { 9 | var celcius = temperature - 273.15 10 | return nmea.toSentence(['$IIMTW', celcius.toFixed(1), 'C']) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sentences/HDT.js: -------------------------------------------------------------------------------- 1 | // NMEA0183 Encoder HDT $IIHDT,200.1,T*21 2 | const nmea = require('../nmea.js') 3 | module.exports = function (app) { 4 | return { 5 | sentence: 'HDT', 6 | title: 'HDT - Heading True', 7 | keys: ['navigation.headingTrue'], 8 | f: function (heading) { 9 | return nmea.toSentence([ 10 | '$IIHDT', 11 | nmea.radsToDeg(heading).toFixed(1), 12 | 'T' 13 | ]) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '16.x' 13 | registry-url: 'https://registry.npmjs.org' 14 | - run: npm publish --access public 15 | env: 16 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /sentences/HDM.js: -------------------------------------------------------------------------------- 1 | // NMEA0183 Encoder HDM $IIHDM,206.7,M*21 2 | const nmea = require('../nmea.js') 3 | module.exports = function (app) { 4 | return { 5 | sentence: 'HDM', 6 | title: 'HDM - Heading Magnetic', 7 | keys: ['navigation.headingMagnetic'], 8 | f: function (heading) { 9 | return nmea.toSentence([ 10 | '$IIHDM', 11 | nmea.radsToDeg(heading).toFixed(1), 12 | 'M' 13 | ]) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sentences/TODO/PNKEP04.js: -------------------------------------------------------------------------------- 1 | /* 2 | $PNKEP,04 Angles to optimise the CMG and VMG and 3 | ANGLE_OPT_CMG, ANGLE_OPT_VMG, 4 | GAIN_ROUTE_CMG, GAIN_ROUTE_VMG. 5 | 6 | From OpenCPN needs verification. 7 | $PNKEP,04,x.x,x.x,x.x,x.x*hh 8 | | | | \ Gain VMG from 0 to 999% 9 | \ \ \ Angle to optimise VMG from 0 to 359° 10 | \ \ Gain CMG from 0 to 999% 11 | \ Angle to optimise CMG from 0 to 359° 12 | 13 | 14 | */ 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [16.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | env: 23 | CI: true -------------------------------------------------------------------------------- /sentences/TODO/PNKEP05.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Sentence 5 4 | Current direction and speed from the atlas 5 | $PNKEP,05,x.x,x.x,N,x.x,K*hh 6 | |Current direction from 0 to 359° 7 | | Current speed in Knots 8 | | Current speed in km/h 9 | 10 | DIREC_COURANT, VITES_COURANT. 11 | 12 | From OpenCPN 13 | $PNKEP,05,x.x,x.x,N,x.x,K*hh 14 | | \ \current speed in km/h 15 | \ \ current speed in knots 16 | \ current direction from 0 à 359° 17 | */ 18 | -------------------------------------------------------------------------------- /sentences/XDRBaro.js: -------------------------------------------------------------------------------- 1 | /** 2 | $IIXDR,P,1.02481,B,Barometer*0D 3 | */ 4 | // $IIXDR,P,1.0050,B,Barometer*13 5 | 6 | const nmea = require('../nmea.js') 7 | module.exports = function (app) { 8 | return { 9 | title: 'XDR (Barometer) - Atomospheric Pressure', 10 | keys: ['environment.outside.pressure'], 11 | f: function (pressure) { 12 | return nmea.toSentence([ 13 | '$IIXDR', 14 | 'P', 15 | (pressure / 1.0e5).toFixed(4), 16 | 'B', 17 | 'Barometer' 18 | ]) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sentences/XDRTemp.js: -------------------------------------------------------------------------------- 1 | /* 2 | $IIXDR,C,19.52,C,TempAir*3D 3 | */ 4 | // $IIXDR,C,34.80,C,TempAir*19 5 | 6 | const nmea = require('../nmea.js') 7 | module.exports = function (app) { 8 | return { 9 | title: 'XDR (TempAir) - Air temperature.', 10 | keys: ['environment.outside.temperature'], 11 | f: function (temperature) { 12 | var celcius = temperature - 273.15 13 | return nmea.toSentence([ 14 | '$IIXDR', 15 | 'C', 16 | celcius.toFixed(2), 17 | 'C', 18 | 'TempAir' 19 | ]) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sentences/HDMC.js: -------------------------------------------------------------------------------- 1 | // NMEA0183 Encoder HDMC $IIHDM,212.2,M*21 2 | const nmea = require('../nmea.js') 3 | module.exports = function (app) { 4 | return { 5 | sentence: 'HDM', 6 | title: 'HDM - Heading Magnetic, calculated from True', 7 | keys: ['navigation.headingTrue', 'navigation.magneticVariation'], 8 | f: function (headingTrue, magneticVariation) { 9 | var heading = headingTrue + magneticVariation 10 | return nmea.toSentence([ 11 | '$IIHDM', 12 | nmea.radsToDeg(heading).toFixed(1), 13 | 'M' 14 | ]) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sentences/MTA.js: -------------------------------------------------------------------------------- 1 | /* 2 | Air temperature: 3 | $IIMTA,x.x,C*hh 4 | I__I_Temperature in degrees C 5 | */ 6 | // $IIMTA,34.80,C*3A 7 | 8 | const nmea = require('../nmea.js') 9 | module.exports = function (app) { 10 | return { 11 | sentence: 'MTA', 12 | title: 'MTA - Air temperature.', 13 | keys: ['environment.outside.temperature'], 14 | f: function (temperature) { 15 | // console.log("Got MTA--------------------------"); 16 | var celcius = temperature - 273.15 17 | return nmea.toSentence(['$IIMTA', celcius.toFixed(2), 'C']) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sentences/DBS.js: -------------------------------------------------------------------------------- 1 | // to verify 2 | const nmea = require('../nmea.js') 3 | module.exports = function (app) { 4 | return { 5 | sentence: 'DBS', 6 | title: 'DBS - Depth Below Surface', 7 | keys: ['environment.depth.belowSurface'], 8 | f: function mwv (depth) { 9 | var feet = depth * 3.28084 10 | var fathoms = depth * 0.546807 11 | return nmea.toSentence([ 12 | '$IIDBS', 13 | feet.toFixed(1), 14 | 'f', 15 | depth.toFixed(2), 16 | 'M', 17 | fathoms.toFixed(1), 18 | 'F' 19 | ]) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sentences/MWVT.js: -------------------------------------------------------------------------------- 1 | // NMEA0183 Encoder MWVTCB $INMWV,61.44,T,6.04,M,A*0A 2 | 3 | const nmea = require('../nmea.js') 4 | module.exports = function (app) { 5 | return { 6 | sentence: 'MWV', 7 | title: 'MWV - True Wind heading and speed', 8 | keys: ['environment.wind.angleTrueWater', 'environment.wind.speedTrue'], 9 | 10 | f: function (angle, speed) { 11 | return nmea.toSentence([ 12 | '$IIMWV', 13 | nmea.radsToPositiveDeg(angle).toFixed(2), 14 | 'T', 15 | speed.toFixed(2), 16 | 'M', 17 | 'A' 18 | ]) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@signalk/signalk-to-nmea0183", 3 | "version": "1.12.1", 4 | "description": "Signal K server plugin to convert Signal K to NMEA0183", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "format": "prettier-standard '*.js*' 'sentences/*.js'" 9 | }, 10 | "keywords": [ 11 | "signalk-node-server-plugin" 12 | ], 13 | "author": "teppo.kurki@iki.fi", 14 | "license": "ISC", 15 | "dependencies": { 16 | "baconjs": "^1.0.1" 17 | }, 18 | "devDependencies": { 19 | "mocha": "^11.7.1", 20 | "prettier-standard": "^16.4.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sentences/DBK.js: -------------------------------------------------------------------------------- 1 | // NMEA0183 Encoder DBK $IIDBK,102.9,f,31.38,M,17.2,F*39 2 | const nmea = require('../nmea.js') 3 | module.exports = function (app) { 4 | return { 5 | sentence: 'DBK', 6 | title: 'DBK - Depth Below Keel', 7 | keys: ['environment.depth.belowKeel'], 8 | f: function mwv (depth) { 9 | var feet = depth * 3.28084 10 | var fathoms = depth * 0.546807 11 | return nmea.toSentence([ 12 | '$IIDBK', 13 | feet.toFixed(1), 14 | 'f', 15 | depth.toFixed(2), 16 | 'M', 17 | fathoms.toFixed(1), 18 | 'F' 19 | ]) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sentences/DBT.js: -------------------------------------------------------------------------------- 1 | // NMEA0183 Encoder DBT $IIDBT,103.0,f,31.38,M,17.2,F*2E 2 | const nmea = require('../nmea.js') 3 | module.exports = function (app) { 4 | return { 5 | sentence: 'DBT', 6 | title: 'DBT - Depth Below Transducer', 7 | keys: ['environment.depth.belowTransducer'], 8 | f: function mwv (depth) { 9 | var feet = depth * 3.28084 10 | var fathoms = depth * 0.546807 11 | return nmea.toSentence([ 12 | '$IIDBT', 13 | feet.toFixed(1), 14 | 'f', 15 | depth.toFixed(2), 16 | 'M', 17 | fathoms.toFixed(1), 18 | 'F' 19 | ]) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sentences/PNKEP02.js: -------------------------------------------------------------------------------- 1 | /** 2 | $PNKEP,02,x.x*hh 3 | \ Course (COG) on other tack from 0 to 359° 4 | */ 5 | // to verify 6 | const nmea = require('../nmea.js') 7 | module.exports = function (app) { 8 | return { 9 | title: 'PNKEP,02 - Course (COG) on other tack from 0 to 359°', 10 | keys: ['performance.tackMagnetic'], 11 | f: function (tackMagnetic) { 12 | // console.log("Got tackMagnetic --------------------------------------------------"); 13 | 14 | return nmea.toSentence([ 15 | '$PNKEP', 16 | '02', 17 | nmea.radsToDeg(tackMagnetic).toFixed(2) 18 | ]) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sentences/PSILTBS.js: -------------------------------------------------------------------------------- 1 | /* 2 | PSILTBS - Proprietary target boat speed sentence for Silva => Nexus => Garmin displays 3 | 4 | 5 | 0 1 2 6 | | | | 7 | $PSILTBS,XX.xx,N,*hh 8 | Field Number: 9 | 0 Target Boat speed in knots 10 | 1 N for knots 11 | 2 Checksum 12 | */ 13 | 14 | const nmea = require('../nmea.js') 15 | module.exports = function (app) { 16 | return { 17 | title: 'PSILTBS - Garmin proprietary target boat speed', 18 | keys: ['performance.targetSpeed'], 19 | f: function (tbs) { 20 | return nmea.toSentence(['$PSILTBS', nmea.msToKnots(tbs).toFixed(2), 'N']) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sentences/TODO/TODO.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /** 5 | 6 | TODO: 7 | 8 | $INGGA … Global Positioning System Fix Data 9 | $INXDR …,N,x.x,N,FRST … Forestay 10 | $INXDR …,A,x.x,D,ROLL … Heel angle 11 | $INXDR ...,H,x.x,P,HYGR … Humidity 12 | $INXDR …,A,x.x,D,KEEL … Keel Angle 13 | $INXDR …,A,x.x,D,LEEW … Leeway angle 14 | $INRSA … Rudder angle 15 | $INVDR … Set and Drift 16 | $INVPW … VMG 17 | $INWCV … Waypoint closure velocity 18 | $IIXDR Batteries voltage 1 Hz 19 | 20 | 21 | GPS 22 | $GPGGA GPS Fix Data 10 Hz 23 | $GPGSA GNSS DOP and Active Satellites 1 Hz 24 | $GPGSV GNSS Satellites in View 1 Hz 25 | $IIXDR Internal temperature 1 Hz 26 | 27 | 28 | 29 | */ -------------------------------------------------------------------------------- /sentences/XDRNA.js: -------------------------------------------------------------------------------- 1 | /** 2 | $IIXDR,A,-0.7,D,PTCH,A,0.9,D,ROLL*0D 3 | */ 4 | // $IIXDR,A,-0.7,D,PTCH,A,0.9,D,ROLL*13 5 | 6 | const nmea = require('../nmea.js') 7 | module.exports = function (app) { 8 | return { 9 | title: 'XDR (PTCH-ROLL) - Pitch and Roll', 10 | keys: ['navigation.attitude'], 11 | f: function (attitude) { 12 | return nmea.toSentence([ 13 | '$IIXDR', 14 | 'A', 15 | nmea.radsToDeg(attitude.pitch).toFixed(1), 16 | 'D', 17 | 'PTCH', 18 | 'A', 19 | nmea.radsToDeg(attitude.roll).toFixed(1), 20 | 'D', 21 | 'ROLL' 22 | ]) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sentences/XTE.js: -------------------------------------------------------------------------------- 1 | /* 2 | Cross-track error: 3 | $IIXTE,A,A,x.x,a,N,A*hh 4 | I_Cross-track error in miles, L= left, R= right 5 | */ 6 | // to verify 7 | const nmea = require('../nmea.js') 8 | module.exports = function (app) { 9 | return { 10 | title: 'XTE - Cross-track error (w.r.t. Rhumb line)', 11 | keys: ['navigation.course.calcValues.crossTrackError'], 12 | f: function (crossTrackError) { 13 | return nmea.toSentence([ 14 | '$IIXTE', 15 | 'A', 16 | 'A', 17 | Math.abs(nmea.mToNm(crossTrackError)).toFixed(3), 18 | crossTrackError < 0 ? 'R' : 'L', 19 | 'N' 20 | ]) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sentences/XTE-GC.js: -------------------------------------------------------------------------------- 1 | /* 2 | Cross-track error: 3 | $IIXTE,A,A,x.x,a,N,A*hh 4 | I_Cross-track error in miles, L= left, R= right 5 | */ 6 | // to verify 7 | const nmea = require('../nmea.js') 8 | module.exports = function (app) { 9 | return { 10 | title: 'XTE - Cross-track error (w.r.t. Great Circle)', 11 | keys: ['navigation.courseGreatCircle.crossTrackError'], 12 | f: function (crossTrackError) { 13 | return nmea.toSentence([ 14 | '$IIXTE', 15 | 'A', 16 | 'A', 17 | Math.abs(nmea.mToNm(crossTrackError)).toFixed(3), 18 | crossTrackError < 0 ? 'R' : 'L', 19 | 'N' 20 | ]) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sentences/VLW.js: -------------------------------------------------------------------------------- 1 | /** 2 | Total log and daily log: 3 | $IIVLW,x.x,N,x.x,N*hh 4 | I I I__I_Daily log in miles 5 | I__I_Total log in miles 6 | */ 7 | // NMEA0183 Encoder VLW $IIVLW,9417.40,N,43.18,N*4C 8 | 9 | const nmea = require('../nmea.js') 10 | module.exports = function (app) { 11 | return { 12 | sentence: 'VLW', 13 | title: 'VLW - Total log and daily log', 14 | keys: ['navigation.log', 'navigation.trip.log'], 15 | f: function (logDistance, tripDistance) { 16 | return nmea.toSentence([ 17 | '$IIVLW', 18 | nmea.mToNm(logDistance).toFixed(2), 19 | 'N', 20 | nmea.mToNm(tripDistance).toFixed(2), 21 | 'N' 22 | ]) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sentences/RSA.js: -------------------------------------------------------------------------------- 1 | /** 2 | Rudder Sensor Angle: 3 | $--RSA,x.x,A,x.x,A*hh 4 | Field Number: 5 | 1 Starboard (or single) rudder sensor, "-" means Turn To Port 6 | 2 Status, A means data is valid 7 | 3 Port rudder sensor 8 | 4 Status, A means data is valid 9 | 5 Checksum 10 | */ 11 | 12 | const nmea = require('../nmea.js') 13 | module.exports = function (app) { 14 | return { 15 | sentence: 'RSA', 16 | title: 'RSA - Rudder Sensor Angle', 17 | keys: ['steering.rudderAngle'], 18 | f: function (rudderAngle) { 19 | return nmea.toSentence([ 20 | '$IIRSA', 21 | nmea.radsToDeg(rudderAngle).toFixed(2), 22 | 'A', 23 | '', 24 | '' 25 | ]) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sentences/MMB.js: -------------------------------------------------------------------------------- 1 | /* 2 | Barometer: 3 | $IIMMB,x.x,I,x.x,B*hh 4 | I I I__I_Atmospheric pressure in bars 5 | I_ I_Atmospheric pressure in inches of mercury 6 | */ 7 | // $IIMMB,29.6776,I,1.00,B*73 8 | const nmea = require('../nmea.js') 9 | module.exports = function (app) { 10 | return { 11 | sentence: 'MMB', 12 | title: 'MMB - Environment outside pressure', 13 | keys: ['environment.outside.pressure'], 14 | f: function (pressure) { 15 | // console.log("Got MMB--------------------------"); 16 | return nmea.toSentence([ 17 | '$IIMMB', 18 | (pressure / 3386.39).toFixed(4), 19 | 'I', 20 | (pressure / 1.0e5).toFixed(4), 21 | 'B' 22 | ]) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sentences/PNKEP01.js: -------------------------------------------------------------------------------- 1 | /* 2 | Sentence 1 3 | $PNKEP,01,x.x,N,x.x,K*hh 4 | | STW target in knots 5 | | STW target in km/h 6 | */ 7 | 8 | // $PNKEP,01,3.69,N,6.83,K*69 9 | const nmea = require('../nmea.js') 10 | module.exports = function (app) { 11 | return { 12 | title: 'PNKEP,01 - Target Polar speed', 13 | keys: ['performance.polarSpeed'], 14 | f: function (polarSpeed) { 15 | // console.log("Got Polar speed --------------------------------------------------"); 16 | return nmea.toSentence([ 17 | '$PNKEP', 18 | '01', 19 | nmea.msToKnots(polarSpeed).toFixed(2), 20 | 'N', 21 | nmea.msToKM(polarSpeed).toFixed(2), 22 | 'K' 23 | ]) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sentences/VPW.js: -------------------------------------------------------------------------------- 1 | /** 2 | $IIVPW,x.x,N,x.x,M*hh 3 | I I I I__I_Surface speed in meters per second 4 | __I_Surface speed in knots 5 | */ 6 | 7 | // NMEA0183 Encoder VPW $IIVHW,6.5,N,12.64,M*48 8 | 9 | const nmea = require('../nmea.js') 10 | module.exports = function (app) { 11 | return { 12 | sentence: 'VPW', 13 | title: 'VPW - Speed – Measured Parallel to Wind', 14 | keys: [ 15 | 'performance.velocityMadeGood' 16 | ], 17 | f: function vpw (velocityMadeGood) { 18 | return nmea.toSentence([ 19 | '$IIVPW', 20 | nmea.msToKnots(velocityMadeGood).toFixed(2), 21 | 'N', 22 | velocityMadeGood.toFixed(2), 23 | 'M' 24 | ]) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sentences/HDTC.js: -------------------------------------------------------------------------------- 1 | // NMEA0183 Encoder HDT $IIHDT,200.1,T*21 2 | const nmea = require('../nmea.js') 3 | module.exports = function (app) { 4 | return { 5 | sentence: 'HDTC', 6 | title: 'HDT - Heading True calculated from magnetic heading and variation', 7 | keys: ['navigation.headingMagnetic', 'navigation.magneticVariation' ], 8 | f: function (headingMagnetic, magneticVariation) { 9 | var heading = headingMagnetic + magneticVariation 10 | if (heading > 2 * Math.PI) heading -= 2 * Math.PI 11 | else if (heading < 0 ) heading += 2 * Math.PI 12 | return nmea.toSentence([ 13 | '$IIHDT', 14 | nmea.radsToDeg(heading).toFixed(1), 15 | 'T' 16 | ]) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sentences/PSILCD1.js: -------------------------------------------------------------------------------- 1 | /*PSILCD1 - Proprietary polar boat speed sentence for Silva => Nexus => Garmin displays 2 | 3 | 4 | 0 1 2 5 | | | | 6 | $PSILCD1,XX.xx,YY.yy,*hh 7 | Field Number: 8 | 0 Polar Boat speed in knots 9 | 1 Target wind angle 10 | 2 Checksum 11 | */ 12 | 13 | const nmea = require('../nmea.js') 14 | module.exports = function (app) { 15 | return { 16 | title: 'PSILCD1 - Send polar speed and target wind angle to Silva/Nexus/Garmin displays', 17 | keys: ['performance.polarSpeed', 'performance.targetAngle'], 18 | f: function (polarSpeed, targetAngle) { 19 | return nmea.toSentence(['$PSILCD1', nmea.msToKnots(polarSpeed).toFixed(2), nmea.radsToDeg(targetAngle).toFixed(2)]) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .DS_Store 39 | 40 | package-lock.json 41 | -------------------------------------------------------------------------------- /.github/workflows/release_on_tag.yml: -------------------------------------------------------------------------------- 1 | name: 'Release on tag' 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | jobs: 8 | release: 9 | permissions: 10 | contents: write 11 | if: startsWith(github.ref, 'refs/tags/') 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Build Changelog 15 | id: github_release 16 | uses: mikepenz/release-changelog-builder-action@v1 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }} 19 | 20 | - name: Create Release 21 | uses: actions/create-release@v1 22 | with: 23 | tag_name: ${{ github.ref }} 24 | release_name: ${{ github.ref }} 25 | body: ${{steps.github_release.outputs.changelog}} 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }} -------------------------------------------------------------------------------- /test/testutil.js: -------------------------------------------------------------------------------- 1 | const Bacon = require('baconjs') 2 | 3 | module.exports = { 4 | createAppWithPlugin: function (onEmit, enabledConversion) { 5 | const streams = { 6 | } 7 | const app = { 8 | streambundle: { 9 | getSelfStream: path => { 10 | if (streams[path]) { 11 | return streams[path] 12 | } else { 13 | return streams[path] = new Bacon.Bus() 14 | } 15 | } 16 | }, 17 | emit: (name, value) => { 18 | if (name === 'nmea0183out') { 19 | onEmit(name, value) 20 | } 21 | }, 22 | debug: (msg) => console.log(msg) 23 | } 24 | const plugin = require('../')(app) 25 | const options = {} 26 | options[enabledConversion] = true 27 | plugin.start(options) 28 | return app 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sentences/PNKEP03.js: -------------------------------------------------------------------------------- 1 | /* 2 | Sentence 3 3 | $PNKEP,03,x.x,x.x,x.x*hh 4 | | optimum angle from 0 to 359° 5 | | VMG efficiency up/down wind in % 6 | | Polar efficiency in % 7 | 8 | */ 9 | 10 | // to verify 11 | const nmea = require('../nmea.js') 12 | module.exports = function (app) { 13 | return { 14 | title: 'PNKEP,03 - Polar and VMG, and optimum angle.', 15 | keys: [ 16 | 'performance.targetAngle', 17 | 'performance.polarVelocityMadeGoodRatio', 18 | 'performance.polarSpeedRatio' 19 | ], 20 | f: function (targetAngle, polarVelocityMadeGoodRatio, polarSpeedRatio) { 21 | return nmea.toSentence([ 22 | '$PNKEP', 23 | '03', 24 | nmea.radsToDeg(targetAngle).toFixed(2), 25 | (polarVelocityMadeGoodRatio * 100.0).toFixed(2), 26 | (polarSpeedRatio * 100.0).toFixed(2) 27 | ]) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sentences/VWT.js: -------------------------------------------------------------------------------- 1 | /** 2 | $IIVWT,x.x,a,x.x,N,x.x,M,x.x,K*hh 3 | I I I I I I I__I_Wind speed in kph 4 | I I I I I__I_Wind speed in m/s 5 | I I I_ I_Wind speed in knots 6 | I__I_True wind angle from 0° to 180° , L=port, R=starboard 7 | */ 8 | 9 | // NMEA0183 Encoder VWT $IIVWT,86.71,a,12.58,N,6.47,M,23.29,K*45 10 | 11 | const nmea = require('../nmea.js') 12 | module.exports = function (app) { 13 | return { 14 | sentence: 'VWT', 15 | title: 'VWT - True wind speed relative to boat.', 16 | keys: ['environment.wind.angleTrueWater', 'environment.wind.speedTrue'], 17 | f: function (angleTrueWater, speedTrue) { 18 | return nmea.toSentence([ 19 | '$IIVWT', 20 | nmea.radsToDeg(angleTrueWater).toFixed(2), 21 | 'a', 22 | nmea.msToKnots(speedTrue).toFixed(2), 23 | 'N', 24 | speedTrue.toFixed(2), 25 | 'M', 26 | nmea.msToKM(speedTrue).toFixed(2), 27 | 'K' 28 | ]) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sentences/DPT.js: -------------------------------------------------------------------------------- 1 | /** 2 | DPT - Depth of Water 3 | 1 2 3 4 4 | | | | | 5 | $--DPT,x.x,x.x,x.x*hh 6 | Field Number: 7 | 1. Water depth relative to transducer, meters 8 | 2. Offset from transducer, meters positive means distance from transducer to water line negative means distance from transducer to keel 9 | 3. Maximum range scale in use (NMEA 3.0 and above) 10 | 4. Checksum 11 | */ 12 | // NMEA0183 Encoder DPT $IIDPT,69.21,-0.001*60 13 | const nmea = require('../nmea.js') 14 | module.exports = function (app) { 15 | return { 16 | sentence: 'DPT', 17 | title: 'DPT - Depth', 18 | keys: [ 19 | 'environment.depth.belowTransducer', 20 | 'environment.depth.transducerToKeel' 21 | ], 22 | f: function dpt (belowTransducer, transducerToKeel) { 23 | return nmea.toSentence([ 24 | '$IIDPT', 25 | belowTransducer.toFixed(2), 26 | (-Math.abs(transducerToKeel)).toFixed(3) 27 | ]) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sentences/DPT-surface.js: -------------------------------------------------------------------------------- 1 | /** 2 | DPT - Depth of Water 3 | 1 2 3 4 4 | | | | | 5 | $--DPT,x.x,x.x,x.x*hh 6 | Field Number: 7 | 1. Water depth relative to transducer, meters 8 | 2. Offset from transducer, meters positive means distance from transducer to water line negative means distance from transducer to keel 9 | 3. Maximum range scale in use (NMEA 3.0 and above) 10 | 4. Checksum 11 | */ 12 | // NMEA0183 Encoder DPT $IIDPT,9.2,1.1*4B 13 | const nmea = require('../nmea.js') 14 | module.exports = function (app) { 15 | return { 16 | sentence: 'DPT', 17 | title: 'DPT - Depth at Surface (using surfaceToTransducer)', 18 | keys: [ 19 | 'environment.depth.belowTransducer', 20 | 'environment.depth.surfaceToTransducer' 21 | ], 22 | f: function dpt (belowTransducer, surfaceToTransducer) { 23 | return nmea.toSentence([ 24 | '$IIDPT', 25 | belowTransducer.toFixed(2), 26 | surfaceToTransducer.toFixed(3) 27 | ]) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sentences/ZDA.js: -------------------------------------------------------------------------------- 1 | /* 2 | UTC time and date: 3 | $IIZDA,hhmmss.ss,xx,xx,xxxx,,*hh 4 | I I I I_Year 5 | I I I_Month 6 | I I_Day 7 | I_Time 8 | */ 9 | // NMEA0183 Encoder ZDA $IIZDA,200006.020,15,08,2014,,*4C 10 | const nmea = require('../nmea.js') 11 | module.exports = function (app) { 12 | return { 13 | title: 'ZDA - UTC time and date', 14 | keys: ['navigation.datetime'], 15 | f: function (datetime8601) { 16 | var datetime = new Date(datetime8601) 17 | var hours = ('00' + datetime.getUTCHours()).slice(-2) 18 | var minutes = ('00' + datetime.getUTCMinutes()).slice(-2) 19 | var seconds = ('00' + datetime.getUTCSeconds()).slice(-2) 20 | var day = ('00' + datetime.getUTCDate()).slice(-2) 21 | var month = ('00' + (datetime.getUTCMonth() + 1)).slice(-2) 22 | return nmea.toSentence([ 23 | '$IIZDA', 24 | hours + minutes + seconds + '.020', 25 | day, 26 | month, 27 | datetime.getUTCFullYear(), 28 | '', 29 | '' 30 | ]) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sentences/HDG.js: -------------------------------------------------------------------------------- 1 | /* 2 | Heading magnetic: 3 | $IIHDG,x.x,,,,*hh 4 | I_Heading magnetic 5 | */ 6 | // NMEA0183 Encoder HDG $IIHDG,206.71,,,,*7B 7 | 8 | const nmea = require('../nmea.js') 9 | module.exports = function (app) { 10 | return { 11 | sentence: 'HDG', 12 | title: 'HDG - Heading magnetic:.', 13 | keys: ['navigation.headingMagnetic', 'navigation.magneticVariation' ], 14 | defaults: [undefined, ''], 15 | f: function hdg (headingMagnetic, magneticVariation) { 16 | var magneticVariationDir = '' 17 | if ( magneticVariation != '' ) { 18 | magneticVariationDir = 'E' 19 | if ( headingMagnetic < 0 ) { 20 | magneticVariationDir = 'W' 21 | magneticVariation = Math.abs(magneticVariation) 22 | } 23 | var magneticVariationDeg = nmea.radsToDeg(magneticVariation).toFixed(2) 24 | } 25 | 26 | return nmea.toSentence([ 27 | '$IIHDG', 28 | nmea.radsToDeg(headingMagnetic).toFixed(2), 29 | magneticVariationDeg, 30 | magneticVariationDir, 31 | '', 32 | '' 33 | ]) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sentences/MWVR.js: -------------------------------------------------------------------------------- 1 | /* 2 | === MWV - Wind Speed and Angle === 3 | 4 | ------------------------------------------------------------------------------ 5 | 1 2 3 4 5 6 | | | | | | 7 | $--MWV,x.x,a,x.x,a*hh 8 | ------------------------------------------------------------------------------ 9 | 10 | Field Number: 11 | 12 | 1. Wind Angle, 0 to 360 degrees 13 | 2. Reference, R = Relative, T = True 14 | 3. Wind Speed 15 | 4. Wind Speed Units, K/M/N 16 | 5. Status, A = Data Valid 17 | 6. Checksum 18 | */ 19 | 20 | // NMEA0183 Encoder MWVR $INMWV,35.01,R,7.9,M,A*30 21 | const nmea = require('../nmea.js') 22 | module.exports = function (app) { 23 | return { 24 | sentence: 'MWV', 25 | title: 'MWV - Aparent Wind heading and speed', 26 | keys: ['environment.wind.angleApparent', 'environment.wind.speedApparent'], 27 | f: function (angle, speed) { 28 | return nmea.toSentence([ 29 | '$IIMWV', 30 | nmea.radsToPositiveDeg(angle).toFixed(2), 31 | 'R', 32 | speed.toFixed(2), 33 | 'M', 34 | 'A' 35 | ]) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sentences/PNKEP99.js: -------------------------------------------------------------------------------- 1 | /** test */ 2 | 3 | // to verify 4 | const nmea = require('../nmea.js') 5 | module.exports = function (app) { 6 | return { 7 | title: 'PNKEP,99 - Debug', 8 | keys: [ 9 | 'environment.wind.angleApparent', 10 | 'environment.wind.speedApparent', 11 | 'environment.wind.angleTrue', 12 | 'environment.wind.speedTrue', 13 | 'navigation.speedThroughWater', 14 | 'performance.polarSpeed', 15 | 'performance.polarSpeedRatio' 16 | ], 17 | f: function ( 18 | angleApparent, 19 | speedApparent, 20 | angleTrueWater, 21 | speedTrue, 22 | speedThroughWater, 23 | polarSpeed, 24 | polarSpeedRatio 25 | ) { 26 | // console.log("Got Polar speed --------------------------------------------------"); 27 | return nmea.toSentence([ 28 | '$PNKEP', 29 | '99', 30 | nmea.radsToDeg(angleApparent), 31 | nmea.msToKnots(speedApparent), 32 | nmea.radsToDeg(angleTrueWater), 33 | nmea.msToKnots(speedTrue), 34 | nmea.msToKnots(speedThroughWater), 35 | nmea.msToKnots(polarSpeed), 36 | polarSpeedRatio 37 | ]) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sentences/VTG.js: -------------------------------------------------------------------------------- 1 | /* 2 | Bottom heading and speed: 3 | $IIVTG,x.x,T,x.x,M,x.x,N,x.x,K,A*hh 4 | I I I I I I I__I_Bottom speed in kph 5 | I I I I I__I_Bottom speed in knots 6 | I I I__I_Magnetic bottom heading 7 | I__ I_True bottom heading 8 | */ 9 | // NMEA0183 Encoder VTG $IIVTG,224.17,T,224.17,M,12.95,N,23.98,K,A*3B 10 | const nmea = require('../nmea.js') 11 | module.exports = function (app) { 12 | return { 13 | sentence: 'VTG', 14 | title: 'VTG - Track made good and Ground Speed (COG,SOG)', 15 | keys: [ 16 | 'navigation.courseOverGroundMagnetic', 17 | 'navigation.courseOverGroundTrue', 18 | 'navigation.speedOverGround' 19 | ], 20 | f: function ( 21 | courseOverGroundMagnetic, 22 | courseOverGroundTrue, 23 | speedOverGround 24 | ) { 25 | return nmea.toSentence([ 26 | '$IIVTG', 27 | nmea.radsToDeg(courseOverGroundTrue).toFixed(2), 28 | 'T', 29 | nmea.radsToDeg(courseOverGroundMagnetic).toFixed(2), 30 | 'M', 31 | nmea.msToKnots(speedOverGround).toFixed(2), 32 | 'N', 33 | nmea.msToKM(speedOverGround).toFixed(2), 34 | 'K', 35 | 'A' 36 | ]) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sentences/VWR.js: -------------------------------------------------------------------------------- 1 | /** 2 | Apparent wind angle and speed: 3 | $IIVWR,x.x,a,x.x,N,x.x,M,x.x,K*hh 4 | I I I I I I I__I_Wind speed in kph 5 | I I I I I__I_Wind speed in m/s 6 | I I I__I_Wind speed in knots 7 | I__I_Apparent wind angle from 0° to 180°, L=port, R=starboard 8 | */ 9 | 10 | // NMEA0183 Encoder VWR $IIVWR,42.01,R,14.11,N,7.26,M,26.14,K*75 11 | const nmea = require('../nmea.js') 12 | module.exports = function (app) { 13 | return { 14 | sentence: 'VWR', 15 | optionKey: 'VWR', 16 | title: 'VWR - Apparent wind angle and speed', 17 | keys: ['environment.wind.speedApparent', 'environment.wind.angleApparent'], 18 | f: function (speedApparent, angleApparent) { 19 | var windDirection = 'R' 20 | if (angleApparent < 0) { 21 | angleApparent = -angleApparent 22 | windDirection = 'L' 23 | } 24 | return nmea.toSentence([ 25 | '$IIVWR', 26 | nmea.radsToDeg(angleApparent).toFixed(2), 27 | windDirection, 28 | nmea.msToKnots(speedApparent).toFixed(2), 29 | 'N', 30 | speedApparent.toFixed(2), 31 | 'M', 32 | nmea.msToKM(speedApparent).toFixed(2), 33 | 'K' 34 | ]) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sentences/GLL.js: -------------------------------------------------------------------------------- 1 | /* 2 | Geographical position, latitude and longitude: 3 | $IIGLL,IIII.II,a,yyyyy.yy,a,hhmmss.ss,A,A*hh 4 | I I I I I I_Statut, A= valid data, V= non valid data 5 | I I I I I_UTC time 6 | I I I___ I_Longitude, E/W 7 | I__I_Latidude, N/S 8 | */ 9 | // NMEA0183 Encoder GLL $GPGLL,5943.4970,N,2444.1983,E,200001.020,A*16 10 | 11 | const nmea = require('../nmea.js') 12 | module.exports = function (app) { 13 | return { 14 | sentence: 'GLL', 15 | title: 'GLL - Geographical position, latitude and longitude', 16 | keys: ['navigation.datetime', 'navigation.position'], 17 | f: function gll (datetime8601, position) { 18 | var datetime = new Date(datetime8601) 19 | var hours = ('00' + datetime.getHours()).slice(-2) 20 | var minutes = ('00' + datetime.getMinutes()).slice(-2) 21 | var seconds = ('00' + datetime.getSeconds()).slice(-2) 22 | if (position !== null) { 23 | return nmea.toSentence([ 24 | '$GPGLL', 25 | nmea.toNmeaDegreesLatitude(position.latitude), 26 | nmea.toNmeaDegreesLongitude(position.longitude), 27 | hours + minutes + seconds + '.020', 28 | 'A' 29 | ]) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sentences/MWD.js: -------------------------------------------------------------------------------- 1 | /** 2 | True wind direction and speed: 3 | $IIMWD,x.x,T,x.x,M,x.x,N,x.x,M*hh 4 | I I I I I I I__I_Wind speed in m/s 5 | I I I I I__I_ Wind speed in knots 6 | I I I__I_Wind direction from 0° to 359° magnetic 7 | I__I_Wind direction from 0° to 359° true 8 | 9 | speed Might be ground speed. 10 | */ 11 | 12 | // NMEA0183 Encoder MWD $IIMWD,279.07,T,90.97,M,9.75,N,5.02,M*74 13 | const nmea = require('../nmea.js') 14 | module.exports = function (app) { 15 | return { 16 | sentence: 'MWD', 17 | title: 'MWD - Wind relative to North, speed might be ground speed.', 18 | keys: [ 19 | 'environment.wind.directionTrue', 20 | 'navigation.magneticVariation', 21 | 'environment.wind.speedTrue' 22 | ], 23 | f: function (directionTrue, magneticVariation, speedTrue) { 24 | var directionMagnetic = nmea.fixAngle(directionTrue - magneticVariation) 25 | return nmea.toSentence([ 26 | '$IIMWD', 27 | nmea.radsToDeg(directionTrue).toFixed(2), 28 | 'T', 29 | nmea.radsToDeg(directionMagnetic).toFixed(2), 30 | 'M', 31 | nmea.msToKnots(speedTrue).toFixed(2), 32 | 'N', 33 | speedTrue.toFixed(2), 34 | 'M' 35 | ]) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sentences/VHW.js: -------------------------------------------------------------------------------- 1 | /** 2 | $IIVHW,x .x,T,x.x,M,x.x,N,x.x,K*hh 3 | I I I I I I I__I_Surface speed in kph 4 | I I I I I__I_Surface speed in knots 5 | I I I__I_Magnetic compass heading 6 | I__I_True compass heading 7 | */ 8 | 9 | // NMEA0183 Encoder VHW $IIVHW,201.1,T,209.2,M,6.5,N,12.0,K*6E 10 | 11 | const nmea = require('../nmea.js') 12 | module.exports = function (app) { 13 | return { 14 | sentence: 'VHW', 15 | title: 'VHW - Speed and direction', 16 | keys: [ 17 | 'navigation.headingTrue', 18 | 'navigation.magneticVariation', 19 | 'navigation.speedThroughWater' 20 | ], 21 | f: function vhw (headingTrue, magneticVariation, speedThroughWater) { 22 | var headingMagnetic = headingTrue + magneticVariation 23 | if (headingMagnetic > Math.PI * 2) { 24 | headingMagnetic -= Math.PI * 2 25 | } 26 | if (headingMagnetic < 0) { 27 | headingMagnetic += Math.PI * 2 28 | } 29 | return nmea.toSentence([ 30 | '$IIVHW', 31 | nmea.radsToDeg(headingTrue).toFixed(1), 32 | 'T', 33 | nmea.radsToDeg(headingMagnetic).toFixed(1), 34 | 'M', 35 | nmea.msToKnots(speedThroughWater).toFixed(2), 36 | 'N', 37 | nmea.msToKM(speedThroughWater).toFixed(2), 38 | 'K' 39 | ]) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sentences/RMB.js: -------------------------------------------------------------------------------- 1 | /* 2 | Heading and distance to waypoint: 3 | $IIRMB,A,x.x,a,,,IIII.II,a,yyyyy.yy,a,x.x,x.x,x.x,A,a*hh 4 | I I I I I I I I I_Speed to WP in knots 5 | I I I I I I I I_True heading to destination in degrees 6 | I I I I I I I_Distance to destination in miles 7 | I I I I I_ ___ I_Longitude of the WP to destination, E/W 8 | I I I__ I_Latidude of the WP to destination, N/S 9 | I I_Direction of cross-track error, L/R 10 | I_Distance of cross-track error in miles 11 | */ 12 | // to verify 13 | const nmea = require('../nmea.js') 14 | module.exports = function (app) { 15 | return { 16 | sentence: 'RMB', 17 | title: 'RMB - Heading and distance to waypoint', 18 | keys: [ 19 | 'navigation.course.calcValues.crossTrackError', 20 | 'navigation.course.nextPoint', 21 | 'navigation.course.calcValues.distance', 22 | 'navigation.course.calcValues.bearingTrue' 23 | ], 24 | f: function (crossTrackError, wp, wpDistance, bearingTrue) { 25 | return nmea.toSentence([ 26 | '$IIRMB', 27 | Math.abs(nmea.mToNm(crossTrackError)).toFixed(3), 28 | crossTrackError < 0 ? 'R' : 'L', 29 | nmea.toNmeaDegreesLatitude(wp.position?.latitude), 30 | nmea.toNmeaDegreesLongitude(wp.position?.longitude), 31 | wpDistance.toFixed(2), 32 | nmea.radsToDeg(bearingTrue).toFixed(2), 33 | 'V', // dont set the arrival flag as it will set of alarms. 34 | '' 35 | ]) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/GGA.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const {createAppWithPlugin} = require ('./testutil') 4 | 5 | describe('GGA', function() { 6 | 7 | it('works with default values', done => { 8 | const onEmit = (event, value) => { 9 | assert.equal(value ,'$GPGGA,172814,3723.4659,N,12202.2696,W,0,0,0.0,0.0,M,0.0,M,,*5B') 10 | done() 11 | } 12 | const app = createAppWithPlugin(onEmit, 'GGA') 13 | app.streambundle.getSelfStream('navigation.datetime').push('2015-12-05T17:28:14Z') 14 | app.streambundle.getSelfStream('navigation.position').push({ longitude: -122.03782631066667, latitude: 37.39109795066667 }) 15 | }) 16 | 17 | it('works with sample values', done=> { 18 | const onEmit = (event, value) => { 19 | assert.equal(value ,'$GPGGA,172814,3723.4659,N,12202.2696,W,2,6,1.2,18.9,M,-25.7,M,2,0031*41') 20 | done() 21 | } 22 | const app = createAppWithPlugin(onEmit, 'GGA') 23 | app.streambundle.getSelfStream('navigation.datetime').push('2015-12-05T17:28:14Z') 24 | app.streambundle.getSelfStream('navigation.gnss.methodQuality').push('DGNSS fix') 25 | app.streambundle.getSelfStream('navigation.gnss.satellites').push(6) 26 | app.streambundle.getSelfStream('navigation.gnss.horizontalDilution').push(1.2) 27 | app.streambundle.getSelfStream('navigation.gnss.antennaAltitude').push(18.893) 28 | app.streambundle.getSelfStream('navigation.gnss.geoidalSeparation').push(-25.669) 29 | app.streambundle.getSelfStream('navigation.gnss.differentialAge').push(2.0) 30 | app.streambundle.getSelfStream('navigation.gnss.differentialReference').push('0031') 31 | app.streambundle.getSelfStream('navigation.position').push({ longitude: -122.03782631066667, latitude: 37.39109795066667 }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/MWV.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const { createAppWithPlugin } = require('./testutil') 4 | 5 | describe('MWV relative', function () { 6 | it('works with positive angle', done => { 7 | const onEmit = (event, value) => { 8 | assert.equal(value, '$IIMWV,180.00,R,2.00,M,A*35') 9 | done() 10 | } 11 | const app = createAppWithPlugin(onEmit, 'MWVR') 12 | app.streambundle 13 | .getSelfStream('environment.wind.angleApparent') 14 | .push(Math.PI) 15 | app.streambundle.getSelfStream('environment.wind.speedApparent').push(2) 16 | }) 17 | 18 | it('works with negative angle', done => { 19 | const onEmit = (event, value) => { 20 | assert.equal(value, '$IIMWV,270.00,R,2.00,M,A*39') 21 | done() 22 | } 23 | const app = createAppWithPlugin(onEmit, 'MWVR') 24 | app.streambundle 25 | .getSelfStream('environment.wind.angleApparent') 26 | .push(-Math.PI / 2) 27 | app.streambundle.getSelfStream('environment.wind.speedApparent').push(2) 28 | }) 29 | }) 30 | 31 | describe('MWV true', function () { 32 | it('works with positive angle', done => { 33 | const onEmit = (event, value) => { 34 | assert.equal(value, '$IIMWV,180.00,T,2.00,M,A*33') 35 | done() 36 | } 37 | 38 | const app = createAppWithPlugin(onEmit, 'MWVT') 39 | app.streambundle 40 | .getSelfStream('environment.wind.angleTrueWater') 41 | .push(Math.PI) 42 | app.streambundle.getSelfStream('environment.wind.speedTrue').push(2) 43 | }) 44 | 45 | it('works with negative angle', done => { 46 | const onEmit = (event, value) => { 47 | assert.equal(value, '$IIMWV,270.00,T,2.00,M,A*3F') 48 | done() 49 | } 50 | 51 | const app = createAppWithPlugin(onEmit, 'MWVT') 52 | app.streambundle 53 | .getSelfStream('environment.wind.angleTrueWater') 54 | .push(-Math.PI / 2) 55 | app.streambundle.getSelfStream('environment.wind.speedTrue').push(2) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/nmea.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { toNmeaDegreesLatitude, toNmeaDegreesLongitude } = require('../nmea.js') 3 | 4 | describe('nmea', function () { 5 | describe('toNmeaDegreesLatitude()', function(){ 6 | it('convert correctly to Degrees and Decimal Minutes in format DDMM.MMMM', function(){ 7 | assert.equal(toNmeaDegreesLatitude(0),'0000.0000,N') 8 | assert.equal(toNmeaDegreesLatitude(0.016668333333333334),'0001.0001,N') 9 | assert.equal(toNmeaDegreesLatitude(-1.016668333333333334),'0101.0001,S') 10 | assert.equal(toNmeaDegreesLatitude(1.016668333333333334),'0101.0001,N') 11 | assert.throws(function(){toNmeaDegreesLatitude(-99.999)}, Error, 'expected Error') 12 | assert.throws(function(){toNmeaDegreesLatitude(100)}, Error, 'expected Error') 13 | assert.throws(function(){toNmeaDegreesLatitude('23.333')}, Error, 'expected Error') 14 | assert.throws(function(){toNmeaDegreesLatitude(undefined)}, Error, 'expected Error') 15 | assert.throws(function(){toNmeaDegreesLatitude('hello world')}, Error, 'expected Error') 16 | }) 17 | }) 18 | describe('toNmeaDegreesLongitude()', function(){ 19 | it('convert correctly to Degrees and Decimal Minutes in format DDDMM.MMMM', function(){ 20 | assert.equal(toNmeaDegreesLongitude(0),'00000.0000,E') 21 | assert.equal(toNmeaDegreesLongitude(0.016668333333333334),'00001.0001,E') 22 | assert.equal(toNmeaDegreesLongitude(-1.016668333333333334),'00101.0001,W') 23 | assert.equal(toNmeaDegreesLongitude(1.016668333333333334),'00101.0001,E') 24 | assert.equal(toNmeaDegreesLongitude(-99.9999983333333333),'09959.9999,W') 25 | assert.equal(toNmeaDegreesLongitude(-122.4208),'12225.2480,W') 26 | assert.throws(function(){toNmeaDegreesLongitude(-181)}, Error, 'expected Error') 27 | assert.throws(function(){toNmeaDegreesLongitude(197)}, Error, 'expected Error') 28 | assert.throws(function(){toNmeaDegreesLongitude('-122')}, Error, 'expected Error') 29 | assert.throws(function(){toNmeaDegreesLongitude(undefined)}, Error, 'expected Error') 30 | assert.throws(function(){toNmeaDegreesLongitude('hello world')}, Error, 'expected Error') 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/RMC.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const {createAppWithPlugin} = require ('./testutil') 4 | 5 | describe('RMC', function () { 6 | it('works without datetime & magneticVariation', done => { 7 | const onEmit = (event, value) => { 8 | assert.equal(value, '$GPRMC,,A,0600.0000,N,00500.0000,E,1.9,114.6,,,E*51') 9 | done() 10 | } 11 | const app = createAppWithPlugin(onEmit, 'RMC') 12 | app.streambundle.getSelfStream('navigation.speedOverGround').push('1') 13 | app.streambundle.getSelfStream('navigation.courseOverGroundTrue').push('2') 14 | app.streambundle 15 | .getSelfStream('navigation.position') 16 | .push({ longitude: 5, latitude: 6 }) 17 | }) 18 | 19 | it('works with large longitude & magnetic variation', done => { 20 | const onEmit = (event, value) => { 21 | assert.equal(value, '$GPRMC,,A,3749.6038,N,12225.2480,W,1.9,114.6,,180.0,E*6B') 22 | done() 23 | } 24 | const app = createAppWithPlugin(onEmit, 'RMC') 25 | app.streambundle.getSelfStream('navigation.speedOverGround').push('1') 26 | app.streambundle.getSelfStream('navigation.courseOverGroundTrue').push('2') 27 | app.streambundle.getSelfStream('navigation.magneticVariation').push(Math.PI) 28 | app.streambundle 29 | .getSelfStream('navigation.position') 30 | .push({ longitude: -122.4208, latitude: 37.82673 }) 31 | }) 32 | 33 | it('ignores a too large longitude', done => { 34 | const onEmit = (event, value) => { 35 | assert.equal(value, '$GPRMC,,A,3749.6038,N,12225.2480,W,1.9,114.6,,,E*4C') 36 | done() 37 | } 38 | const app = createAppWithPlugin(onEmit, 'RMC') 39 | app.streambundle.getSelfStream('navigation.speedOverGround').push('1') 40 | app.streambundle.getSelfStream('navigation.courseOverGroundTrue').push('2') 41 | app.streambundle 42 | .getSelfStream('navigation.position') 43 | .push({ longitude: -222.4208, latitude: 37.82673 }) 44 | //output is debounce(20), so wait a little our output makes it through 45 | setTimeout(() => { 46 | app.streambundle 47 | .getSelfStream('navigation.position') 48 | .push({ longitude: -122.4208, latitude: 37.82673 }) 49 | }, 50) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # signalk-to-nmea0183 2 | Signal K Node server plugin to convert Signal K to NMEA 0183. See the code for a list of supported sentences. 3 | 4 | To use the plugin you need to activate the plugin and the relevant sentences in server's Admin interface. This will make the conversion results (NMEA 0183) available on Signalk's built-in TCP NMEA 0183 server (Port 10110). 5 | 6 | As the plugin automatically sends NMEA 0183 data to Signalk's built-in TCP NMEA 0183 server, it is possible to have access to the NMEA 0183 strings without configuring anything (Aka a serial output device) by connecting to port 10110 with a TCP client (e.g. OpenCPN, Netcat, kplex etc) 7 | 8 | If you want to output the conversion result into a serial connection you need to configure the serial connection in the server's Admin interface and add an extra line to the `settings.json`, specifying that the serial connection should output the plugin's output: 9 | 10 | 11 | ``` 12 | { 13 | "pipedProviders": [ 14 | { 15 | "pipeElements": [ 16 | { 17 | "type": "providers/simple", 18 | "options": { 19 | "logging": false, 20 | "type": "NMEA0183", 21 | "subOptions": { 22 | "validateChecksum": true, 23 | "type": "serial", 24 | "suppress0183event": true, 25 | "providerId": "a", 26 | "device": "/dev/ttyExample", 27 | "baudrate": 4800, 28 | "toStdout": "nmea0183out" <------------ ADD THIS LINE 29 | }, 30 | "providerId": "a" 31 | } 32 | } 33 | ], 34 | "id": "example", 35 | "enabled": true 36 | } 37 | ], 38 | "interfaces": {} 39 | } 40 | ``` 41 | 42 | Note: Internally the plugin emits the converted NMEA 0183 messages as `Events` under the event identifier `nmea0183out`. The above configuration sends the converted data (NMEA 0183) under the `nmea0183out` events identifier to the serialport's output. 43 | 44 | Troubleshooting: If you cannot connect to Signalk's built-in TCP NMEA 0183 server, ensure it is enabled. To verify the TCP NMEA 0183 server is enabled go to Signalk's Dashboard, then Server->Settings->Interfaces->nmea-tcp . 45 | 46 | ![image](https://user-images.githubusercontent.com/1049678/63366888-64283700-c383-11e9-9a5f-7f9975e007f3.png) 47 | 48 | -------------------------------------------------------------------------------- /sentences/APB.js: -------------------------------------------------------------------------------- 1 | /* 2 | ------------------------------------------------------------------------------ 3 | 13 15 4 | 1 2 3 4 5 6 7 8 9 10 11 12| 14| 5 | | | | | | | | | | | | | | | | 6 | $--APB,A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a*hh 7 | ------------------------------------------------------------------------------ 8 | 9 | Field Number: 10 | 11 | 1. Status 12 | V = LORAN-C Blink or SNR warning 13 | V = general warning flag or other navigation systems when a reliable 14 | fix is not available 15 | 2. Status 16 | V = Loran-C Cycle Lock warning flag 17 | A = OK or not used 18 | 3. Cross Track Error Magnitude 19 | 4. Direction to steer, L or R 20 | 5. Cross Track Units, N = Nautical Miles 21 | 6. Status 22 | A = Arrival Circle Entered 23 | 7. Status 24 | A = Perpendicular passed at waypoint 25 | 8. Bearing origin to destination 26 | 9. M = Magnetic, T = True 27 | 10. Destination Waypoint ID 28 | 11. Bearing, present position to Destination 29 | 12. M = Magnetic, T = True 30 | 13. Heading to steer to destination waypoint 31 | 14. M = Magnetic, T = True 32 | 15. Checksum 33 | 34 | Example: $GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*82 35 | */ 36 | // to verify 37 | const nmea = require('../nmea.js') 38 | module.exports = function (app) { 39 | return { 40 | sentence: 'APB', 41 | title: 'APB - Autopilot info', 42 | keys: [ 43 | 'navigation.course.calcValues.crossTrackError', 44 | 'navigation.course.calcValues.bearingTrackTrue', 45 | 'navigation.course.calcValues.bearingTrue', 46 | 'navigation.course.calcValues.bearingMagnetic' 47 | ], 48 | f: function (xte, originToDest, bearingTrue, bearingMagnetic) { 49 | return nmea.toSentence([ 50 | '$IIAPB', 51 | 'A', 52 | 'A', 53 | Math.abs(nmea.mToNm(xte)).toFixed(3), // NMEA 0183 4.11 prescribes units must be the Nautical miles 54 | xte > 0 ? 'L' : 'R', 55 | 'N', 56 | 'V', 57 | 'V', 58 | nmea.radsToPositiveDeg(originToDest).toFixed(0), 59 | 'T', 60 | '00', 61 | nmea.radsToPositiveDeg(bearingTrue).toFixed(0), 62 | 'T', 63 | nmea.radsToPositiveDeg(bearingMagnetic).toFixed(0), 64 | 'M' 65 | ]) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sentences/RMC.js: -------------------------------------------------------------------------------- 1 | /* 2 | RMC - Recommended Minimum Navigation Information 3 | This is one of the sentences commonly emitted by GPS units. 4 | 5 | 12 6 | 1 2 3 4 5 6 7 8 9 10 11| 13 7 | | | | | | | | | | | | | | 8 | $--RMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,xxxx,x.x,a,m,*hh 9 | Field Number: 10 | 1 UTC Time 11 | 2 Status, V=Navigation receiver warning A=Valid 12 | 3 Latitude 13 | 4 N or S 14 | 5 Longitude 15 | 6 E or W 16 | 7 Speed over ground, knots 17 | 8 Track made good, degrees true 18 | 9 Date, ddmmyy 19 | 10 Magnetic Variation, degrees 20 | 11 E or W 21 | 12 FAA mode indicator (NMEA 2.3 and later) 22 | 13 Checksum 23 | */ 24 | // This needs to run faster that others. 25 | 26 | // NMEA0183 Encoder RMC $INRMC,200152.020,A,5943.2980,N,2444.1043,E,6.71,194.30,0000,8.1,E*40 27 | const { toSentence, toNmeaDegreesLatitude, toNmeaDegreesLongitude, radsToDeg } = require('../nmea.js') 28 | module.exports = function (app) { 29 | return { 30 | sentence: 'RMC', 31 | title: 'RMC - GPS recommended minimum', 32 | keys: [ 33 | 'navigation.datetime', 34 | 'navigation.speedOverGround', 35 | 'navigation.courseOverGroundTrue', 36 | 'navigation.position', 37 | 'navigation.magneticVariation' 38 | ], 39 | defaults: ['', undefined, undefined, undefined, ''], 40 | f: function (datetime8601, sog, cog, position, magneticVariation) { 41 | let time = '' 42 | let date = '' 43 | if (datetime8601.length > 0) { 44 | let datetime = new Date(datetime8601) 45 | let hours = ('00' + datetime.getUTCHours()).slice(-2) 46 | let minutes = ('00' + datetime.getUTCMinutes()).slice(-2) 47 | let seconds = ('00' + datetime.getUTCSeconds()).slice(-2) 48 | 49 | let day = ('00' + datetime.getUTCDate()).slice(-2) 50 | let month = ('00' + (datetime.getUTCMonth() + 1)).slice(-2) // months from 1-12 51 | let year = ('00' + datetime.getUTCFullYear()).slice(-2) 52 | time = hours + minutes + seconds 53 | date = day + month + year 54 | } 55 | var magneticVariationDir = 'E' 56 | if ( magneticVariation < 0 ) { 57 | magneticVariationDir = 'W'; 58 | magneticVariation = magneticVariation * -1; 59 | } 60 | return toSentence([ 61 | '$GPRMC', 62 | time, 63 | 'A', 64 | toNmeaDegreesLatitude(position.latitude), 65 | toNmeaDegreesLongitude(position.longitude), 66 | (sog * 1.94384).toFixed(1), 67 | radsToDeg(cog).toFixed(1), 68 | date, 69 | typeof magneticVariation === 'number' ? radsToDeg(magneticVariation).toFixed(1) : magneticVariation, 70 | magneticVariationDir 71 | ]) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### v1.6.0 (2019/09/06 15:05 +00:00) 4 | - [#42](https://github.com/SignalK/signalk-to-nmea0183/pull/42) fix: compatibility with navionics which only accepts $GP for RMC (@cmotelet) 5 | 6 | ### v1.5.1 (2019/08/24 05:29 +00:00) 7 | - [#40](https://github.com/SignalK/signalk-to-nmea0183/pull/40) Fix GGA: gnssMethodQuality is numeric (@free-x) 8 | - [#38](https://github.com/SignalK/signalk-to-nmea0183/pull/38) docs: clarify default TCP output (@GaryWSmith) 9 | 10 | ### v1.5.0 (2019/05/15 19:27 +00:00) 11 | - [#35](https://github.com/SignalK/signalk-to-nmea0183/pull/35) chore: Use $II as a talker ID to extend compatibility with other software (@cmotelet) 12 | 13 | ### v1.4.0 (2019/03/20 18:21 +00:00) 14 | - [#31](https://github.com/SignalK/signalk-to-nmea0183/pull/31) Add support for GGA sentence (@fabdrol) 15 | 16 | ### v1.3.1 (2019/03/16 19:14 +00:00) 17 | - [#33](https://github.com/SignalK/signalk-to-nmea0183/pull/33) MWV angle should be always positive (@tkurki) 18 | 19 | ### v1.3.0 (2018/10/08 05:49 +00:00) 20 | - [#28](https://github.com/SignalK/signalk-to-nmea0183/pull/28) Rudder Sensor Angle (@Dirk--) 21 | 22 | ### v1.2.1 (2018/09/24 17:14 +00:00) 23 | - [#27](https://github.com/SignalK/signalk-to-nmea0183/pull/27) fix: use environment.wind.angleTrueWater (@tkurki) 24 | 25 | ### v1.2.0 (2018/08/23 16:15 +00:00) 26 | - [#16](https://github.com/SignalK/signalk-to-nmea0183/pull/16) feature: Add back Silva/Nexus/Garmin proprietary sentences TBS and CD1 (@joabakk) 27 | - [#22](https://github.com/SignalK/signalk-to-nmea0183/pull/22) Fix: Set time of position fix time to UTC as defined in RMC sentence (@davidsanner) 28 | - [#23](https://github.com/SignalK/signalk-to-nmea0183/pull/23) Rename XDRNA,js to XDRNA.js (@davidsanner) 29 | 30 | ### v1.1.0 (2018/05/08 20:15 +00:00) 31 | - [#21](https://github.com/SignalK/signalk-to-nmea0183/pull/21) fix RMC sentence, add Variation to HDG setence (@davidsanner) 32 | 33 | ### v1.0.2 (2018/05/07 04:20 +00:00) 34 | - [#20](https://github.com/SignalK/signalk-to-nmea0183/pull/20) fix: RMC for >99 degrees, error handling (@tkurki) 35 | 36 | ### v1.0.0 (2018/04/08 20:05 +00:00) 37 | - [#17](https://github.com/SignalK/signalk-to-nmea0183/pull/17) Create XDRNA.js (@CaptainRon47) 38 | - [#13](https://github.com/SignalK/signalk-to-nmea0183/pull/13) fix: use blank as the default when magneticVariation is missing (@tkurki) 39 | - [#14](https://github.com/SignalK/signalk-to-nmea0183/pull/14) Fix undefined default check (@tkurki) 40 | 41 | ### v0.0.2 (2017/12/31 08:22 +00:00) 42 | - [#12](https://github.com/SignalK/signalk-to-nmea0183/pull/12) fix: remove magneticVariation from RMC sentence (@sbender9) 43 | 44 | ### v0.1.0 (2017/10/09 18:58 +00:00) 45 | - [#10](https://github.com/SignalK/signalk-to-nmea0183/pull/10) Split sentences to files and add conversions (@tkurki) 46 | - [#9](https://github.com/SignalK/signalk-to-nmea0183/pull/9) Add mechanism for providing default values, fix RMC datetime, add test (@tkurki) 47 | - [#7](https://github.com/SignalK/signalk-to-nmea0183/pull/7) Fix sentence format for picky parsers like Isailor/Android (@netAction) 48 | - [#4](https://github.com/SignalK/signalk-to-nmea0183/pull/4) Fix convertion to celsius for MTW sentence (@sbender9) 49 | - [#2](https://github.com/SignalK/signalk-to-nmea0183/pull/2) Add more NMEA 0183 conversions (@sbender9) 50 | - [#3](https://github.com/SignalK/signalk-to-nmea0183/pull/3) Add real lat/lon to RMC sentence (@sbender9) 51 | - [#1](https://github.com/SignalK/signalk-to-nmea0183/pull/1) Added proprietary Silva/Nexus/Garmin sentences for displaying perform… (@joabakk) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Bacon = require('baconjs') 2 | const { 3 | toSentence, 4 | computeChecksum, 5 | toHexString, 6 | radsToDeg, 7 | padd, 8 | toNmeaDegrees 9 | } = require('./nmea') 10 | const path = require('path') 11 | const fs = require('fs') 12 | 13 | module.exports = function (app) { 14 | var plugin = { 15 | unsubscribes: [] 16 | } 17 | 18 | plugin.id = 'sk-to-nmea0183' 19 | plugin.name = 'Convert Signal K to NMEA0183' 20 | plugin.description = 'Plugin to convert Signal K to NMEA0183' 21 | 22 | plugin.schema = { 23 | type: 'object', 24 | title: 'Conversions to NMEA0183', 25 | description: 26 | 'If there is SK data for the conversion generate the following NMEA0183 sentences from Signal K data. For converting NMEA2000 AIS to NMEA 0183 use the signalk-n2kais-to-nmea0183 plugin.', 27 | properties: {} 28 | } 29 | 30 | plugin.start = function (options) { 31 | const selfContext = 'vessels.' + app.selfId 32 | const selfMatcher = delta => delta.context && delta.context === selfContext 33 | 34 | function mapToNmea (encoder, throttle) { 35 | const selfStreams = encoder.keys.map((key, index) => { 36 | let stream = app.streambundle.getSelfStream(key) 37 | if (encoder.defaults && typeof encoder.defaults[index] != 'undefined') { 38 | stream = stream.merge(Bacon.once(encoder.defaults[index])) 39 | } 40 | return stream 41 | }, app.streambundle) 42 | const sentenceEvent = encoder.sentence ? `g${encoder.sentence}` : undefined 43 | 44 | let stream = Bacon.combineWith(function () { 45 | try { 46 | return encoder.f.apply(this, arguments) 47 | } catch (e) { 48 | console.error(e.message) 49 | } 50 | }, selfStreams) 51 | .filter(v => typeof v !== 'undefined') 52 | .changes() 53 | .debounceImmediate(20) 54 | 55 | if (throttle) { 56 | stream = stream.throttle(throttle) 57 | } 58 | 59 | plugin.unsubscribes.push( 60 | stream 61 | .onValue(nmeaString => { 62 | if ( app.reportOutputMessages ) { 63 | app.reportOutputMessages(1) 64 | } 65 | app.emit('nmea0183out', nmeaString) 66 | if (sentenceEvent) { 67 | app.emit(sentenceEvent, nmeaString) 68 | } 69 | app.debug(nmeaString) 70 | }) 71 | ) 72 | } 73 | 74 | Object.keys(plugin.sentences).forEach(name => { 75 | if (options[name]) { 76 | mapToNmea(plugin.sentences[name], options[getThrottlePropname(name)]) 77 | } 78 | }) 79 | } 80 | 81 | plugin.stop = function () { 82 | plugin.unsubscribes.forEach(f => f()) 83 | } 84 | 85 | plugin.sentences = loadSentences(app, plugin) 86 | buildSchemaFromSentences(plugin) 87 | return plugin 88 | } 89 | 90 | function buildSchemaFromSentences (plugin) { 91 | Object.keys(plugin.sentences).forEach(key => { 92 | var sentence = plugin.sentences[key] 93 | const throttlePropname = getThrottlePropname(key) 94 | plugin.schema.properties[key] = { 95 | title: sentence['title'], 96 | type: 'boolean', 97 | default: false 98 | } 99 | plugin.schema.properties[throttlePropname] = { 100 | title: `${key} throttle ms`, 101 | type: 'number', 102 | default: 0 103 | } 104 | }) 105 | } 106 | 107 | function loadSentences (app, plugin) { 108 | const fpath = path.join(__dirname, 'sentences') 109 | return fs 110 | .readdirSync(fpath) 111 | .filter(filename => filename.endsWith('.js')) 112 | .reduce((acc, fname) => { 113 | let sentence = path.basename(fname, '.js') 114 | acc[sentence] = require(path.join(fpath, sentence))(app, plugin) 115 | return acc 116 | }, {}) 117 | } 118 | 119 | const getThrottlePropname = (key) => `${key}_throttle` 120 | -------------------------------------------------------------------------------- /nmea.js: -------------------------------------------------------------------------------- 1 | const m_hex = [ 2 | '0', 3 | '1', 4 | '2', 5 | '3', 6 | '4', 7 | '5', 8 | '6', 9 | '7', 10 | '8', 11 | '9', 12 | 'A', 13 | 'B', 14 | 'C', 15 | 'D', 16 | 'E', 17 | 'F' 18 | ] 19 | 20 | function toSentence (parts) { 21 | var base = parts.join(',') 22 | return base + computeChecksum(base) 23 | } 24 | 25 | function computeChecksum (sentence) { 26 | // skip the $ 27 | let i = 1 28 | // init to first character 29 | let c1 = sentence.charCodeAt(i) 30 | // process rest of characters, zero delimited 31 | for (i = 2; i < sentence.length; ++i) { 32 | c1 = c1 ^ sentence.charCodeAt(i) 33 | } 34 | return '*' + toHexString(c1) 35 | } 36 | 37 | function toHexString (v) { 38 | let msn = (v >> 4) & 0x0f 39 | let lsn = (v >> 0) & 0x0f 40 | return m_hex[msn] + m_hex[lsn] 41 | } 42 | 43 | function radsToDeg (radians) { 44 | return radians * 180 / Math.PI 45 | } 46 | 47 | function msToKnots (v) { 48 | return v * 3600 / 1852.0 49 | } 50 | 51 | function msToKM (v) { 52 | return v * 3600.0 / 1000.0 53 | } 54 | 55 | function mToNm (v) { 56 | return v * 0.000539957 57 | } 58 | 59 | function padd (n, p, c) { 60 | let pad_char = typeof c !== 'undefined' ? c : '0' 61 | let pad = new Array(1 + p).join(pad_char) 62 | return (pad + n).slice(-pad.length) 63 | } 64 | 65 | function decimalDegreesToDegreesAndDecimalMinutes ( degrees ) { 66 | /* 67 | decimalDegreesToDegreesAndDecimalMinutes takes a float (degrees) 68 | representing decimal degrees and returns a tuple [deg, min, dir], where 69 | deg is an int representing degrees, min is a float representing decimal 70 | minutes and dir is a positive or negative integer representing the 71 | direction from the origin ( +1 for N and E, -1 for S and W ) 72 | 73 | NOTE: 0 degrees is N or E 74 | */ 75 | 76 | let dir=1 // default to N or E 77 | 78 | if (degrees<0) { 79 | dir = -1 80 | degrees *= -1 81 | } 82 | 83 | let degrees_out = Math.floor(degrees) 84 | let minutes = (degrees % 1) * 60 85 | return [ degrees_out, minutes, dir ] 86 | } 87 | 88 | function toNmeaDegreesLatitude (inVal) { 89 | /* 90 | toNmeaDegreesLatitude takes a float (inVal) representing decimal degrees 91 | and returns a string formatted as degrees and decimal minutes suitable for 92 | use in an NMEA0183 sentence. (e.g. DDMM.MMMM) 93 | */ 94 | 95 | if (typeof inVal != 'number' || inVal < -90 || inVal > 90) { 96 | throw new Error("invalid input to toNmeaDegreesLatitude: " + inVal) 97 | } 98 | 99 | let [degrees, minutes, dir] = decimalDegreesToDegreesAndDecimalMinutes(inVal) 100 | 101 | return( 102 | padd(degrees.toFixed(0), 2) 103 | + padd(minutes.toFixed(4), 7) 104 | + "," + (dir > 0 ? "N" : "S") 105 | ) 106 | } 107 | 108 | function toNmeaDegreesLongitude (inVal) { 109 | /* 110 | toNmeaDegreesLongitude takes a float (inVal) representing decimal degrees 111 | and returns a string formatted as degrees and decimal minutes suitable for 112 | use in an NMEA0183 sentence. (e.g. DDDMM.MMMM) 113 | */ 114 | 115 | if (typeof inVal != 'number' || inVal <= -180 || inVal > 180) { 116 | throw new Error("invalid input to toNmeaDegreesLongitude: " + inVal) 117 | } 118 | 119 | let [degrees, minutes, dir] = decimalDegreesToDegreesAndDecimalMinutes(inVal) 120 | 121 | return( 122 | padd(degrees.toFixed(0), 3) 123 | + padd(minutes.toFixed(4), 7) 124 | + "," + (dir > 0 ? "E" : "W") 125 | ) 126 | } 127 | 128 | function fixAngle (d) { 129 | let result = d 130 | if (d > Math.PI) result -= 2 * Math.PI 131 | if (d < -Math.PI) result += 2 * Math.PI 132 | return result 133 | } 134 | 135 | function toPositiveRadians (d) { 136 | return d < 0 ? d + 2 * Math.PI : d 137 | } 138 | 139 | function radsToPositiveDeg(r) { 140 | return radsToDeg(toPositiveRadians(r)) 141 | } 142 | 143 | module.exports = { 144 | toSentence: toSentence, 145 | radsToDeg: radsToDeg, 146 | msToKnots: msToKnots, 147 | msToKM: msToKM, 148 | toNmeaDegreesLatitude: toNmeaDegreesLatitude, 149 | toNmeaDegreesLongitude: toNmeaDegreesLongitude, 150 | fixAngle: fixAngle, 151 | radsToPositiveDeg, 152 | mToNm 153 | } 154 | -------------------------------------------------------------------------------- /sentences/GGA.js: -------------------------------------------------------------------------------- 1 | /* 2 | GGA - Time, position, and fix related data 3 | This is one of the sentences commonly emitted by GPS units. 4 | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 5 | | | | | | | | | | | | | | | | 6 | $GPGGA,172814.0,3723.46587704,N,12202.26957864,W,2,6,1.2,18.893,M,-25.669,M,2.0,0031*hh 7 | 8 | Field Number: 9 | 0 Message ID $GPGGA 10 | 1 UTC of position fix 11 | 2 Latitude 12 | 3 Direction of latitude: N (north) or S (south) 13 | 4 Longitude 14 | 5 Direction of longitude: E (east) or W (west) 15 | 6 GPS Quality indicator: 0 = Fix not valid; 1 = GPS fix; 2 = Differential GPS fix, OmniSTAR VBS; 4 = Real-Time Kinematic, fixed integers; 5 = Real-Time Kinematic, float integers, OmniSTAR XP/HP or Location RTK 16 | 7 Number of SVs in use, range from 00 through to 24+ 17 | 8 HDOP 18 | 9 Orthometric height (MSL reference) 19 | 10 M: unit of measure for orthometric height is meters 20 | 11 Geoid separation 21 | 12 M: geoid separation measured in meters 22 | 13 Age of differential GPS data record, Type 1 or Type 9. Null field when DGPS is not used. 23 | 14 Reference station ID, range 0000-4095. A null field when any reference station ID is selected and no corrections are received 24 | */ 25 | 26 | const { 27 | toSentence, 28 | toNmeaDegreesLatitude, 29 | toNmeaDegreesLongitude 30 | } = require('../nmea.js') 31 | 32 | module.exports = function (app) { 33 | return { 34 | sentence: 'GGA', 35 | title: 'GGA - Time, position, and fix related data', 36 | keys: [ 37 | 'navigation.datetime', 38 | 'navigation.position', 39 | 'navigation.gnss.methodQuality', 40 | 'navigation.gnss.satellites', 41 | 'navigation.gnss.horizontalDilution', 42 | 'navigation.gnss.antennaAltitude', 43 | 'navigation.gnss.geoidalSeparation', 44 | 'navigation.gnss.differentialAge', 45 | 'navigation.gnss.differentialReference' 46 | ], 47 | defaults: [ 48 | null, // navigation.datetime 49 | null, // navigation.position 50 | 0, // navigation.gnss.methodQuality (= GPS Quality indicator: 0 = Fix not valid; 1 = GPS fix; 2 = Differential GPS fix, OmniSTAR VBS; 4 = Real-Time Kinematic, fixed integers; 5 = Real-Time Kinematic, float integers, OmniSTAR XP/HP or Location RTK) 51 | 0, // navigation.gnss.satellites (= Number of SVs in use, range from 00 through to 24+) 52 | 0, // navigation.gnss.horizontalDilution (= HDOP) 53 | 0, // navigation.gnss.antennaAltitude (= Orthometric height (MSL reference)) 54 | 0, // navigation.gnss.geoidalSeparation (= Geoid separation), 55 | null, // navigation.gnss.differentialAge (= Age of differential GPS data record, Type 1 or Type 9. Null field when DGPS is not used) 56 | null // navigation.gnss.differentialReference (= Reference station ID, range 0000-4095. A null field when any reference station ID is selected and no corrections are received) 57 | ], 58 | f: function (datetime8601, position, gnssMethodQuality, gnssSatellites, gnssHorizontalDilution, gnssAntennaAltitude, gnssgeoidalSeparation, gnssDifferentialAge, gnssDifferentialReference) { 59 | let time = '' 60 | let ignssMethodQuality = 0 61 | 62 | if (!datetime8601 || (typeof datetime8601 === 'string' && datetime8601.trim() === '')) { 63 | datetime8601 = new Date().toISOString() 64 | } 65 | 66 | if (datetime8601.length > 0) { 67 | let datetime = new Date(datetime8601) 68 | let hours = ('00' + datetime.getUTCHours()).slice(-2) 69 | let minutes = ('00' + datetime.getUTCMinutes()).slice(-2) 70 | let seconds = ('00' + datetime.getUTCSeconds()).slice(-2) 71 | time = hours + minutes + seconds 72 | } 73 | 74 | if (!position) { 75 | console.error(`[signalk-to-nmea0183] GGA: no position, not converting`) 76 | return 77 | } 78 | 79 | if (!gnssDifferentialAge) { 80 | gnssDifferentialAge = '' 81 | } 82 | 83 | if (!gnssDifferentialReference) { 84 | gnssDifferentialReference = '' 85 | } 86 | 87 | switch (gnssMethodQuality) { 88 | case 'no GPS' : 89 | ignssMethodQuality = 0 90 | break 91 | case 'GNSS Fix' : 92 | ignssMethodQuality = 1 93 | break 94 | case 'DGNSS fix' : 95 | ignssMethodQuality = 2 96 | break 97 | case 'Precise GNSS' : 98 | ignssMethodQuality = 3 99 | break 100 | case 'RTK fixed integer' : 101 | ignssMethodQuality = 4 102 | break 103 | case 'RTK float' : 104 | ignssMethodQuality = 5 105 | break 106 | case 'Estimated (DR) mode' : 107 | ignssMethodQuality = 6 108 | break 109 | case 'Manual input' : 110 | ignssMethodQuality = 7 111 | break 112 | case 'Simulator mode' : 113 | ignssMethodQuality = 8 114 | break 115 | } 116 | 117 | return toSentence([ 118 | '$GPGGA', 119 | time, 120 | toNmeaDegreesLatitude(position.latitude), 121 | toNmeaDegreesLongitude(position.longitude), 122 | ignssMethodQuality, 123 | gnssSatellites, 124 | gnssHorizontalDilution.toFixed(1), 125 | gnssAntennaAltitude.toFixed(1), 126 | 'M', 127 | gnssgeoidalSeparation.toFixed(1), 128 | 'M', 129 | gnssDifferentialAge, 130 | gnssDifferentialReference 131 | ]) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------