From ed907165f477f6f9d90606d302f5747c5015bae8 Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Fri, 1 Feb 2019 18:00:19 +1000 Subject: [PATCH] feat(client): Add support for sending messages as if they are coming from a BBMD --- lib/bvlc.js | 21 ++++++++- lib/client.js | 118 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 113 insertions(+), 26 deletions(-) diff --git a/lib/bvlc.js b/lib/bvlc.js index a55d41b2..d2d62aba 100644 --- a/lib/bvlc.js +++ b/lib/bvlc.js @@ -2,11 +2,28 @@ const baEnum = require('./enum'); -module.exports.encode = (buffer, func, msgLength) => { +const DefaultBACnetPort = 47808; + +module.exports.encode = (buffer, func, msgLength, forwardedFrom) => { buffer[0] = baEnum.BVLL_TYPE_BACNET_IP; - buffer[1] = func; + // buffer[1] set below buffer[2] = (msgLength & 0xFF00) >> 8; buffer[3] = (msgLength & 0x00FF) >> 0; + if (forwardedFrom) { + // This is always a FORWARDED_NPDU regardless of the 'func' parameter. + buffer[1] = baEnum.BvlcResultPurpose.FORWARDED_NPDU; + const [ipstr, portstr] = forwardedFrom.split(':'); + const port = parseInt(portstr) || DefaultBACnetPort; + const ip = ipstr.split('.'); + buffer[4] = parseInt(ip[0]); + buffer[5] = parseInt(ip[1]); + buffer[6] = parseInt(ip[2]); + buffer[7] = parseInt(ip[3]); + buffer[8] = (port & 0xFF00) >> 8; + buffer[9] = (port & 0x00FF) >> 0; + return 6 + baEnum.BVLC_HEADER_LENGTH; + } + buffer[1] = func; return baEnum.BVLC_HEADER_LENGTH; }; diff --git a/lib/client.js b/lib/client.js index 2790c980..50ea691e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -15,6 +15,7 @@ const baEnum = require('./enum'); const DEFAULT_HOP_COUNT = 0xFF; const BVLC_HEADER_LENGTH = 4; +const BVLC_FWD_HEADER_LENGTH = 10; // FORWARDED_NPDU /** * To be able to communicate to BACNET devices, you have to initialize a new bacstack instance. @@ -91,10 +92,10 @@ class Client extends EventEmitter { }; } - _getBuffer() { + _getBuffer(isForwarded) { return { buffer: Buffer.alloc(this._transport.getMaxPayload()), - offset: BVLC_HEADER_LENGTH + offset: isForwarded ? BVLC_FWD_HEADER_LENGTH : BVLC_HEADER_LENGTH }; } @@ -797,13 +798,15 @@ class Client extends EventEmitter { const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, - invokeId: options.invokeId || this._getInvokeId() + invokeId: options.invokeId || this._getInvokeId(), + isForwarded: !!options.forwardedFrom, + forwardedFrom: options.forwardedFrom || null, }; - const buffer = this._getBuffer(); + const buffer = this._getBuffer(settings.isForwarded); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.CONFIRMED_COV_NOTIFICATION, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.covNotify.encode(buffer, subscribeId, initiatingDeviceId, monitoredObject, lifetime, values); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset, settings.forwardedFrom); this._transport.send(buffer.buffer, buffer.offset, address); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); @@ -1203,35 +1206,85 @@ class Client extends EventEmitter { } // Public Device Functions - readPropertyResponse(receiver, invokeId, objectId, property, value) { - const buffer = this._getBuffer(); + + /** + * The readPropertyResponse call sends a response with information about one of our properties. + * @function bacstack.readPropertyResponse + * @param {string} receiver - IP address of the target device. + * @param {number} invokeId - ID of the original readProperty request. + * @param {object} objectId - objectId from the original request, + * @param {object} property - property being read, taken from the original request. + * @param {object=} options varying behaviour for special circumstances + * @param {string=} options.forwardedFrom - If functioning as a BBMD, the IP address this message originally came from. + */ + readPropertyResponse(receiver, invokeId, objectId, property, value, options = {}) { + const settings = { + isForwarded: !!options.forwardedFrom, + forwardedFrom: options.forwardedFrom || null, + }; + + const buffer = this._getBuffer(settings.isForwarded); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeComplexAck(buffer, baEnum.PduTypes.COMPLEX_ACK, baEnum.ConfirmedServiceChoice.READ_PROPERTY, invokeId); baServices.readProperty.encodeAcknowledge(buffer, objectId, property.id, property.index, value); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset, settings.forwardedFrom); this._transport.send(buffer.buffer, buffer.offset, receiver); } - readPropertyMultipleResponse(receiver, invokeId, values) { - const buffer = this._getBuffer(); + readPropertyMultipleResponse(receiver, invokeId, values, options = {}) { + const settings = { + isForwarded: !!options.forwardedFrom, + forwardedFrom: options.forwardedFrom || null, + }; + + const buffer = this._getBuffer(settings.isForwarded); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeComplexAck(buffer, baEnum.PduTypes.COMPLEX_ACK, baEnum.ConfirmedServiceChoice.READ_PROPERTY_MULTIPLE, invokeId); baServices.readPropertyMultiple.encodeAcknowledge(buffer, values); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset, settings.forwardedFrom); this._transport.send(buffer.buffer, buffer.offset, receiver); } - iAmResponse(deviceId, segmentation, vendorId) { - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, this._transport.getBroadcastAddress()); + /** + * The iAmResponse command is sent as a reply to a whoIs request. + * @function bacstack.iAmResponse + * @param {number} deviceId - Our device ID. + * @param {number} segmentation - an enum.Segmentation value. + * @param {number} vendorId - The numeric ID assigned to the organisation providing this application. + * @param {object=} options varying behaviour for special circumstances + * @param {string=} options.forwardedFrom - If functioning as a BBMD, the IP address this message originally came from. + * @param {string=} options.receiver - If functioning as a BBMD, the upstream device to send this message to. By default it is broadcasted to the local subnet, but this can be overridden here. An object like {net: 65535} is also permitted. + * @param {number=} options.hops - Number of hops until packet should be dropped, default 255. + */ + iAmResponse(deviceId, segmentation, vendorId, options) { + const settings = { + isForwarded: !!options.forwardedFrom, + forwardedFrom: options.forwardedFrom || null, + receiver: options.receiver || this._transport.getBroadcastAddress(), + hops: options.hops || DEFAULT_HOP_COUNT, + }; + + const buffer = this._getBuffer(settings.isForwarded); + baNpdu.encode( + buffer, + baEnum.NpduControlPriority.NORMAL_MESSAGE, + settings.receiver, + undefined, + settings.hops + ); baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduTypes.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.I_AM); baServices.iAmBroadcast.encode(buffer, deviceId, this._transport.getMaxPayload(), segmentation, vendorId); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, this._transport.getBroadcastAddress()); + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU, buffer.offset, settings.forwardedFrom); + this._transport.send(buffer.buffer, buffer.offset, settings.receiver); } - iHaveResponse(deviceId, objectId, objectName) { - const buffer = this._getBuffer(); + iHaveResponse(deviceId, objectId, objectName, options = {}) { + const settings = { + isForwarded: !!options.forwardedFrom, + forwardedFrom: options.forwardedFrom || null, + }; + + const buffer = this._getBuffer(settings.isForwarded); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, this._transport.getBroadcastAddress()); baApdu.EecodeUnconfirmedServiceRequest(buffer, baEnum.PduTypes.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.I_HAVE); baServices.EncodeIhaveBroadcast(buffer, deviceId, objectId, objectName); @@ -1239,14 +1292,26 @@ class Client extends EventEmitter { this._transport.send(buffer.buffer, buffer.offset, this._transport.getBroadcastAddress()); } - simpleAckResponse(receiver, service, invokeId) { - const buffer = this._getBuffer(); + simpleAckResponse(receiver, service, invokeId, options = {}) { + const settings = { + isForwarded: !!options.forwardedFrom, + forwardedFrom: options.forwardedFrom || null, + }; + + const buffer = this._getBuffer(settings.isForwarded); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeSimpleAck(buffer, baEnum.PduTypes.SIMPLE_ACK, service, invokeId); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset, settings.forwardedFrom); this._transport.send(buffer.buffer, buffer.offset, receiver); } + /** + * The resultResponse is a BVLC-Result message used to respond to certain events, such as BBMD registration. + * This message cannot be wrapped for passing through a BBMD, as it is used as a BBMD control message. + * @function bacstack.resultResponse + * @param {string} receiver - IP address of the target device. + * @param {number} resultCode - Single value from BvlcResultFormat enum. + */ resultResponse(receiver, resultCode) { const buffer = this._getBuffer(); baApdu.encodeResult(buffer, resultCode); @@ -1254,12 +1319,17 @@ class Client extends EventEmitter { this._transport.send(buffer.buffer, buffer.offset, receiver); } - errorResponse(receiver, service, invokeId, errorClass, errorCode) { - const buffer = this._getBuffer(); + errorResponse(receiver, service, invokeId, errorClass, errorCode, options = {}) { + const settings = { + isForwarded: !!options.forwardedFrom, + forwardedFrom: options.forwardedFrom || null, + }; + + const buffer = this._getBuffer(settings.isForwarded); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeError(buffer, baEnum.PduTypes.ERROR, service, invokeId); baServices.error.encode(buffer, errorClass, errorCode); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset, settings.forwardedFrom); this._transport.send(buffer.buffer, buffer.offset, receiver); }