UNPKG

knx

Version:

KNXnet/IP protocol implementation for Node(>=6.x)

204 lines (187 loc) 8.01 kB
/** * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ /* Datatypes ========= KNX/EIB Function Information length EIS DPT Value Switch 1 Bit EIS 1 DPT 1 0,1 Dimming (Position, Control, Value) 1 Bit, 4 Bit, 8 Bit EIS 2 DPT 3 [0,0]...[1,7] Time 3 Byte EIS 3 DPT 10 Date 3 Byte EIS 4 DPT 11 Floating point 2 Byte EIS 5 DPT 9 -671088,64 - 670760,96 8-bit unsigned value 1 Byte EIS 6 DPT 5 0...255 8-bit unsigned value 1 Byte DPT 5.001 DPT 5.001 0...100 Blinds / Roller shutter 1 Bit EIS 7 DPT 1 0,1 Priority 2 Bit EIS 8 DPT 2 [0,0]...[1,1] IEEE Floating point 4 Byte EIS 9 DPT 14 4-Octet Float Value IEEE 754 16-bit unsigned value 2 Byte EIS 10 DPT 7 0...65535 16-bit signed value 2 Byte DPT 8 DPT 8 -32768...32767 32-bit unsigned value 4 Byte EIS 11 DPT 12 0...4294967295 32-bit signed value 4 Byte DPT 13 DPT 13 -2147483648...2147483647 Access control 1 Byte EIS 12 DPT 15 ASCII character 1 Byte EIS 13 DPT 4 8859_1 character 1 Byte DPT 4.002 DPT 4.002 8-bit signed value 1 Byte EIS 14 DPT 6 -128...127 14 character ASCII 14 Byte EIS 15 DPT 16 14 character 8859_1 14 Byte DPT 16.001 DPT 16.001 Scene 1 Byte DPT 17 DPT 17 0...63 HVAC 1 Byte DPT 20 DPT 20 0..255 Unlimited string 8859_1 . DPT 24 DPT 24 List 3-byte value 3 Byte DPT 232 DPT 232 RGB[0,0,0]...[255,255,255] */ const fs = require('fs'); const path = require('path'); const util = require('util'); const log = require('log-driver').logger; const dpts = {}; for (const entry of fs.readdirSync(__dirname)) { const matches = entry.match(/(dpt.*)\.js/); if (!matches) continue; const dptid = matches[1].toUpperCase(); // DPT1..DPTxxx const mod = require(__dirname + path.sep + entry); if ( !mod.hasOwnProperty('basetype') || !mod.basetype.hasOwnProperty('bitlength') ) throw 'incomplete ' + dptid + ', missing basetype and/or bitlength!'; mod.id = dptid; dpts[dptid] = mod; //log.trace('DPT library: loaded %s (%s)', dptid, dpts[dptid].basetype.desc); } // a generic DPT resolution function // DPTs might come in as 9/"9"/"9.001"/"DPT9.001" dpts.resolve = (dptid) => { const m = dptid .toString() .toUpperCase() .match(/^(?:DPT)?(\d+)(\.(\d+))?$/); if (m === null) throw 'Invalid DPT format: ' + dptid; const dpt = dpts[util.format('DPT%s', m[1])]; if (!dpt) throw 'Unsupported DPT: ' + dptid; const cloned_dpt = cloneDpt(dpt); if (m[3]) { cloned_dpt.subtypeid = m[3]; cloned_dpt.subtype = cloned_dpt.subtypes[m[3]]; } return cloned_dpt; }; /* POPULATE an APDU object from a given Javascript value for the given DPT * - either by a custom DPT formatAPDU function * - or by this generic version, which: * -- 1) checks if the value adheres to the range set from the DPT's bitlength * */ dpts.populateAPDU = (value, apdu, dptid) => { const dpt = dpts.resolve(dptid || 'DPT1'); const nbytes = Math.ceil(dpt.basetype.bitlength / 8); apdu.data = Buffer.alloc(nbytes); apdu.bitlength = (dpt.basetype && dpt.basetype.bitlength) || 1; let tgtvalue = value; // get the raw APDU data for the given JS value if (typeof dpt.formatAPDU == 'function') { // nothing to do here, DPT-specific formatAPDU implementation will handle everything // log.trace('>>> custom formatAPDU(%s): %j', dptid, value); apdu.data = dpt.formatAPDU(value); // log.trace('<<< custom formatAPDU(%s): %j', dptid, apdu.data); return apdu; } if (!isFinite(value)) throw util.format('Invalid value, expected a %s', dpt.desc); // check if value is in range, be it explicitly defined or implied from bitlength const [r_min, r_max] = dpt.basetype.hasOwnProperty('range') ? dpt.basetype.range : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; // TODO: Maybe bitshift instead of pow? // is there a scalar range? eg. DPT5.003 angle degrees (0=0, ff=360) if ( dpt.hasOwnProperty('subtype') && dpt.subtype.hasOwnProperty('scalar_range') ) { const [s_min, s_max] = dpt.subtype.scalar_range; if (value < s_min || value > s_max) { log.trace( 'Value %j(%s) out of scalar range(%j) for %s', value, typeof value, scalar, dpt.id ); } else { // convert value from its scalar representation // e.g. in DPT5.001, 50(%) => 0x7F , 100(%) => 0xFF const a = (s_max - s_min) / (r_max - r_min); const b = s_min - r_min; tgtvalue = Math.round((value - b) / a); } } // just a plain numeric value, only check if within bounds else if (value < r_min || value > r_max) { log.trace( 'Value %j(%s) out of bounds(%j) for %s.%s', value, typeof value, range, dpt.id, dpt.subtypeid ); } // generic APDU is assumed to convey an unsigned integer of arbitrary bitlength if ( dpt.basetype.hasOwnProperty('signedness') && dpt.basetype.signedness == 'signed' ) { apdu.data.writeIntBE(tgtvalue, 0, nbytes); } else { apdu.data.writeUIntBE(tgtvalue, 0, nbytes); } // log.trace('generic populateAPDU tgtvalue=%j(%s) nbytes=%d => apdu=%j', tgtvalue, typeof tgtvalue, nbytes, apdu); }; /* get the correct Javascript value from a APDU buffer for the given DPT * - either by a custom DPT formatAPDU function * - or by this generic version, which: * -- 1) checks if the value adheres to the range set from the DPT's bitlength */ dpts.fromBuffer = (buf, dpt) => { // sanity check if (!dpt) throw util.format('DPT %s not found', dpt); // get the raw APDU data for the given JS value if (typeof dpt.fromBuffer == 'function') { // nothing to do here, DPT-specific fromBuffer implementation will handle everything return dpt.fromBuffer(buf); } // log.trace('%s buflength == %d => %j', typeof buf, buf.length, JSON.stringify(buf) ); if (buf.length > 6) { throw 'cannot handle unsigned integers more then 6 bytes in length'; } let value = 0; if ( dpt.basetype.hasOwnProperty('signedness') && dpt.basetype.signedness == 'signed' ) value = buf.readIntBE(0, buf.length); else value = buf.readUIntBE(0, buf.length); // log.trace(' ../knx/src/index.js : DPT : ' + JSON.stringify(dpt)); // for exploring dpt and implementing description if ( dpt.hasOwnProperty('subtype') && dpt.subtype.hasOwnProperty('scalar_range') ) { const [r_min, r_max] = dpt.basetype.hasOwnProperty('range') ? dpt.basetype.range : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; const [s_min, s_max] = dpt.subtype.scalar_range; // convert value from its scalar representation // e.g. in DPT5.001, 50(%) => 0x7F , 100(%) => 0xFF const a = (s_max - s_min) / (r_max - r_min); const b = s_min - r_min; value = Math.round(a * value + b); //log.trace('fromBuffer scalar a=%j b=%j %j', a,b, value); } // log.trace('generic fromBuffer buf=%j, value=%j', buf, value); return value; }; const cloneDpt = (d) => { const { fromBuffer, formatAPDU } = d; return { ...JSON.parse(JSON.stringify(d)), fromBuffer, formatAPDU }; }; module.exports = dpts;