Skip to content

Commit

Permalink
feature: add custom sentence parser mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
tkurki committed Mar 27, 2021
1 parent eebf181 commit 01bfecc
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 8 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ package-lock.json
.yarn-integrity

.DS_Store
*.db
*.db

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 `` 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": "teppo.kurki@iki.fi",
"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
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 <teppo.kurki@iki.fi>
*
* 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 01bfecc

Please sign in to comment.