UNPKG

knx

Version:

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

369 lines (342 loc) 12 kB
/** * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ const util = require('util'); const FSM = require('./FSM'); const DPTLib = require('./dptlib'); const KnxLog = require('./KnxLog'); const KnxConstants = require('./KnxConstants'); const KnxNetProtocol = require('./KnxProtocol'); // bind incoming UDP packet handler FSM.prototype.onUdpSocketMessage = function(msg, rinfo, callback) { // get the incoming packet's service type ... try { const reader = KnxNetProtocol.createReader(msg); reader.KNXNetHeader('tmp'); const dg = reader.next()['tmp']; const descr = datagramDesc(dg); KnxLog.get().trace('(%s): Received %s message: %j', this.compositeState(), descr, dg); if (!isNaN(this.channel_id) && ((dg.hasOwnProperty('connstate') && dg.connstate.channel_id != this.channel_id) || (dg.hasOwnProperty('tunnstate') && dg.tunnstate.channel_id != this.channel_id))) { KnxLog.get().trace('(%s): *** Ignoring %s datagram for other channel (own: %d)', this.compositeState(), descr, this.channel_id); } else { // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") const signal = util.format('inbound_%s', descr); if (descr === "DISCONNECT_REQUEST") { KnxLog.get().info("empty internal fsm queue due to %s: ", signal); this.clearQueue(); } this.handle(signal, dg); } } catch(err) { KnxLog.get().debug('(%s): Incomplete/unparseable UDP packet: %s: %s', this.compositeState(),err, msg.toString('hex') ); } }; FSM.prototype.AddConnState = function(datagram) { datagram.connstate = { channel_id: this.channel_id, state: 0 } } FSM.prototype.AddTunnState = function(datagram) { // add the remote IP router's endpoint datagram.tunnstate = { channel_id: this.channel_id, tunnel_endpoint: this.remoteEndpoint.addr + ':' + this.remoteEndpoint.port } } const AddCRI = (datagram) => { // add the CRI datagram.cri = { connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, unused: 0 } } FSM.prototype.AddCEMI = function(datagram, msgcode) { const sendAck = ((msgcode || 0x11) == 0x11) && !this.options.suppress_ack_ldatareq; // only for L_Data.req datagram.cemi = { msgcode: msgcode || 0x11, // default: L_Data.req for tunneling ctrl: { frameType: 1, // 0=extended 1=standard reserved: 0, // always 0 repeat: 1, // the OPPOSITE: 1=do NOT repeat broadcast: 1, // 0-system broadcast 1-broadcast priority: 3, // 0-system 1-normal 2-urgent 3-low acknowledge: sendAck ? 1 : 0, confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error // 2nd byte destAddrType: 1, // FIXME: 0-physical 1-groupaddr hopCount: 6, extendedFrame: 0 }, src_addr: this.options.physAddr || "15.15.15", dest_addr: "0/0/0", // apdu: { // default operation is GroupValue_Write apci: 'GroupValue_Write', tpci: 0, data: 0 } } } /* * submit an outbound request to the state machine * * type: service type * datagram_template: * if a datagram is passed, use this as * if a function is passed, use this to DECORATE * if NULL, then just make a new empty datagram. Look at AddXXX methods */ FSM.prototype.Request = function(type, datagram_template, callback) { // populate skeleton datagram const datagram = this.prepareDatagram(type); // decorate the datagram, if a function is passed if (typeof datagram_template == 'function') { datagram_template(datagram); } // make sure that we override the datagram service type! datagram.service_type = type; const st = KnxConstants.keyText('SERVICE_TYPE', type); // hand off the outbound request to the state machine this.handle('outbound_' + st, datagram); if (typeof callback === 'function') callback(); } // prepare a datagram for the given service type FSM.prototype.prepareDatagram = function(svcType) { const datagram = { "header_length": 6, "protocol_version": 16, // 0x10 == version 1.0 "service_type": svcType, "total_length": null, // filled in automatically } // AddHPAI(datagram); // switch (svcType) { case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: AddTunn(datagram); AddCRI(datagram); // no break! case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: this.AddConnState(datagram); break; case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: this.AddCEMI(datagram, KnxConstants.MESSAGECODES['L_Data.ind']); break; case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: AddTunn(datagram); this.AddTunnState(datagram); this.AddCEMI(datagram); break; case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: this.AddTunnState(datagram); break; default: KnxLog.get().debug('Do not know how to deal with svc type %d', svcType); } return datagram; } /* send the datagram over the wire */ FSM.prototype.send = function(datagram, callback) { var cemitype; // TODO: set, but unused try { this.writer = KnxNetProtocol.createWriter(); switch (datagram.service_type) { case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: // append the CEMI service type if this is a tunneling request... cemitype = KnxConstants.keyText('MESSAGECODES', datagram.cemi.msgcode); break; } const packet = this.writer.KNXNetHeader(datagram); const buf = packet.buffer; const svctype = KnxConstants.keyText('SERVICE_TYPE', datagram.service_type); // TODO: unused const descr = datagramDesc(datagram); KnxLog.get().trace('(%s): Sending %s ==> %j', this.compositeState(), descr, datagram); this.socket.send( buf, 0, buf.length, this.remoteEndpoint.port, this.remoteEndpoint.addr.toString(), (err) => { KnxLog.get().trace('(%s): UDP sent %s: %s %s', this.compositeState(), (err ? err.toString() : 'OK'), descr, buf.toString('hex') ); if (typeof callback === 'function') callback(err); } ); } catch (e) { KnxLog.get().warn(e); if (typeof callback === 'function') callback(e); } } FSM.prototype.write = function(grpaddr, value, dptid, callback) { if (grpaddr == null || value == null) { KnxLog.get().warn('You must supply both grpaddr and value!'); return; } try { // outbound request onto the state machine const serviceType = this.useTunneling ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; this.Request(serviceType, function(datagram) { DPTLib.populateAPDU(value, datagram.cemi.apdu, dptid); datagram.cemi.dest_addr = grpaddr; }, callback); } catch (e) { KnxLog.get().warn(e); } } FSM.prototype.respond = function(grpaddr, value, dptid) { if (grpaddr == null || value == null) { KnxLog.get().warn('You must supply both grpaddr and value!'); return; } const serviceType = this.useTunneling ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; this.Request(serviceType, function(datagram) { DPTLib.populateAPDU(value, datagram.cemi.apdu, dptid); // this is a READ request datagram.cemi.apdu.apci = "GroupValue_Response"; datagram.cemi.dest_addr = grpaddr; return datagram; }); } FSM.prototype.writeRaw = function(grpaddr, value, bitlength, callback) { if (grpaddr == null || value == null) { KnxLog.get().warn('You must supply both grpaddr and value!'); return; } if (!Buffer.isBuffer(value)) { KnxLog.get().warn('Value must be a buffer!'); return; } // outbound request onto the state machine const serviceType = this.useTunneling ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; this.Request(serviceType, function(datagram) { datagram.cemi.apdu.data = value; datagram.cemi.apdu.bitlength = bitlength ? bitlength : (value.byteLength * 8); datagram.cemi.dest_addr = grpaddr; }, callback); } // send a READ request to the bus // you can pass a callback function which gets bound to the RESPONSE datagram event FSM.prototype.read = function(grpaddr, callback) { if (typeof callback == 'function') { // when the response arrives: const responseEvent = 'GroupValue_Response_' + grpaddr; KnxLog.get().trace('Binding connection to ' + responseEvent); const binding = (src, data) => { // unbind the event handler this.off(responseEvent, binding); // fire the callback callback(src, data); } // prepare for the response this.on(responseEvent, binding); // clean up after 3 seconds just in case no one answers the read request setTimeout( () => this.off(responseEvent, binding), 3000); } const serviceType = this.useTunneling ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; this.Request(serviceType, function(datagram) { // this is a READ request datagram.cemi.apdu.apci = "GroupValue_Read"; datagram.cemi.dest_addr = grpaddr; return datagram; }); } FSM.prototype.Disconnect = function(cb) { var that = this; if(this.state === 'connecting') { KnxLog.get().debug('Disconnecting directly'); that.transition("uninitialized"); if(cb) { cb() } return } KnxLog.get().debug('waiting for Idle-State'); this.onIdle(function() { KnxLog.get().trace('In Idle-State'); that.on('disconnected', () => { KnxLog.get().debug('Disconnected from KNX'); if(cb) { cb() } }); KnxLog.get().debug('Disconnecting from KNX'); that.transition("disconnecting"); }); // machina.js removeAllListeners equivalent: // this.off(); } FSM.prototype.onIdle = function(cb) { if(this.state === 'idle') { KnxLog.get().trace('Connection is already Idle'); cb(); } else { this.on("transition", function(data) { if(data.toState === 'idle') { KnxLog.get().trace('Connection just transitioned to Idle'); cb(); } }); } } // return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) const datagramDesc = (dg) => { let blurb = KnxConstants.keyText('SERVICE_TYPE', dg.service_type); if (dg.service_type == KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || dg.service_type == KnxConstants.SERVICE_TYPE.ROUTING_INDICATION) { blurb += '_' + KnxConstants.keyText('MESSAGECODES', dg.cemi.msgcode); } return blurb; } // add the control udp local endpoint. UPDATE: not needed apparnently? const AddHPAI = (datagram) => { datagram.hpai = { protocol_type: 1, // UDP //tunnel_endpoint: this.localAddress + ":" + this.control.address().port tunnel_endpoint: '0.0.0.0:0' }; } // add the tunneling udp local endpoint UPDATE: not needed apparently? const AddTunn = (datagram) => { datagram.tunn = { protocol_type: 1, // UDP tunnel_endpoint: '0.0.0.0:0' //tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port }; } // TODO: Conncetion is obviously not a constructor, but tests call it with `new`. That should be deprecated. function Connection(options) { const conn = new FSM(options); // register with the FSM any event handlers passed into the options object if (typeof options.handlers === 'object') { for (const [key, value] of Object.entries(options.handlers)) { if (typeof value === 'function') { conn.on(key, value); } } } // boot up the KNX connection unless told otherwise if (!options.manualConnect) conn.Connect(); return conn; }; module.exports = Connection;