Skip to content

Commit

Permalink
Custom sentence parser mechanism (#193)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tkurki authored Mar 31, 2021
1 parent b09574f commit b8122b6
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ package-lock.json
.DS_Store
*.db

.vscode
.vscode
custom-sentence-plugin/package-lock.json
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion bin/nmea0183-signalk
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
34 changes: 34 additions & 0 deletions custom-sentence-plugin/index.js
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 14 additions & 0 deletions custom-sentence-plugin/package.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"license": "Apache-2.0"
}
37 changes: 31 additions & 6 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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`)
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
2 changes: 1 addition & 1 deletion lib/transformSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
61 changes: 61 additions & 0 deletions test/customSentenceParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright 2021 Signal K and Teppo Kurki <[email protected]>
*
* 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)
})
})

0 comments on commit b8122b6

Please sign in to comment.