From b8122b6dbe5754b0be0c5af65820aa692e51b5a5 Mon Sep 17 00:00:00 2001 From: Teppo Kurki Date: Wed, 31 Mar 2021 22:18:03 +0300 Subject: [PATCH] Custom sentence parser mechanism (#193) * fix: do not require source to be there There is no need to require that individual sentence parsers set source in their output. This change allows source to not be there. This makes adding custom sentence parsers a little more robust, as the exception from a missing source was silently ignored. * feature: add custom sentence parser mechanism --- .gitignore | 3 +- README.md | 9 +++++ bin/nmea0183-signalk | 4 +- custom-sentence-plugin/index.js | 34 ++++++++++++++++ custom-sentence-plugin/package.json | 14 +++++++ lib/index.js | 37 ++++++++++++++--- lib/transformSource.js | 2 +- test/customSentenceParser.js | 61 +++++++++++++++++++++++++++++ 8 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 custom-sentence-plugin/index.js create mode 100644 custom-sentence-plugin/package.json create mode 100644 test/customSentenceParser.js 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..f4b63b23 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 `nmea0183sentenceParser` 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/lib/transformSource.js b/lib/transformSource.js index 741ba5cc..943199aa 100644 --- a/lib/transformSource.js +++ b/lib/transformSource.js @@ -22,7 +22,7 @@ module.exports = function transformSource(data, sentence, talker) { return update } - const _source = update.source + const _source = update.source || '' const tagSentence = _source.split(':')[1] let tagTalker = _source.split(':')[0] 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) + }) +})