diff --git a/.gitignore b/.gitignore index 7c43a4fe..ea543828 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,5 @@ package-lock.json .DS_Store *.db -.vscode \ No newline at end of file +.vscode +custom-sentence-plugin/package-lock.json diff --git a/README.md b/README.md index 6c7e73a6..38ee567e 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,18 @@ - [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) +- [Custom Sentences](#custom-sentences) **Note:** *at this time, unknown sentences will be silently discarded.* +### Custom Sentences + +You can add custom sentence parsers via the [Signal K Server plugin mechanism](https://github.com/SignalK/signalk-server/blob/master/SERVERPLUGINS.md). A plugin can register custom parsers by emitting `` PropertyValues with a value that has the properties +- sentence: the three letter id of the sentence +- parser: a function with the signature `({ id, sentence, parts, tags }, session) => delta` + +See [custom-sentence-plugin](./custom-sentence-plugin) for an example. + ## Usage ### JavaScript API diff --git a/bin/nmea0183-signalk b/bin/nmea0183-signalk index efd80751..8fc3d0d9 100755 --- a/bin/nmea0183-signalk +++ b/bin/nmea0183-signalk @@ -1,7 +1,9 @@ #!/usr/bin/env node const Parser = require('../lib/index.js') -const parser = new Parser() +const parser = new Parser({ + validateChecksum: false +}) process.stdin.resume() process.stdin.setEncoding('utf8') diff --git a/custom-sentence-plugin/index.js b/custom-sentence-plugin/index.js new file mode 100644 index 00000000..ac2548ef --- /dev/null +++ b/custom-sentence-plugin/index.js @@ -0,0 +1,34 @@ +/** + * To test the plugin + * - install the plugin (for example with npm link) + * - activate the plugin + * - add an UDP NMEA0183 connection to the server + * - send data via udp + * echo '$IIXXX,1,2,3,foobar,D*17' | nc -u -w 0 127.0.0.1 7777 + */ + +module.exports = function (app) { + const plugin = {} + plugin.id = plugin.name = plugin.description = 'signalk-nmea0183-custom-sentence-plugin' + + plugin.start = function () { + app.emitPropertyValue('nmea0183sentenceParser', { + sentence: 'XXX', + parser: ({ id, sentence, parts, tags }, session) => { + return { + updates: [ + { + values: [ + { path: 'navigation.speedOverGround', value: Number(parts[0]) } + ] + } + ] + } + } + }) + } + + plugin.stop = function () { } + plugin.schema = {} + return plugin +} \ No newline at end of file diff --git a/custom-sentence-plugin/package.json b/custom-sentence-plugin/package.json new file mode 100644 index 00000000..73b124ce --- /dev/null +++ b/custom-sentence-plugin/package.json @@ -0,0 +1,14 @@ +{ + "name": "signalk-nmea0183-custom-sentence-plugin", + "version": "1.0.0", + "description": "Example of a plugin for Signal K Server that adds a custom sentence to the NMEA0183 parser", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "signalk-node-server-plugin" + ], + "author": "teppo.kurki@iki.fi", + "license": "Apache-2.0" +} diff --git a/lib/index.js b/lib/index.js index f609c998..547a6dea 100644 --- a/lib/index.js +++ b/lib/index.js @@ -19,11 +19,15 @@ const getTagBlock = require('./getTagBlock') const transformSource = require('./transformSource') const utils = require('@signalk/nmea0183-utilities') -const hooks = require('../hooks') +const defaultHooks = require('../hooks') const pkg = require('../package.json') +const debug = require('debug')('signalk-parser-nmea0183') class Parser { - constructor (opts) { + constructor(opts = { + emitPropertyValue: () => undefined, + onPropertyValues: () => undefined + }) { this.options = (typeof opts === 'object' && opts !== null) ? opts : {} if (!Object.keys(this.options).includes('validateChecksum')) { this.options.validateChecksum = true @@ -34,9 +38,22 @@ class Parser { this.version = pkg.version this.author = pkg.author this.license = pkg.license + this.hooks = { ...defaultHooks } + + opts.onPropertyValues && opts.onPropertyValues('nmea0183sentenceParser', propertyValues_ => { + if (propertyValues_ === undefined) { + return + } + const propValues = propertyValues_.filter(v => v) + .map(propValue => propValue.value) + .filter(isValidSentenceParserEntry) + .map(({ sentence, parser }) => { + debug(`setting custom parser ${sentence}`) + this.hooks[sentence] = parser }) + }) } - parse (sentence) { + parse(sentence) { let tags = getTagBlock(sentence) if (tags !== false) { @@ -51,7 +68,7 @@ class Parser { } let valid = utils.valid(sentence, this.options.validateChecksum) - + if (valid === false) { throw new Error(`Sentence "${sentence.trim()}" is invalid`) } @@ -84,8 +101,8 @@ class Parser { tags.source = `${tags.source}:${id}` } - if (typeof hooks[internalId] === 'function') { - const result = hooks[internalId]({ + if (typeof this.hooks[internalId] === 'function') { + const result = this.hooks[internalId]({ id, sentence, parts: split, @@ -98,4 +115,12 @@ class Parser { } } +function isValidSentenceParserEntry(entry) { + const isValid = typeof entry.sentence === 'string' && typeof entry.parser === 'function' + if (!isValid) { + console.error(`Invalid sentence parser entry:${JSON.stringify(entry)}`) + } + return isValid +} + module.exports = Parser diff --git a/test/customSentenceParser.js b/test/customSentenceParser.js new file mode 100644 index 00000000..7b415fe8 --- /dev/null +++ b/test/customSentenceParser.js @@ -0,0 +1,61 @@ +/** + * Copyright 2021 Signal K and Teppo Kurki + * + * 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 Parser = require('../lib') +const chai = require('chai') +const { expect } = require('chai') +const should = chai.Should() +chai.use(require('chai-things')) + + +describe('Custom Sentence Parser', () => { + it('works', () => { + const TEST_SENTENCE_PARTS = ['1', '2', '3', 'foobar', 'D'] + const TEST_CUSTOM_SENTENCE = `$IIXXX,${TEST_SENTENCE_PARTS.join(',')}*17` + const DELTA = { + updates: [ + { + values: [ + { path: 'a.b.c', value: 3.14 } + ] + } + ] + } + let onPropValuesCallCount = 0 + const options = { + onPropertyValues: (propertyName, cb) => { + onPropValuesCallCount++ + cb(undefined) + cb([{ + value: { + sentence: 'XXX', + parser: ({ id, sentence, parts, tags }, session) => { + id.should.equal('XXX') + sentence.should.equal(TEST_CUSTOM_SENTENCE) + parts.should.have.members(TEST_SENTENCE_PARTS) + expect(typeof session).to.equal('object') + return DELTA + } + } + }]) + } + } + const parser = new Parser(options) + onPropValuesCallCount.should.equal(1) + const delta = parser.parse(TEST_CUSTOM_SENTENCE) + delta.should.deep.equal(DELTA) + }) +})