diff --git a/hooks/PASHR.js b/hooks/PASHR.js new file mode 100644 index 0000000..ef4ee2c --- /dev/null +++ b/hooks/PASHR.js @@ -0,0 +1,87 @@ +/** + * Copyright 2019 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/PASHR') +const utils = require('@signalk/nmea0183-utilities') + +/** + * $PASHR,225444.123,125.12,T,12.45,11.91,xxx.xx,1.123,1.456,2.653,1,4*38 + * + * Roll and pitch information - https://help.fieldsystems.trimble.com/r780/nmea0183-messages-pashr.htm + * + * 0) 225444.123 UTC time of fix 22:54:44.123 + * 1,2) 125.12,T True Heading, flag to indicate that the Heading is True Heading + * 3) 12.45 Roll Angle in degrees + * 4) 11.91 Pitch Angle in degrees + * 5) xxx.xx Reserved Field (Not Supported) + * 6) 1.123 Roll Angle accuracy estimate in degrees + * 7) 1.456 Pitch Angle accuracy estimate in degrees + * 8) 2.653 Heading Angle accuracy estimate in degrees + * 9) 1 GNSS Quality [0-6] + * 10) 4 IMU Alignment Status [0-4] + **/ + + +module.exports = function PASHRHook(input) { + const { id, sentence, parts, tags } = input + + debug(`[PASHRHook] decoding sentence ${id} => ${sentence}`) + + let state = { + timestamp: utils.timestamp(parts[0]), + trueHeading: parts[2] == 'T' ? parts[1] : null, + rollAngle: parts[3], + pitchAngle: parts[4], + rollAngleAccuracy: parts[6], + pitchAngleAccuracy: parts[7], + trueHeadingAccuracy: parts[8], + } + + return { + updates: [ + { + timestamp: state.timestamp, + source: tags.source, + values: [ + { + path: 'navigation.headingTrue', + value: state.trueHeading ? utils.transform(utils.float(state.trueHeading), 'deg', 'rad') : null, + }, + { + path: 'balance.rollAngle', + value: state.rollAngle ? utils.transform(utils.float(state.rollAngle), 'deg', 'rad') : null, + }, + { + path: 'balance.pitchAngle', + value: state.pitchAngle ? utils.transform(utils.float(state.pitchAngle), 'deg', 'rad') : null, + }, + { + path: 'balance.rollAngleAccuracy', + value: state.rollAngle && state.rollAngleAccuracy ? utils.transform(utils.float(state.rollAngleAccuracy), 'deg', 'rad') : null, + }, + { + path: 'balance.pitchAngleAccuracy', + value: state.pitchAngle && state.pitchAngleAccuracy ? utils.transform(utils.float(state.pitchAngleAccuracy), 'deg', 'rad') : null, + }, + { + path: 'navigation.headingTrueAccuracy', + value: state.trueHeading && state.trueHeadingAccuracy ? utils.transform(utils.float(state.trueHeadingAccuracy), 'deg', 'rad') : null, + } + ], + }, + ], + } +} diff --git a/hooks/index.js b/hooks/index.js index 2c2ca4b..0305446 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -41,4 +41,5 @@ module.exports = { BWC: require('./BWC.js'), BWR: require('./BWR.js'), HSC: require('./HSC.js'), + PASHR: require('./PASHR.js') } diff --git a/test/PASHR.js b/test/PASHR.js new file mode 100644 index 0000000..551618d --- /dev/null +++ b/test/PASHR.js @@ -0,0 +1,187 @@ +/** + * Copyright 2025 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. + */ + +'use strict' + +const Parser = require('../lib') +const chai = require('chai') +const { expect } = require('chai') +const utils = require('@signalk/nmea0183-utilities') + +chai.Should() + +chai.use(require('chai-things')) + +// { +// path: 'navigation.headingTrue', +// value: utils.transform(utils.float(state.trueHeading), 'deg', 'rad'), +// }, +// { +// path: 'balance.rollAngle', +// value: utils.transform(utils.float(state.rollAngle), 'deg', 'rad'), +// }, +// { +// path: 'balance.pitchAngle', +// value: utils.transform(utils.float(state.pitchAngle), 'deg', 'rad'), +// }, +// { +// path: 'balance.rollAngleAccuracy', +// value: utils.float(state.rollAngleAccuracy), +// }, +// { +// path: 'balance.pitchAngleAccuracy', +// value: utils.float(state.pitchAngleAccuracy), +// } + + +describe('PASHR', () => { + it('Should understand full trame', () => { + const delta = new Parser().parse('$PASHR,225444.123,125.12,T,12.45,11.91,xxx.xx,1.123,1.456,2.653,1,4*56') + delta.should.be.an('object') + + testAllKeys(delta) + + // headingTrue + delta.updates[0].values[0].value.should.be.closeTo( + 2, 18166156, + 0.01 + ) + + // rollAngle + delta.updates[0].values[1].value.should.be.closeTo( + 0, 2712, + 0.01 + ) + + // pitchAngle + delta.updates[0].values[2].value.should.be.closeTo( + 0, 2078, + 0.01 + ) + + // rollAngleAccuracy + delta.updates[0].values[3].value.should.be.closeTo( + 0.0196, + 0.01 + ) + + // pitchAngleAccuracy + delta.updates[0].values[4].value.should.be.closeTo( + 0.0254, + 0.01 + ) + + // headingTrueAccuracy + delta.updates[0].values[5].value.should.be.closeTo( + 0.0463, + 0.01 + ) + }) + + + it('Should dont set heading if the flag isnt present', () => { + const delta = new Parser().parse('$PASHR,225444.123,125.12,,12.45,11.91,xxx.xx,1.123,1.456,2.653,1,4*02') + delta.should.be.an('object') + + testAllKeys(delta) + + // headingTrue + expect(delta.updates[0].values[0].value).to.be.null + + // rollAngle + delta.updates[0].values[1].value.should.be.closeTo( + 0, 2712, + 0.01 + ) + + // pitchAngle + delta.updates[0].values[2].value.should.be.closeTo( + 0, 2078, + 0.01 + ) + + // rollAngleAccuracy + delta.updates[0].values[3].value.should.be.closeTo( + 0.0196, + 0.01 + ) + + // pitchAngleAccuracy + delta.updates[0].values[4].value.should.be.closeTo( + 0.0254, + 0.01 + ) + + // headingTrueAccuracy + expect(delta.updates[0].values[5].value).to.be.null + }) + + + it('Should handle all null values', () => { + const delta = new Parser().parse('$PASHR,225444.123,,,,,,,,,,*6b') + delta.should.be.an('object') + + testAllKeys(delta) + + // headingTrue + expect(delta.updates[0].values[0].value).to.be.null + + // rollAngle + expect(delta.updates[0].values[1].value).to.be.null + + + // pitchAngle + expect(delta.updates[0].values[2].value).to.be.null + + + // rollAngleAccuracy + expect(delta.updates[0].values[3].value).to.be.null + + + // pitchAngleAccuracy + expect(delta.updates[0].values[4].value).to.be.null + + + // headingTrueAccuracy + expect(delta.updates[0].values[5].value).to.be.null + }) + +}) + + +function testAllKeys(delta) { + delta.updates[0].values.should.contain.an.item.with.property( + "path", + 'navigation.headingTrue', + ) + + delta.updates[0].values.should.contain.an.item.with.property( + "path", + 'balance.rollAngle', + ) + delta.updates[0].values.should.contain.an.item.with.property( + "path", + 'balance.pitchAngle', + ) + delta.updates[0].values.should.contain.an.item.with.property( + "path", + 'balance.rollAngleAccuracy', + ) + delta.updates[0].values.should.contain.an.item.with.property( + "path", + 'balance.pitchAngleAccuracy', + ) +} \ No newline at end of file