diff --git a/hooks/XDR.js b/hooks/XDR.js new file mode 100644 index 00000000..b29eb824 --- /dev/null +++ b/hooks/XDR.js @@ -0,0 +1,160 @@ +// $IIXDR,C,C,10.7,C,AIRTEMP,A,0.5,D,HEEL,A,-1.-3,D,TRIM,P,1.026,B,BARO,A,A,-4.-3,D,RUDDER*18 + +const math = require('math-expression-evaluator'); +const fs = require('fs'); +const xdrDictionary = { definitions: [] }; + +try { + // Populate a dictionary + const xdrDictPath = require.resolve('xdr-parser-plugin/xdrDict'); + if (fs.existsSync(xdrDictPath)) { + try { + const json = JSON.parse(fs.readFileSync(xdrDictPath, 'utf-8')); + + if (json && Array.isArray(json.definitions)) { + xdrDictionary.definitions = [ ...json.definitions ]; + } + } catch (err) { + console.warn('No dictionary found for xdr-parser-plugin'); + } + } +} catch (e) { + console.warn('Using default dictionary'); +} + +xdrDictionary.definitions = [ + ...xdrDictionary.definitions, + { + type: "value", + data: "temperature", + units: "C", + name: "AIRTEMP", + expression: "(x+273.15)", + sk_path: "environment.outside.temperature", + }, + { + type: "roll", + data: "angle", + units: "D", + name: "HEEL", + expression: "(x*pi/180)", + sk_path: "navigation.attitude" + }, + { + type: "value", + data: "angle", + units: "D", + name: "RUDDER", + expression: "(x*pi/180)", + sk_path: "steering.rudderAngle" + } +] + +module.exports = function (input) { + if (!Array.isArray(xdrDictionary.definitions)) { + return null; + } + + const isUpperCaseChar = (p, minLen = 0) => { + const num = parseFloat(p) + return (isNaN(num) && typeof p === 'string' && p.length > minLen && p.toUpperCase() === p) + } + + const { definitions } = xdrDictionary + const { parts } = input + const subs = {} + const boundaries = parts.slice(1).filter(p => isUpperCaseChar(p, 1)) + const values = [] + + for (const boundary of boundaries) { + const index = boundaries.indexOf(boundary); + const prevBoundary = index === 0 ? null : boundaries[index - 1]; + const elements = []; + let fill = false; + + for (p of parts.slice(1)) { + if (!fill && (!prevBoundary || p === prevBoundary) && elements.length === 0) { + fill = true; + } + + if (fill === false || p === boundary) { + fill = false; + continue + } + + if (p !== prevBoundary) { + elements.push(p); + } + } + + if (elements.length) { + subs[boundary] = elements + } + } + + for (const boundary of boundaries) { + const data = subs[boundary] + + let typeFlag = null + let value = null + let unit = null + + if (data.length === 3) { + ([ typeFlag, value, unit ] = data); + } + + if (value === null || typeFlag === null || unit === null) { + continue + } + + if (isNaN(parseFloat(value))) { + continue + } + + const def = definitions.find(({ name }) => (name === boundary)); + + if (!def) { + continue; + } + + if (def.units !== unit) { + // Not parsing as unit doesn't match + continue; + } + + const expression = def.expression.replace(/x/g, value); + const attitudeTypes = ['yaw', 'pitch', 'roll']; + + let path = def.sk_path; + let result = math.eval(expression); + + if (!result || isNaN(result)) { + continue + } + + result = parseFloat(result.toFixed(4)) + + if (attitudeTypes.includes(def.type.toLowerCase())) { + path = `${path}.${def.type}` + } + + values.push({ + path, + value: result + }) + } + + if (values.length === 0) { + return null; + } + + return { + updates: [ + { + source: 'XDR', + timestamp: new Date().toISOString(), + values, + }, + ], + } +} \ No newline at end of file diff --git a/hooks/index.js b/hooks/index.js index b2044687..253531f4 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -40,4 +40,5 @@ module.exports = { BWC: require('./BWC.js'), BWR: require('./BWR.js'), HSC: require('./HSC.js'), + XDR: require('./XDR.js'), } diff --git a/package.json b/package.json index 122bbeaf..6b17b56f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@signalk/nmea0183-utilities": "^0.8.0", "@signalk/signalk-schema": "1.6.0", "ggencoder": "^1.0.4", + "math-expression-evaluator": "^1.4.0", "moment-timezone": "^0.5.21", "split": "^1.0.1" }, diff --git a/test/XDR.js b/test/XDR.js new file mode 100644 index 00000000..ea3c77f1 --- /dev/null +++ b/test/XDR.js @@ -0,0 +1,27 @@ +// $IIXDR,C,C,10.7,C,AIRTEMP,A,0.5,D,HEEL,A,-1.-3,D,TRIM,P,1.026,B,BARO,A,A,-4.-3,D,RUDDER*18 + +const Parser = require('../lib') + +const chai = require('chai') +const expect = chai.expect + +chai.Should() +chai.use(require('chai-things')) + +describe('XDR', () => { + it('Converts OK using individual parser', () => { + const delta = new Parser().parse('$IIXDR,C,C,10.7,C,AIRTEMP,A,0.5,D,HEEL,A,-1.-3,D,TRIM,P,1.026,B,BARO,A,A,-4.-3,D,RUDDER*18') + + delta.updates[0].values.should.contain.an.item.with.property( + 'path', + 'environment.outside.temperature' + ) + delta.updates[0].values[0].value.should.be.closeTo(283.85, 0.005) + + delta.updates[0].values.should.contain.an.item.with.property( + 'path', + 'navigation.attitude.roll' + ) + delta.updates[0].values[1].value.should.be.closeTo(0.0087, 0.00005) + }) +})