diff --git a/README.md b/README.md index 8b6be097..1ef6960e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ - [HSC - Heading Steering Command](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) - KEP - NKE Performance data - [MDA - Meteorological Composite](https://gpsd.gitlab.io/gpsd/NMEA.html#_mda_meteorilogical_composite) +- [MTA - Mean Temperature of Air](https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf) - [MTW - Mean Temperature of Water](https://gpsd.gitlab.io/gpsd/NMEA.html#_mtw_mean_temperature_of_water) +- [MWD - Wind Speed and Direction](https://lists.nongnu.org/archive/html/gpsd-dev/2012-04/msg00048.html) - [MWV - Wind Speed and Angle](https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle) - [RMB - Recommended Minimum Navigation Information](https://gpsd.gitlab.io/gpsd/NMEA.html#_rmb_recommended_minimum_navigation_information) - [RMC - Recommended Minimum Navigation Information](https://gpsd.gitlab.io/gpsd/NMEA.html#_rmc_recommended_minimum_navigation_information) @@ -38,6 +40,7 @@ - [VPW - Speed - Measured Parallel to Wind](https://gpsd.gitlab.io/gpsd/NMEA.html#_vpw_speed_measured_parallel_to_wind) - [VTG - Track Made Good and Ground Speed](https://gpsd.gitlab.io/gpsd/NMEA.html#_vtg_track_made_good_and_ground_speed) - [VWR - Relative Wind Speed and Angle](https://gpsd.gitlab.io/gpsd/NMEA.html#_vwr_relative_wind_speed_and_angle) +- [VWT - True Wind Angle and Speed](https://lists.nongnu.org/archive/html/gpsd-dev/2012-04/msg00048.html) - [ZDA - UTC day, month, and year, and local time zone offset](https://gpsd.gitlab.io/gpsd/NMEA.html#_zda_time_amp_date_utc_day_month_year_and_local_time_zone) - [XTE - Cross-track Error](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) - [ZDA - UTC day, month, and year, and local time zone offset](http://www.trimble.com/oem_receiverhelp/v4.44/en/NMEA-0183messages_ZDA.html) diff --git a/hooks/MTA.js b/hooks/MTA.js new file mode 100644 index 00000000..b898efd6 --- /dev/null +++ b/hooks/MTA.js @@ -0,0 +1,57 @@ +/** + * Copyright 2016 Signal K and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + + const utils = require('@signalk/nmea0183-utilities') + + /* + * MTA - Mean Temperature of Air + * + * 0 1 2 + * | | | + * $--MTA,x.x,C*hh + * + * Field Number: + * 0. Degrees + * 1. Unit of Measurement, Celcius + * 2. Checksum + * + */ + +module.exports = function (input) { + const { id, sentence, parts, tags } = input; + + if(parts[1] != 'C') { + return null; + } + const delta = { + updates: [ + { + source: tags.source, + timestamp: tags.timestamp, + values: [ + { + path: 'environment.outside.temperature', + value: utils.transform(utils.float(parts[0]), 'c', 'k') + } + ] + } + ], + } + + return delta +} diff --git a/hooks/MWD.js b/hooks/MWD.js new file mode 100644 index 00000000..7538b9be --- /dev/null +++ b/hooks/MWD.js @@ -0,0 +1,93 @@ +'use strict' + +/** + * Copyright 2016 Signal K and Fabian Tollenaar . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const debug = require('debug')('signalk-parser-nmea0183/MWD') +const utils = require('@signalk/nmea0183-utilities') + +/* + * $WIMWD,<0>,<1>,<2>,<3>,<4>,<5>,<6>,<7>*hh + * + * NMEA 0183 standard Wind Direction and Speed, with respect to north. + * + * <0> Wind direction, 0.0 to 359.9 degrees True, to the nearest 0.1 degree + * <1> T = True + * <2> Wind direction, 0.0 to 359.9 degrees Magnetic, to the nearest 0.1 degree + * <3> M = Magnetic + * <4> Wind speed, knots, to the nearest 0.1 knot. + * <5> N = Knots + * <6> Wind speed, meters/second, to the nearest 0.1 m/s. + * <7> M = Meters/second + */ + +module.exports = function(input) { + const { id, sentence, parts, tags } = input; + var pathValues = []; + + // get direction data: + // return both, true and magnetic direction, if present in the NMEA sentence + var haveDirection = false; + if(parts[0] != '' && parts[1] == 'T') { + haveDirection = true; + pathValues.push({ + 'path': 'environment.wind.directionTrue', + 'value': utils.transform(utils.float(parts[0]), 'deg', 'rad') + }) + } + if(parts[2] != '' && parts[3] == 'M') { + haveDirection = true; + pathValues.push({ + 'path': 'environment.wind.directionMagnetic', + 'value': utils.transform(utils.float(parts[2]), 'deg', 'rad') + }) + } + if( !haveDirection ) { + return null; + } + + // get speed data: + // speed given in kn is used in case no speed in m/s is present in the NMEA sentence + var haveSpeed = false; + var speed; + if(parts[4] != '' && parts[5] == 'N') { + haveSpeed = true; + speed = utils.transform(utils.float(parts[4]), 'knots', 'ms'); + } + if(parts[6] != '' && parts[7] == 'M') { + haveSpeed = true; + speed = utils.float(parts[6]); + } + if( !haveSpeed ) { + return null; + } + pathValues.push({ + 'path': 'environment.wind.speedTrue', + 'value': speed + }) + + const delta = { + updates: [ + { + source: tags.source, + timestamp: tags.timestamp, + values: pathValues + } + ], + } + + return delta +} diff --git a/hooks/VWT.js b/hooks/VWT.js new file mode 100644 index 00000000..424f46a4 --- /dev/null +++ b/hooks/VWT.js @@ -0,0 +1,116 @@ +'use strict' + +/** + * Copyright 2016 Signal K and Fabian Tollenaar . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const debug = require('debug')('signalk-parser-nmea0183/VWT') +const utils = require('@signalk/nmea0183-utilities') + +/* + * $WIVWT,<0>,<1>,<2>,<3>,<4>,<5>,<6>,<7>*hh + * + * NMEA 0183 True wind angle in relation to the vessel's heading, and true wind + * speed referenced to the water. True wind is the vector sum of the Relative + * (apparent) wind vector and the vessel's velocity vector relative to the water along + * the heading line of the vessel. It represents the wind at the vessel if it were + * stationary relative to the water and heading in the same direction. + * + * <0> Calculated wind angle relative to the vessel, 0 to 180°, left/right of + * vessel heading, to the nearest 0.1 degree + * <1> L = left, or R = right + * <2> Calculated wind speed, knots, to the nearest 0.1 knot + * <3> N = knots + * <4> Wind speed, meters per second, to the nearest 0.1 m/s + * <5> M = meters per second + * <6> Wind speed, km/h, to the nearest km/h + * <7> K = km/h + */ + +// $IIVWT,030.,R,10.1,N,05.2,M,018.7,K*75 + + +function convertToWindAngle(angle) { + const numAngle = utils.float(angle) % 360 + if (numAngle > 180 && numAngle <= 360) { + return numAngle - 360 + } + return numAngle +} + +module.exports = function(input) { + const { id, sentence, parts, tags } = input; + + // get direction + if(!parts[0]) { + return null; + } + var angle = convertToWindAngle(parts[0]); + + if(!parts[1]) { + return null; + } + switch( parts[1] ) { + case 'L': + angle = -1 * utils.transform( angle, 'deg', 'rad'); + break; + case 'R': + angle = utils.transform( angle, 'deg', 'rad'); + break; + default: + return null; + } + + // get speed data: + // speed value given in m/s is given precedence if present in the NMEA sentence + var haveSpeed = false; + var speed; + if(parts[2] != '' && parts[3] == 'N') { + haveSpeed = true; + speed = utils.transform(utils.float(parts[2]), 'knots', 'ms'); + } + if(parts[6] != '' && parts[7] == 'K') { + haveSpeed = true; + speed = utils.transform(utils.float(parts[6]), 'kph', 'ms'); + } + if(parts[4] != '' && parts[5] == 'M') { // overwrite speed from knots or km/h if present + haveSpeed = true; + speed = utils.float(parts[4]); + } + if( !haveSpeed ) { + return null; + } + + const delta = { + updates: [ + { + source: tags.source, + timestamp: tags.timestamp, + values: [ + { + path: 'environment.wind.speedTrue', + value: speed + }, + { + path: 'environment.wind.angleTrueWater', + value: angle + } + ] + } + ], + } + + return delta +} diff --git a/hooks/index.js b/hooks/index.js index d0c68f21..5083c5cb 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -14,7 +14,9 @@ module.exports = { 'PBVE': require('./proprietary/PBVE.js'), 'PNKEP': require('./proprietary/PNKEP.js'), 'MDA': require('./MDA.js'), + 'MTA': require('./MTA.js'), 'MTW': require('./MTW.js'), + 'MWD': require('./MWD.js'), 'MWV': require('./MWV.js'), 'RMB': require('./RMB.js'), 'RMC': require('./RMC.js'), @@ -29,6 +31,7 @@ module.exports = { 'VPW': require('./VPW.js'), 'VTG': require('./VTG.js'), 'VWR': require('./VWR.js'), + 'VWT': require('./VWT.js'), 'ZDA': require('./ZDA.js'), 'XTE': require('./XTE.js'), 'BOD': require('./BOD.js'), diff --git a/test/MTA.js b/test/MTA.js new file mode 100644 index 00000000..5902909b --- /dev/null +++ b/test/MTA.js @@ -0,0 +1,38 @@ +/** + * Copyright 2016 Signal K and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const Parser = require('../lib') +const chai = require('chai') +const should = chai.Should() + +chai.use(require('chai-things')) + +describe('MTA', () => { + it('Converts OK using individual parser', () => { + const delta = new Parser().parse('$IIMTA,26.,C*31') + + delta.updates[0].values[0].path.should.equal('environment.outside.temperature') + delta.updates[0].values[0].value.should.be.closeTo(299.15, 0.005) + }) + it('Does not accept units other than Celsius', () => { + var should = chai.should(); + const delta = new Parser().parse('$IIMTA,26.,F*34') + + should.equal(delta,null); + }) +}) diff --git a/test/MWD.js b/test/MWD.js new file mode 100644 index 00000000..d180326b --- /dev/null +++ b/test/MWD.js @@ -0,0 +1,109 @@ +/** + * Copyright 2016 Signal K and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const Parser = require('../lib') +const chai = require('chai') +const should = chai.Should() + +chai.use(require('chai-things')) + +describe('MWD', () => { + + it('speed & direction data (#1)', () => { + const delta = new Parser().parse('$IIMWD,,,046.,M,10.1,N,05.2,M*0B') + + delta.updates[0].values[0].path.should.equal('environment.wind.directionMagnetic') + delta.updates[0].values[0].value.should.be.closeTo(0.802851, 0.00005) + delta.updates[0].values[1].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[1].value.should.equal(5.2) + }) + + it('speed & direction data (#2)', () => { + const delta = new Parser().parse('$IIMWD,046.,T,046.,M,10.1,N,,*17') + + delta.updates[0].values[0].path.should.equal('environment.wind.directionTrue') + delta.updates[0].values[0].value.should.be.closeTo(0.802851, 0.00005) + delta.updates[0].values[1].path.should.equal('environment.wind.directionMagnetic') + delta.updates[0].values[1].value.should.be.closeTo(0.802851, 0.00005) + delta.updates[0].values[2].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[2].value.should.be.closeTo(5.2, 0.005) + }) + + it('speed & direction data (#3)', () => { + const delta = new Parser().parse('$IIMWD,046.,T,,,,,5.2,M*72') + + delta.updates[0].values[0].path.should.equal('environment.wind.directionTrue') + delta.updates[0].values[0].value.should.be.closeTo(0.802851, 0.00005) + delta.updates[0].values[1].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[1].value.should.be.equal(5.2) + }) + + it('missing direction data', () => { + const delta = new Parser().parse('$IIMWD,,,,,,,5.2,M*3A') + + should.equal(delta,null); + }) + + it('missing speed data', () => { + const delta = new Parser().parse('$IIMWD,,,046.,M,,,,*0F') + + should.equal(delta,null); + }) + + it('improper direction designator (#1)', () => { + const delta = new Parser().parse('$IIMWD,,,046.,T,,,,*16') + + should.equal(delta,null); + }) + + it('improper direction designator (#2)', () => { + const delta = new Parser().parse('$IIMWD,046.,M,,,,,,*0F') + + should.equal(delta,null); + }) + + it('improper speed designator (#1)', () => { + const delta = new Parser().parse('$IIMWD,,,046.,M,10.1,n,,*7F') + + should.equal(delta,null); + }) + + it('improper speed designator (#2)', () => { + const delta = new Parser().parse('$IIMWD,,,046.,M,,,0.0,m*4C') + + should.equal(delta,null); + }) + + it('improper direction designator for degrees magnetic, using degrees true', () => { + const delta = new Parser().parse('$IIMWD,046.,T,0.,m,10.1,N,5.2,M*51') + + delta.updates[0].values[0].path.should.equal('environment.wind.directionTrue') + delta.updates[0].values[0].value.should.be.closeTo(0.802851, 0.00005) + delta.updates[0].values[1].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[1].value.should.equal(5.2) + }) + + it('improper speed designator for m/s, using speed in kn', () => { + const delta = new Parser().parse('$IIMWD,,,046.,M,10.1,N,0.0,m*1C') + + delta.updates[0].values[0].path.should.equal('environment.wind.directionMagnetic') + delta.updates[0].values[0].value.should.be.closeTo(0.802851, 0.00005) + delta.updates[0].values[1].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[1].value.should.be.closeTo(5.2, 0.005) + }) +}) diff --git a/test/VWT.js b/test/VWT.js new file mode 100644 index 00000000..d1779170 --- /dev/null +++ b/test/VWT.js @@ -0,0 +1,129 @@ +'use strict' + +const Parser = require('../lib') +const chai = require('chai') +const should = chai.Should() + +chai.use(require('chai-things')) + +describe('VWT', () => { + it('speed & direction data (#1)', () => { + const delta = new Parser().parse('$IIVWT,030.,R,10.1,N,05.2,M,018.7,K*75') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.equal(5.2) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(0.523599, 0.005) + }) + + it('speed & direction data (#2)', () => { + const delta = new Parser().parse('$IIVWT,180.,R,10.1,N,05.2,M,018.7,K*7F') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.equal(5.2) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(3.1415, 0.005) + }) + + it('speed & direction data (#3)', () => { // 190R = 170L + const delta = new Parser().parse('$IIVWT,190.,R,10.1,N,05.2,M,018.7,K*7E') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.equal(5.2) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(-2.967, 0.005) + }) + + it('speed & direction data (#4)', () => { + const delta = new Parser().parse('$IIVWT,170.,L,10.1,N,05.2,M,018.7,K*6E') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.equal(5.2) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(-2.967, 0.005) + }) + + it('speed & direction data (#5)', () => { // 170 + 360 = 530 + const delta = new Parser().parse('$IIVWT,530.,L,10.1,N,05.2,M,018.7,K*6E') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.equal(5.2) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(-2.967, 0.005) + }) + + it('missing speed data (#1)', () => { + const delta = new Parser().parse('$IIVWT,030.,R,,N,05.2,M,018.7,*20') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.equal(5.2) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(0.523599, 0.005) + }) + + it('missing speed data (#2)', () => { + const delta = new Parser().parse('$IIVWT,030.,R,10.1,N,,,018.7,K*21') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.be.closeTo(5.2, 0.05) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(0.523599, 0.005) + }) + + it('missing speed data (#3)', () => { + const delta = new Parser().parse('$IIVWT,030.,R,10.1,N,,,,K*01') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.be.closeTo(5.2, 0.05) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(0.523599, 0.005) + }) + + it('missing direction data (#1)', () => { + const delta = new Parser().parse('$IIVWT,,R,10.1,N,,,,K*1C') + + should.equal(delta,null); + }) + + it('missing direction data (#2)', () => { + const delta = new Parser().parse('$IIVWT,0.0,,10.1,N,,,,K*60') + + should.equal(delta,null); + }) + + it('improper direction designator (#1)', () => { + const delta = new Parser().parse('$IIVWT,030.,r,10.1,N,05.2,M,018.7,K*55') + + should.equal(delta,null); + }) + + it('improper speed designator (#1)', () => { + const delta = new Parser().parse('$IIVWT,030.,R,10.1,X,05.2,M,018.7,X*70') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.equal(5.2) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(0.523599, 0.005) + }) + + it('improper speed designator (#2)', () => { + const delta = new Parser().parse('$IIVWT,030.,R,10.1,N,05.2,X,018.7,K*60') + + delta.updates[0].values[0].path.should.equal('environment.wind.speedTrue') + delta.updates[0].values[0].value.should.be.closeTo(5.2, 0.05) + delta.updates[0].values[1].path.should.equal('environment.wind.angleTrueWater') + delta.updates[0].values[1].value.should.be.closeTo(0.523599, 0.005) + }) + + it('improper speed designator (#3)', () => { + const delta = new Parser().parse('$IIVWT,030.,R,10.1,X,05.2,X,018.7,X*65') + + should.equal(delta,null); + }) + + it('Doesn\'t choke on empty sentences', () => { + const delta = new Parser().parse('$IIVWT,,,,*55') + should.equal(delta, null) + }) + +})