knx-dpt
Version:
Serialize and deserialize KNX datapoints
354 lines (313 loc) • 12.1 kB
JavaScript
const fs = require('fs');
const path = require('path');
const bufrev = require('./utils/buffer-reverse');
const mergeObjects = require('./utils/merge-objects');
/**
* A collection of all layouts that are loaded.
*/
var layouts = {};
/**
* A collection of all property types that are loaded.
*/
var proptypes = {};
/**
* A collection of all datapoints that are currently loaded.
* @type {{}}
*/
var datapoints = {};
/**
* The folder that contains all layouts.
* @type {string}
*/
var layoutDir = `${__dirname}/layouts`;
/**
* The folder that contains all property types.
* @type {string}
*/
var propDir = `${__dirname}/props`;
/**
* The object that represents the library of datapoints
* @type {{}}
*/
var dptlib = {
formatAPDU: function (value, dpt) {
return dpt.formatAPDU(value);
},
fromBuffer: function (buf, dpt) {
return dpt.fromBuffer(buf);
},
loadLayout: function (layout) {
// If layout contains list of layouts, load them separately
if (Array.isArray(layout)) {
layout.forEach(function (l) {
dptlib.loadLayout(l);
});
} else {
// Store the layout in the layouts object
layouts[layout.id] = layout;
}
},
loadPropType: function (proptype) {
// If proptype contains list of proptypes, load them separately
if (Array.isArray(proptype)) {
proptype.forEach(function (p) {
dptlib.loadPropType(p);
});
} else {
// Store the proptype in the proptypes object
proptypes[proptype.id] = proptype;
}
},
/**
* a generic DPT resolution function
* @param dptid The datapoint id. Allowed formats: 9/"9"/"9.001"/"DPT9.001"
*/
resolve: function (dptid) {
var baseId = null;
var subId = null;
// Parse the dpt id
// If it is a raw number
if (typeof dptid === 'number' && isFinite(dptid)) {
// we're passed in a raw number (9)
baseId = dptid;
// If it is a string
} else if (typeof dptid == 'string') {
var m = dptid.toUpperCase().match(/(\d+)(\.(\d+))?/);
baseId = parseInt(m[1]);
if (m[3]) {
subId = m[3];
}
}
// Verify whether it exists
if (baseId === null || !layouts[baseId] || (subId !== null && !layouts[baseId].subs[subId])) {
console.trace("no such DPT: %j", dptid);
throw "No such DPT";
} else {
return buildDPT(baseId, subId);
}
}
};
/**
* Return the library.
*/
module.exports = new Proxy(dptlib, {
get: function (target, name) {
// If target has the specified property, return it
if (target[name])
return target[name];
// Check if string in format dpt*
var m = name.match(/dpt(\d+)/);
if (m) {
return dptlib.resolve(`DPT${m[1]}`);
}
// Cannot get that property
return undefined;
}
});
/**
* Load all property type specifications into the proptypes object.
*/
var propDirEntries = fs.readdirSync(propDir);
for (var i = 0; i < propDirEntries.length; i++) {
// Get individual filename
var filename = propDir + path.sep + propDirEntries[i];
// Only load the file if it is not a directory
if (!fs.lstatSync(filename).isDirectory()) {
// Load the property from the file
var prop = require(filename);
// Load the property into the library
dptlib.loadPropType(prop);
}
}
/**
* Load all datapoint specifications into the layouts object.
*/
var dirEntries = fs.readdirSync(layoutDir);
for (var i = 0; i < dirEntries.length; i++) {
// Get individual filename
var filename = layoutDir + path.sep + dirEntries[i];
// Only load the file if it is not a directory
if (!fs.lstatSync(filename).isDirectory()) {
// Load the layout from the file
var dptLayout = require(filename);
// Load the layout into the library
dptlib.loadLayout(dptLayout);
}
}
/**
* Builds an DPT
* @param baseId
* @param subId
* @returns {*}
*/
var buildDPT = function (baseId, subId) {
// Determine datapoint FQN
var name = subId === null ? baseId.toString() : baseId.toString() + '.' + subId;
// Check if datapoint is cached
if (datapoints[name])
return datapoints[name];
// Not cached, so build the datapoint, first we determine the layout
var specs = layouts[baseId];
var layout = subId === null ? specs.base : mergeObjects(specs.base, specs.subs[subId]);
// Then we build the base datapoint intformation
var r = {};
r.name = layout.name;
r.desc = layout.desc;
r.unit = layout.unit;
r.use = layout.use;
// First, determine the buffer size
var bitsize = 0;
for (var k in layout.props) {
if (layout.props.hasOwnProperty(k)) {
var prop = layout.props[k];
if(prop.type === 'skip') {
bitsize += prop.size;
continue;
}
bitsize += proptypes[prop.type].size(prop);
}
}
var bytesize = Math.ceil(bitsize / 8);
// Define the formatAPDU function
r.formatAPDU = function (input) {
// Allocate buffer and needed counters
var buffer = Buffer.alloc(bytesize);
var bitsWritten = 0;
// Check beforeSerialize
if (layout.beforeSerialize && typeof layout.beforeSerialize === 'function') {
input = layout.beforeSerialize(input);
}
// Write out all properties
for (var pk in layout.props) {
if (layout.props.hasOwnProperty(pk)) {
var pv = layout.props[pk];
// If property type is skip, increment bits written and go to next
if (pv.type === "skip") {
bitsWritten += pv.size;
continue;
}
// Get value from input
var value;
if (pv.hasOwnProperty("index") && pv.index !== null) {
if (typeof pv.index === 'string') {
value = input;
if (pv.index !== '') {
pv.index.split('.').forEach(function (p) {
value = value[p];
});
}
} else if (typeof pv.index === 'object') {
value = pv.index.get(input);
}
}
// Map if necessary
if (pv.range && pv.mapRangeTo) {
var lowerBound = null;
var upperBound = null;
// Find upper and lower bound
for (var i = 0; i < pv.mapRangeTo.length; i++) {
var point = pv.mapRangeTo[i];
lowerBound = point[1] <= value && (lowerBound === null || point[1] >= lowerBound[1]) ? point : lowerBound;
upperBound = point[1] >= value && (upperBound === null || point[1] <= upperBound[1]) ? point : upperBound;
}
// Check if value is a defined point
if (lowerBound !== null && lowerBound[1] === value) {
value = lowerBound[0];
} else if (upperBound !== null && upperBound[1] === value) {
value = upperBound[0];
} else {
value = (value - lowerBound[1]) * (upperBound[0] - lowerBound[0]) / (upperBound[1] - lowerBound[1]) + lowerBound[0];
}
}
// Check range
if (pv.range) {
if (value < pv.range[0] || value > pv.range[1]) {
throw `Invalid range: ${pv.range[0]}<=${value}<=${pv.range[1]}`;
}
}
// Write value to buffer
bitsWritten += proptypes[pv.type].write(pv, buffer, value, bitsWritten);
}
}
// Apparently KNX wants to have the MSB first, so reverse the buffer
bufrev(buffer);
// Return the buffer with the formatted data
return buffer;
};
// Define the fromBuffer function
r.fromBuffer = function (buffer) {
// First determine if buffer is correct size
if (buffer.length != bytesize) throw "Buffer should be " + bytesize + " byte" + (bytesize > 1 ? "s" : "") + " long";
// Apparently KNX wants to have the MSB first, so reverse the buffer
bufrev(buffer);
// Create result
var result = null;
if (typeof layout.beforeDeserialize === 'function') {
result = layout.beforeDeserialize();
} else {
result = layout.beforeDeserialize;
}
// Read data from buffer
var bitsRead = 0;
for (var pk in layout.props) {
if (layout.props.hasOwnProperty(pk)) {
var pv = layout.props[pk];
// If property type is skip, increment bits read and go to next
if (pv.type === "skip") {
bitsRead += pv.size;
continue;
}
// Read value from buffer
var readOut = proptypes[pv.type].read(pv, buffer, bitsRead);
bitsRead += readOut.bitsRead;
var value = readOut.value;
// Map if necessary
if (pv.range && pv.mapRangeTo) {
var lowerBound = null;
var upperBound = null;
// Find upper and lower bound
for (var i = 0; i < pv.mapRangeTo.length; i++) {
var point = pv.mapRangeTo[i];
lowerBound = point[0] <= value && (lowerBound === null || point[0] >= lowerBound[0]) ? point : lowerBound;
upperBound = point[0] >= value && (upperBound === null || point[0] <= upperBound[0]) ? point : upperBound;
}
// Check if value is a defined point
if (lowerBound !== null && lowerBound[0] === value) {
value = lowerBound[1];
} else if (upperBound !== null && upperBound[0] === value) {
value = upperBound[1];
} else {
value = (value - lowerBound[0]) * (upperBound[1] - lowerBound[1]) / (upperBound[0] - lowerBound[0]) + lowerBound[1];
}
}
// Insert value into result
if (pv.hasOwnProperty("index") && pv.index !== null) {
if (typeof pv.index === 'string') {
if (pv.index === '') {
result = value;
} else {
var selectionPath = pv.index.split('.');
var selected = result;
// Select correct part of result
for (var i = 0; i < selectionPath.length - 1; i++) {
selected = selected[selectionPath[i]] ? selected[selectionPath[i]] : selected[selectionPath[i]] = {};
}
// Set property
selected[selectionPath[selectionPath.length - 1]] = value;
}
} else if (typeof pv.index === 'object') {
pv.index.set(result, value);
}
}
}
}
// Run after deserialization code
if (layout.afterDeserialize && typeof layout.afterDeserialize === 'function') {
result = layout.afterDeserialize(result);
}
// Return the result
return result;
};
// Return the built datapoint
return datapoints[name] = r;
};