diff --git a/README.md b/README.md index 998f01f4..1a68b2ed 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ following services are already supported at this point in time: | Create Object | yes¹ | yes¹ | | Delete Object | yes¹ | yes¹ | | Subscribe COV | yes¹ | yes¹ | +| Confirmed COV Notification | yes¹ | yes¹ | | Subscribe Property | yes¹ | yes¹ | | Atomic Read File | yes¹ | yes¹ | | Atomic Write File | yes¹ | yes¹ | @@ -56,6 +57,8 @@ following services are already supported at this point in time: | Unconfirmed Event Notification | yes¹ | yes¹ | | Unconfirmed Private Transfer | yes¹ | yes¹ | | Confirmed Private Transfer | yes¹ | yes¹ | +| Register Foreign Device | no | yes¹ | +| Distribute Broadcast to Network| no | yes¹ | ¹ Support implemented as Beta (untested, undocumented, breaking interface) diff --git a/lib/apdu.js b/lib/apdu.js index 53489210..487fea04 100644 --- a/lib/apdu.js +++ b/lib/apdu.js @@ -1,5 +1,6 @@ 'use strict'; +const baAsn1 = require('./asn1'); const baEnum = require('./enum'); const getDecodedType = module.exports.getDecodedType = (buffer, offset) => { @@ -153,6 +154,20 @@ module.exports.decodeSegmentAck = (buffer, offset) => { }; }; +module.exports.encodeResult = (buffer, /* BvlcResultFormat */ resultCode) => { + baAsn1.encodeUnsigned(buffer, resultCode, 2); +}; + +module.exports.decodeResult = (buffer, offset) => { + const orgOffset = offset; + const decode = baAsn1.decodeUnsigned(buffer, offset, 2); + offset += decode.len; + return { + len: offset - orgOffset, + resultCode: decode.value, // BvlcResultFormat + }; +}; + module.exports.encodeError = (buffer, type, service, invokeId) => { buffer.buffer[buffer.offset++] = type; buffer.buffer[buffer.offset++] = invokeId; diff --git a/lib/asn1.js b/lib/asn1.js index a1c3d286..655545be 100644 --- a/lib/asn1.js +++ b/lib/asn1.js @@ -46,7 +46,7 @@ const getEncodingType = (encoding, decodingBuffer, decodingOffset) => { } }; -const encodeUnsigned = (buffer, value, length) => { +const encodeUnsigned = module.exports.encodeUnsigned = (buffer, value, length) => { buffer.buffer.writeUIntBE(value, buffer.offset, length, true); buffer.offset += length; }; @@ -484,8 +484,10 @@ const bacappEncodeApplicationData = module.exports.bacappEncodeApplicationData = case baEnum.ApplicationTags.READ_ACCESS_SPECIFICATION: encodeReadAccessSpecification(buffer, value.value); break; + case undefined: + throw new Error('Cannot encode a value if the type has not been specified'); default: - throw 'Unknown type'; + throw 'Unknown ApplicationTags type: ' + baEnum.getEnumName(baEnum.ApplicationTags, value.type); } }; diff --git a/lib/bvlc.js b/lib/bvlc.js index d2a6edf9..0a531f94 100644 --- a/lib/bvlc.js +++ b/lib/bvlc.js @@ -2,11 +2,36 @@ const baEnum = require('./enum'); -module.exports.encode = (buffer, func, msgLength) => { +const DefaultBACnetPort = 47808; + +module.exports.encode = (buffer, func, msgLength, originatingIP) => { buffer[0] = baEnum.BVLL_TYPE_BACNET_IP; buffer[1] = func; buffer[2] = (msgLength & 0xFF00) >> 8; buffer[3] = (msgLength & 0x00FF) >> 0; + if (originatingIP) { + // This is always a FORWARDED_NPDU regardless of the 'func' parameter. + if (func !== baEnum.BvlcResultPurpose.FORWARDED_NPDU) { + throw new Error('Cannot specify originatingIP unless ' + + 'BvlcResultPurpose.FORWARDED_NPDU is used.'); + } + // Encode the IP address and optional port into bytes. + const [ipstr, portstr] = originatingIP.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; + } else { + if (func === baEnum.BvlcResultPurpose.FORWARDED_NPDU) { + throw new Error('Must specify originatingIP if ' + + 'BvlcResultPurpose.FORWARDED_NPDU is used.'); + } + } return baEnum.BVLC_HEADER_LENGTH; }; @@ -15,16 +40,12 @@ module.exports.decode = (buffer, offset) => { const func = buffer[1]; const msgLength = (buffer[2] << 8) | (buffer[3] << 0); if (buffer[0] !== baEnum.BVLL_TYPE_BACNET_IP || buffer.length !== msgLength) return; + let originatingIP = null; switch (func) { case baEnum.BvlcResultPurpose.BVLC_RESULT: case baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU: case baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU: case baEnum.BvlcResultPurpose.DISTRIBUTE_BROADCAST_TO_NETWORK: - len = 4; - break; - case baEnum.BvlcResultPurpose.FORWARDED_NPDU: - len = 10; - break; case baEnum.BvlcResultPurpose.REGISTER_FOREIGN_DEVICE: case baEnum.BvlcResultPurpose.READ_FOREIGN_DEVICE_TABLE: case baEnum.BvlcResultPurpose.DELETE_FOREIGN_DEVICE_TABLE_ENTRY: @@ -32,7 +53,23 @@ module.exports.decode = (buffer, offset) => { case baEnum.BvlcResultPurpose.WRITE_BROADCAST_DISTRIBUTION_TABLE: case baEnum.BvlcResultPurpose.READ_BROADCAST_DISTRIBUTION_TABLE_ACK: case baEnum.BvlcResultPurpose.READ_FOREIGN_DEVICE_TABLE_ACK: + len = 4; + break; + case baEnum.BvlcResultPurpose.FORWARDED_NPDU: + // Work out where the packet originally came from before the BBMD + // forwarded it to us, so we can tell the BBMD where to send any reply to. + const port = (buffer[8] << 8) | buffer[9]; + originatingIP = buffer.slice(4, 8).join('.'); + + // Only add the port if it's not the usual one. + if (port !== DefaultBACnetPort) { + originatingIP += ':' + port; + } + + len = 10; + break; case baEnum.BvlcResultPurpose.SECURE_BVLL: + // unimplemented return; default: return; @@ -40,6 +77,10 @@ module.exports.decode = (buffer, offset) => { return { len: len, func: func, - msgLength: msgLength + msgLength: msgLength, + // Originating IP is set to the IP address of the node that originally + // sent the packet, when it has been forwarded to us by a BBMD (since the + // BBMD's IP address will be in the sender field. + originatingIP: originatingIP, }; }; diff --git a/lib/client.js b/lib/client.js index eebc2d46..0ef4332d 100644 --- a/lib/client.js +++ b/lib/client.js @@ -3,6 +3,7 @@ // Util Modules const EventEmitter = require('events').EventEmitter; const debug = require('debug')('bacstack'); +debug.trace = require('debug')('bacstack:trace'); // Local Modules const baTransport = require('./transport'); @@ -13,8 +14,47 @@ const baNpdu = require('./npdu'); const baBvlc = require('./bvlc'); const baEnum = require('./enum'); -const DEFAULT_HOP_COUNT = 0xFF; const BVLC_HEADER_LENGTH = 4; +const BVLC_FWD_HEADER_LENGTH = 10; // FORWARDED_NPDU + +const beU = baEnum.UnconfirmedServiceChoice; +const unconfirmedServiceMap = { + [beU.I_AM]: 'iAm', + [beU.WHO_IS]: 'whoIs', + [beU.WHO_HAS]: 'whoHas', + [beU.UNCONFIRMED_COV_NOTIFICATION]: 'covNotifyUnconfirmed', + [beU.TIME_SYNCHRONIZATION]: 'timeSync', + [beU.UTC_TIME_SYNCHRONIZATION]: 'timeSyncUTC', + [beU.UNCONFIRMED_EVENT_NOTIFICATION]: 'eventNotify', + [beU.I_HAVE]: 'iHave', + [beU.UNCONFIRMED_PRIVATE_TRANSFER]: 'privateTransfer', +}; +const beC = baEnum.ConfirmedServiceChoice; +const confirmedServiceMap = { + [beC.READ_PROPERTY]: 'readProperty', + [beC.WRITE_PROPERTY]: 'writeProperty', + [beC.READ_PROPERTY_MULTIPLE]: 'readPropertyMultiple', + [beC.WRITE_PROPERTY_MULTIPLE]: 'writePropertyMultiple', + [beC.CONFIRMED_COV_NOTIFICATION]: 'covNotify', + [beC.ATOMIC_WRITE_FILE]: 'atomicWriteFile', + [beC.ATOMIC_READ_FILE]: 'atomicReadFile', + [beC.SUBSCRIBE_COV]: 'subscribeCov', + [beC.SUBSCRIBE_COV_PROPERTY]: 'subscribeProperty', + [beC.DEVICE_COMMUNICATION_CONTROL]: 'deviceCommunicationControl', + [beC.REINITIALIZE_DEVICE]: 'reinitializeDevice', + [beC.CONFIRMED_EVENT_NOTIFICATION]: 'eventNotify', + [beC.READ_RANGE]: 'readRange', + [beC.CREATE_OBJECT]: 'createObject', + [beC.DELETE_OBJECT]: 'deleteObject', + [beC.ACKNOWLEDGE_ALARM]: 'alarmAcknowledge', + [beC.GET_ALARM_SUMMARY]: 'getAlarmSummary', + [beC.GET_ENROLLMENT_SUMMARY]: 'getEnrollmentSummary', + [beC.GET_EVENT_INFORMATION]: 'getEventInformation', + [beC.LIFE_SAFETY_OPERATION]: 'lifeSafetyOperation', + [beC.ADD_LIST_ELEMENT]: 'addListElement', + [beC.REMOVE_LIST_ELEMENT]: 'removeListElement', + [beC.CONFIRMED_PRIVATE_TRANSFER]: 'privateTransfer', +}; /** * To be able to communicate to BACNET devices, you have to initialize a new bacstack instance. @@ -91,10 +131,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 }; } @@ -110,28 +150,36 @@ class Client extends EventEmitter { } _segmentAckResponse(receiver, negative, server, originalInvokeId, sequencenumber, actualWindowSize) { - const buffer = this._getBuffer(); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); baApdu.encodeSegmentAck(buffer, baEnum.PduTypes.SEGMENT_ACK | (negative ? baEnum.PduSegAckBits.NEGATIVE_ACK : 0) | (server ? baEnum.PduSegAckBits.SERVER : 0), originalInvokeId, sequencenumber, actualWindowSize); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, receiver); + this.sendBvlc(receiver, buffer); } - _performDefaultSegmentHandling(sender, adr, type, service, invokeId, maxSegments, maxApdu, sequencenumber, first, moreFollows, buffer, offset, length) { + _performDefaultSegmentHandling(msg, first, moreFollows, buffer, offset, length) { if (first) { this._segmentStore = []; - type &= ~baEnum.PduConReqBits.SEGMENTED_MESSAGE; + msg.type &= ~baEnum.PduConReqBits.SEGMENTED_MESSAGE; let apduHeaderLen = 3; - if ((type & baEnum.PDU_TYPE_MASK) === baEnum.PduTypes.CONFIRMED_REQUEST) { + if ((msg.type & baEnum.PDU_TYPE_MASK) === baEnum.PduTypes.CONFIRMED_REQUEST) { apduHeaderLen = 4; } const apdubuffer = this._getBuffer(); apdubuffer.offset = 0; buffer.copy(apdubuffer.buffer, apduHeaderLen, offset, offset + length); - if ((type & baEnum.PDU_TYPE_MASK) === baEnum.PduTypes.CONFIRMED_REQUEST) { - baApdu.encodeConfirmedServiceRequest(apdubuffer, type, service, maxSegments, maxApdu, invokeId, 0, 0); + if ((msg.type & baEnum.PDU_TYPE_MASK) === baEnum.PduTypes.CONFIRMED_REQUEST) { + baApdu.encodeConfirmedServiceRequest( + apdubuffer, + msg.type, + msg.service, + msg.maxSegments, + msg.maxApdu, + msg.invokeId, + 0, + 0 + ); } else { - baApdu.encodeComplexAck(apdubuffer, type, service, invokeId, 0, 0); + baApdu.encodeComplexAck(apdubuffer, msg.type, msg.service, msg.invokeId, 0, 0); } this._segmentStore.push(apdubuffer.buffer.slice(0, length + apduHeaderLen)); } else { @@ -140,319 +188,208 @@ class Client extends EventEmitter { if (!moreFollows) { const apduBuffer = Buffer.concat(this._segmentStore); this._segmentStore = []; - type &= ~baEnum.PduConReqBits.SEGMENTED_MESSAGE; - this._handlePdu(adr, type, apduBuffer, 0, apduBuffer.length); + msg.type &= ~baEnum.PduConReqBits.SEGMENTED_MESSAGE; + this._handlePdu(apduBuffer, 0, apduBuffer.length, msg.header); } } - _processSegment(receiver, type, service, invokeId, maxSegments, maxApdu, server, sequencenumber, proposedWindowNumber, buffer, offset, length) { + _processSegment(msg, server, buffer, offset, length) { let first = false; - if (sequencenumber === 0 && this._lastSequenceNumber === 0) { + if (msg.sequencenumber === 0 && this._lastSequenceNumber === 0) { first = true; } else { - if (sequencenumber !== this._lastSequenceNumber + 1) { - return this._segmentAckResponse(receiver, true, server, invokeId, this._lastSequenceNumber, proposedWindowNumber); + if (msg.sequencenumber !== this._lastSequenceNumber + 1) { + return this._segmentAckResponse(msg.header.address, true, server, msg.invokeId, this._lastSequenceNumber, msg.proposedWindowNumber); } } - this._lastSequenceNumber = sequencenumber; + this._lastSequenceNumber = msg.sequencenumber; const moreFollows = type & baEnum.PduConReqBits.MORE_FOLLOWS; if (!moreFollows) { this._lastSequenceNumber = 0; } - if ((sequencenumber % proposedWindowNumber) === 0 || !moreFollows) { - this._segmentAckResponse(receiver, false, server, invokeId, sequencenumber, proposedWindowNumber); + if ((msg.sequencenumber % msg.proposedWindowNumber) === 0 || !moreFollows) { + this._segmentAckResponse(msg.header.address, false, server, msg.invokeId, msg.sequencenumber, msg.proposedWindowNumber); } - this._performDefaultSegmentHandling(this, receiver, type, service, invokeId, maxSegments, maxApdu, sequencenumber, first, moreFollows, buffer, offset, length); + this._performDefaultSegmentHandling(msg, first, moreFollows, buffer, offset, length); } - _processConfirmedServiceRequest(address, type, service, maxSegments, maxApdu, invokeId, buffer, offset, length) { + _processServiceRequest(serviceMap, content, buffer, offset, length) { let result; - debug('Handle this._processConfirmedServiceRequest'); - if (service === baEnum.ConfirmedServiceChoice.READ_PROPERTY) { - result = baServices.readProperty.decode(buffer, offset, length); - if (!result) return debug('Received invalid readProperty message'); - this.emit('readProperty', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.WRITE_PROPERTY) { - result = baServices.writeProperty.decode(buffer, offset, length); - if (!result) return debug('Received invalid writeProperty message'); - this.emit('writeProperty', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.READ_PROPERTY_MULTIPLE) { - result = baServices.readPropertyMultiple.decode(buffer, offset, length); - if (!result) return debug('Received invalid readPropertyMultiple message'); - this.emit('readPropertyMultiple', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.WRITE_PROPERTY_MULTIPLE) { - result = baServices.writePropertyMultiple.decode(buffer, offset, length); - if (!result) return debug('Received invalid writePropertyMultiple message'); - this.emit('writePropertyMultiple', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.CONFIRMED_COV_NOTIFICATION) { - result = baServices.covNotify.decode(buffer, offset, length); - if (!result) return debug('Received invalid covNotify message'); - this.emit('covNotify', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.ATOMIC_WRITE_FILE) { - result = baServices.atomicWriteFile.decode(buffer, offset, length); - if (!result) return debug('Received invalid atomicWriteFile message'); - this.emit('atomicWriteFile', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.ATOMIC_READ_FILE) { - result = baServices.atomicReadFile.decode(buffer, offset, length); - if (!result) return debug('Received invalid atomicReadFile message'); - this.emit('atomicReadFile', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.SUBSCRIBE_COV) { - result = baServices.subscribeCov.decode(buffer, offset, length); - if (!result) return debug('Received invalid subscribeCOV message'); - this.emit('subscribeCOV', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.SUBSCRIBE_COV_PROPERTY) { - result = baServices.subscribeProperty.decode(buffer, offset, length); - if (!result) return debug('Received invalid subscribeProperty message'); - this.emit('subscribeProperty', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.DEVICE_COMMUNICATION_CONTROL) { - result = baServices.deviceCommunicationControl.decode(buffer, offset, length); - if (!result) return debug('Received invalid deviceCommunicationControl message'); - this.emit('deviceCommunicationControl', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.REINITIALIZE_DEVICE) { - result = baServices.reinitializeDevice.decode(buffer, offset, length); - if (!result) return debug('Received invalid reinitializeDevice message'); - this.emit('reinitializeDevice', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.CONFIRMED_EVENT_NOTIFICATION) { - result = baServices.eventNotifyData.decode(buffer, offset, length); - if (!result) return debug('Received invalid eventNotifyData message'); - this.emit('eventNotifyData', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.READ_RANGE) { - result = baServices.readRange.decode(buffer, offset, length); - if (!result) return debug('Received invalid readRange message'); - this.emit('readRange', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.CREATE_OBJECT) { - result = baServices.createObject.decode(buffer, offset, length); - if (!result) return debug('Received invalid createObject message'); - this.emit('createObject', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.DELETE_OBJECT) { - result = baServices.deleteObject.decode(buffer, offset, length); - if (!result) return debug('Received invalid deleteObject message'); - this.emit('deleteObject', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.ACKNOWLEDGE_ALARM) { - result = baServices.alarmAcknowledge.decode(buffer, offset, length); - if (!result) return debug('Received invalid alarmAcknowledge message'); - this.emit('alarmAcknowledge', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.GET_ALARM_SUMMARY) { - this.emit('getAlarmSummary', {address: address, invokeId: invokeId}); - } else if (service === baEnum.ConfirmedServiceChoice.GET_ENROLLMENT_SUMMARY) { - result = baServices.getEnrollmentSummary.decode(buffer, offset, length); - if (!result) return debug('Received invalid getEntrollmentSummary message'); - this.emit('getEntrollmentSummary', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.GET_EVENT_INFORMATION) { - result = baServices.getEventInformation.decode(buffer, offset, length); - if (!result) return debug('Received invalid getEventInformation message'); - this.emit('getEventInformation', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.LIFE_SAFETY_OPERATION) { - result = baServices.lifeSafetyOperation.decode(buffer, offset, length); - if (!result) return debug('Received invalid lifeSafetyOperation message'); - this.emit('lifeSafetyOperation', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.ADD_LIST_ELEMENT) { - result = baServices.addListElement.decode(buffer, offset, length); - if (!result) return debug('Received invalid addListElement message'); - this.emit('addListElement', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.REMOVE_LIST_ELEMENT) { - result = baServices.addListElement.decode(buffer, offset, length); - if (!result) return debug('Received invalid removeListElement message'); - this.emit('removeListElement', {address: address, invokeId: invokeId, request: result}); - } else if (service === baEnum.ConfirmedServiceChoice.CONFIRMED_PRIVATE_TRANSFER) { - result = baServices.privateTransfer.decode(buffer, offset, length); - if (!result) return debug('Received invalid privateTransfer message'); - this.emit('privateTransfer', {address: address, invokeId: invokeId, request: result}); - } else { - debug('Received unsupported confirmed service request'); + + const name = serviceMap[content.service]; + if (!name) { + debug('Received unsupported service request:', content.service); + return; } - } + debug.trace('Received service request:', name); - _processUnconfirmedServiceRequest(address, type, service, buffer, offset, length) { - let result; - debug('Handle this._processUnconfirmedServiceRequest'); - if (service === baEnum.UnconfirmedServiceChoice.I_AM) { - result = baServices.iAmBroadcast.decode(buffer, offset); - if (!result) return debug('Received invalid iAm message'); - - /** - * The iAm event represents the response to a whoIs request to detect all devices in a BACNET network. - * @event bacstack.iAm - * @param {object} device - An object representing the detected device. - * @param {string} device.address - The IP address of the detected device. - * @param {number} device.deviceId - The BACNET device-id of the detected device. - * @param {number} device.maxApdu - The max APDU size the detected device is supporting. - * @param {number} device.segmentation - The type of segmentation the detected device is supporting. - * @param {number} device.vendorId - The BACNET vendor-id of the detected device. - * @example - * const bacnet = require('bacstack'); - * const client = new bacnet(); - * - * client.on('iAm', (device) => { - * console.log('address: ', device.address, ' - deviceId: ', device.deviceId, ' - maxApdu: ', device.maxApdu, ' - segmentation: ', device.segmentation, ' - vendorId: ', device.vendorId); - * }); - */ - this.emit('iAm', {address: address, deviceId: result.deviceId, maxApdu: result.maxApdu, segmentation: result.segmentation, vendorId: result.vendorId}); - } else if (service === baEnum.UnconfirmedServiceChoice.WHO_IS) { - result = baServices.whoIs.decode(buffer, offset, length); - if (!result) return debug('Received invalid WhoIs message'); - - /** - * The whoIs event represents the request for an IAm reponse to detect all devices in a BACNET network. - * @event bacstack.whoIs - * @param {object} request - An object representing the received request. - * @param {string} request.address - The IP address of the device sending the request. - * @param {number=} request.lowLimit - The lower limit of the BACNET device-id. - * @param {number=} request.highLimit - The higher limit of the BACNET device-id. - * @example - * const bacnet = require('bacstack'); - * const client = new bacnet(); - * - * client.on('whoIs', (request) => { - * console.log('address: ', device.address, ' - lowLimit: ', device.lowLimit, ' - highLimit: ', device.highLimit); - * }); - */ - this.emit('whoIs', {address: address, lowLimit: result.lowLimit, highLimit: result.highLimit}); - } else if (service === baEnum.UnconfirmedServiceChoice.WHO_HAS) { - result = baServices.whoHas.decode(buffer, offset, length); - if (!result) return debug('Received invalid WhoHas message'); - this.emit('whoHas', {address: address, lowLimit: result.lowLimit, highLimit: result.highLimit, objectId: result.objectId, objectName: result.objectName}); - } else if (service === baEnum.UnconfirmedServiceChoice.UNCONFIRMED_COV_NOTIFICATION) { - result = baServices.covNotify.decode(buffer, offset, length); - if (!result) return debug('Received invalid covNotifyUnconfirmed message'); - this.emit('covNotifyUnconfirmed', {address: address, request: result}); - } else if (service === baEnum.UnconfirmedServiceChoice.TIME_SYNCHRONIZATION) { - result = baServices.timeSync.decode(buffer, offset, length); - if (!result) return debug('Received invalid TimeSync message'); - - /** - * The timeSync event represents the request to synchronize the local time to the received time. - * @event bacstack.timeSync - * @param {object} request - An object representing the received request. - * @param {string} request.address - The IP address of the device sending the request. - * @param {date} request.dateTime - The time to be synchronized to. - * @example - * const bacnet = require('bacstack'); - * const client = new bacnet(); - * - * client.on('timeSync', (request) => { - * console.log('address: ', device.address, ' - dateTime: ', device.dateTime); - * }); - */ - this.emit('timeSync', {address: address, dateTime: result.dateTime}); - } else if (service === baEnum.UnconfirmedServiceChoice.UTC_TIME_SYNCHRONIZATION) { - result = baServices.timeSync.decode(buffer, offset, length); - if (!result) return debug('Received invalid TimeSyncUTC message'); - - /** - * The timeSyncUTC event represents the request to synchronize the local time to the received UTC time. - * @event bacstack.timeSyncUTC - * @param {object} request - An object representing the received request. - * @param {string} request.address - The IP address of the device sending the request. - * @param {date} request.dateTime - The time to be synchronized to. - * @example - * const bacnet = require('bacstack'); - * const client = new bacnet(); - * - * client.on('timeSyncUTC', (request) => { - * console.log('address: ', device.address, ' - dateTime: ', device.dateTime); - * }); - */ - this.emit('timeSyncUTC', {address: address, dateTime: result.dateTime}); - } else if (service === baEnum.UnconfirmedServiceChoice.UNCONFIRMED_EVENT_NOTIFICATION) { - result = baServices.eventNotifyData.decode(buffer, offset, length); - if (!result) return debug('Received invalid EventNotify message'); - this.emit('eventNotify', {address: address, eventData: result.eventData}); - } else if (service === baEnum.UnconfirmedServiceChoice.I_HAVE) { - result = baServices.iHaveBroadcast.decode(buffer, offset, length); - if (!result) return debug('Received invalid ihaveBroadcast message'); - this.emit('ihaveBroadcast', {address: address, eventData: result.eventData}); - } else if (service === baEnum.UnconfirmedServiceChoice.UNCONFIRMED_PRIVATE_TRANSFER) { - result = baServices.privateTransfer.decode(buffer, offset, length); - if (!result) return debug('Received invalid privateTransfer message'); - this.emit('privateTransfer', {address: address, eventData: result.eventData}); + // Find a function to decode the packet. + const serviceHandler = baServices[name]; + if (serviceHandler) { + try { + content.payload = serviceHandler.decode(buffer, offset, length); + } catch (e) { + // Sometimes incomplete or corrupted messages will cause exceptions + // during decoding, but we don't want these to terminate the program, so + // we'll just log them and ignore them. + debug('Exception thrown when processing message:', e); + debug('Original message was', name + ':', content); + return; + } + if (!content.payload) return debug('Received invalid', name, 'message'); } else { - debug('Received unsupported unconfirmed service request'); + debug('No serviceHandler defined for:', name); + // Call the callback anyway, just with no payload. + } + debug.trace('Passing payload over to callback:', content); + + // Call the user code, if they've defined a callback. + if (!this.emit(name, content)) { + // No callback was defined + if (!this.emit('unhandledEvent', content)) { + // No 'unhandled event' handler, so respond with an error ourselves. + // This is better than doing nothing, which can often make the other + // device think we have gone offline. + if (content.header.expectingReply) { + debug('Replying with error for unhandled service:', name); + // Make sure we don't reply pretending to be the caller, if we got a + // forwarded message! Really this should be overridden to be your + // own IP, but only if it's not null/undefined to begin with. + content.header.sender.forwardedFrom = null; + this.errorResponse( + content.header.sender, + content.service, + content.invokeId, + baEnum.ErrorClass.SERVICES, + baEnum.ErrorCode.REJECT_UNRECOGNIZED_SERVICE + ); + } + } } } - _handlePdu(address, type, buffer, offset, length) { - let result; + _handlePdu(buffer, offset, length, header) { + let msg; // Handle different PDU types - switch (type & baEnum.PDU_TYPE_MASK) { + switch (header.apduType & baEnum.PDU_TYPE_MASK) { case baEnum.PduTypes.UNCONFIRMED_REQUEST: - result = baApdu.decodeUnconfirmedServiceRequest(buffer, offset); - this._processUnconfirmedServiceRequest(address, result.type, result.service, buffer, offset + result.len, length - result.len); + msg = baApdu.decodeUnconfirmedServiceRequest(buffer, offset); + msg.header = header; + msg.header.confirmedService = false; + this._processServiceRequest(unconfirmedServiceMap, msg, buffer, offset + msg.len, length - msg.len); break; case baEnum.PduTypes.SIMPLE_ACK: - result = baApdu.decodeSimpleAck(buffer, offset); - offset += result.len; - length -= result.len; - this._invokeCallback(result.invokeId, null, {result: result, buffer: buffer, offset: offset + result.len, length: length - result.len}); + msg = baApdu.decodeSimpleAck(buffer, offset); + offset += msg.len; + length -= msg.len; + this._invokeCallback(msg.invokeId, null, {msg: msg, buffer: buffer, offset: offset + msg.len, length: length - msg.len}); break; case baEnum.PduTypes.COMPLEX_ACK: - result = baApdu.decodeComplexAck(buffer, offset); - if ((type & baEnum.PduConReqBits.SEGMENTED_MESSAGE) === 0) { - this._invokeCallback(result.invokeId, null, {result: result, buffer: buffer, offset: offset + result.len, length: length - result.len}); + msg = baApdu.decodeComplexAck(buffer, offset); + if ((header.apduType & baEnum.PduConReqBits.SEGMENTED_MESSAGE) === 0) { + this._invokeCallback(msg.invokeId, null, {msg: msg, buffer: buffer, offset: offset + msg.len, length: length - msg.len}); } else { - this._processSegment(address, result.type, result.service, result.invokeId, baEnum.MaxSegmentsAccepted.SEGMENTS_0, baEnum.MaxApduLengthAccepted.OCTETS_50, false, result.sequencenumber, result.proposedWindowNumber, buffer, offset + result.len, length - result.len); + this._processSegment(address, msg.type, msg.service, msg.invokeId, baEnum.MaxSegmentsAccepted.SEGMENTS_0, baEnum.MaxApduLengthAccepted.OCTETS_50, false, msg.sequencenumber, msg.proposedWindowNumber, buffer, offset + msg.len, length - msg.len); } break; case baEnum.PduTypes.SEGMENT_ACK: - result = baApdu.decodeSegmentAck(buffer, offset); - //m_last_segment_ack.Set(address, result.originalInvokeId, result.sequencenumber, result.actualWindowSize); - //this._processSegmentAck(address, result.type, result.originalInvokeId, result.sequencenumber, result.actualWindowSize, buffer, offset + result.len, length - result.len); + msg = baApdu.decodeSegmentAck(buffer, offset); + //m_last_segment_ack.Set(address, msg.originalInvokeId, msg.sequencenumber, msg.actualWindowSize); + //this._processSegmentAck(address, msg.type, msg.originalInvokeId, msg.sequencenumber, msg.actualWindowSize, buffer, offset + msg.len, length - msg.len); break; case baEnum.PduTypes.ERROR: - result = baApdu.decodeError(buffer, offset); - this._processError(result.invokeId, buffer, offset + result.len, length - result.len); + msg = baApdu.decodeError(buffer, offset); + this._processError(msg.invokeId, buffer, offset + msg.len, length - msg.len); break; case baEnum.PduTypes.REJECT: case baEnum.PduTypes.ABORT: - result = baApdu.decodeAbort(buffer, offset); - this._processAbort(result.invokeId, result.reason); + msg = baApdu.decodeAbort(buffer, offset); + this._processAbort(msg.invokeId, msg.reason); break; case baEnum.PduTypes.CONFIRMED_REQUEST: - result = baApdu.decodeConfirmedServiceRequest(buffer, offset); - if ((type & baEnum.PduConReqBits.SEGMENTED_MESSAGE) === 0) { - this._processConfirmedServiceRequest(address, result.type, result.service, result.maxSegments, result.maxApdu, result.invokeId, buffer, offset + result.len, length - result.len); + msg = baApdu.decodeConfirmedServiceRequest(buffer, offset); + msg.header = header; + msg.header.confirmedService = true; + if ((header.apduType & baEnum.PduConReqBits.SEGMENTED_MESSAGE) === 0) { + this._processServiceRequest(confirmedServiceMap, msg, buffer, offset + msg.len, length - msg.len); } else { - this._processSegment(address, result.type, result.service, result.invokeId, result.maxSegments, result.maxApdu, true, result.sequencenumber, result.proposedWindowNumber, buffer, offset + result.len, length - result.len); + this._processSegment(msg, true, buffer, offset + result.len, length - result.len); } break; default: - debug('Received unknown PDU type -> Drop package'); + debug(`Received unknown PDU type ${header.apduType} -> Drop packet`); break; } } - _handleNpdu(buffer, offset, msgLength, remoteAddress) { + _handleNpdu(buffer, offset, msgLength, header) { // Check data length - if (msgLength <= 0) return debug('No NPDU data -> Drop package'); + if (msgLength <= 0) return debug.trace('No NPDU data -> Drop package'); // Parse baNpdu header const result = baNpdu.decode(buffer, offset); - if (!result) return debug('Received invalid NPDU header -> Drop package'); + if (!result) return debug.trace('Received invalid NPDU header -> Drop package'); if (result.funct & baEnum.NpduControlBits.NETWORK_LAYER_MESSAGE) { - return debug('Received network layer message -> Drop package'); + return debug.trace('Received network layer message -> Drop package'); } offset += result.len; msgLength -= result.len; - if (msgLength <= 0) return debug('No APDU data -> Drop package'); - const apduType = baApdu.getDecodedType(buffer, offset); - this._handlePdu(remoteAddress, apduType, buffer, offset, msgLength); + if (msgLength <= 0) return debug.trace('No APDU data -> Drop package'); + header.apduType = baApdu.getDecodedType(buffer, offset); + header.expectingReply = !!(result.funct & baEnum.NpduControlBits.EXPECTING_REPLY); + this._handlePdu(buffer, offset, msgLength, header); } _receiveData(buffer, remoteAddress) { // Check data length - if (buffer.length < baBvlc.BVLC_HEADER_LENGTH) return debug('Received invalid data -> Drop package'); + if (buffer.length < baBvlc.BVLC_HEADER_LENGTH) return debug.trace('Received invalid data -> Drop package'); // Parse BVLC header const result = baBvlc.decode(buffer, 0); - if (!result) return debug('Received invalid BVLC header -> Drop package'); + if (!result) return debug.trace('Received invalid BVLC header -> Drop package'); + let header = { + // Which function the packet came in on, so later code can distinguish + // between ORIGINAL_BROADCAST_NPDU and DISTRIBUTE_BROADCAST_TO_NETWORK. + func: result.func, + sender: { + // Address of the host we are directly connected to. String, IP:port. + address: remoteAddress, + // If the host is a BBMD passing messages along to another node, this + // is the address of the distant BACnet node. String, IP:port. + // Typically we won't have network connectivity to this address, but + // we have to include it in replies so the host we are connect to knows + // where to forward the messages. + forwardedFrom: null, + }, + }; // Check BVLC function - if (result.func === baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU || result.func === baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU || result.func === baEnum.BvlcResultPurpose.FORWARDED_NPDU) { - this._handleNpdu(buffer, result.len, buffer.length - result.len, remoteAddress); - } else { - debug('Received unknown BVLC function -> Drop package'); + switch (result.func) { + case baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU: + case baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU: + this._handleNpdu(buffer, result.len, buffer.length - result.len, header); + break; + case baEnum.BvlcResultPurpose.FORWARDED_NPDU: + // Preserve the IP of the node behind the BBMD so we know where to send + // replies back to. + header.sender.forwardedFrom = result.originatingIP; + this._handleNpdu(buffer, result.len, buffer.length - result.len, header); + break; + case baEnum.BvlcResultPurpose.REGISTER_FOREIGN_DEVICE: + let decodeResult = baServices.registerForeignDevice.decode(buffer, result.len, buffer.length - result.len); + if (!decodeResult) return debug.trace('Received invalid registerForeignDevice message'); + this.emit('registerForeignDevice', { + header: header, + payload: decodeResult, + }); + break; + case baEnum.BvlcResultPurpose.DISTRIBUTE_BROADCAST_TO_NETWORK: + this._handleNpdu(buffer, result.len, buffer.length - result.len, header); + break; + default: + debug('Received unknown BVLC function ' + result.func + ' -> Drop package'); + break; } } _receiveError(err) { - /** * @event bacstack.error * @param {error} err - The error object thrown by the underlying transport layer. @@ -482,20 +419,16 @@ class Client extends EventEmitter { * * client.whoIs(); */ - whoIs(options) { - options = options || {}; + whoIs(receiver, payload = {}) { const settings = { - lowLimit: options.lowLimit, - highLimit: options.highLimit, - address: options.address || this._transport.getBroadcastAddress() + lowLimit: payload.lowLimit, + highLimit: payload.highLimit, }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, this._settings.address, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduTypes.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.WHO_IS); baServices.whoIs.encode(buffer, settings.lowLimit, settings.highLimit); - const npduType = (this._settings.address !== this._transport.getBroadcastAddress()) ? baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU : baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU; - baBvlc.encode(buffer.buffer, npduType, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, settings.address); + this.sendBvlc(receiver, buffer); } /** @@ -509,14 +442,12 @@ class Client extends EventEmitter { * * client.timeSync('192.168.1.43', new Date()); */ - timeSync(address, dateTime) { - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, address); + timeSync(receiver, dateTime) { + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduTypes.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.TIME_SYNCHRONIZATION); baServices.timeSync.encode(buffer, dateTime); - const npduType = (address !== this._transport.getBroadcastAddress()) ? baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU : baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU; - baBvlc.encode(buffer.buffer, npduType, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); } /** @@ -530,14 +461,12 @@ class Client extends EventEmitter { * * client.timeSyncUTC('192.168.1.43', new Date()); */ - timeSyncUTC(address, dateTime) { - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, address); + timeSyncUTC(receiver, dateTime) { + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduTypes.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.UTC_TIME_SYNCHRONIZATION); baServices.timeSync.encode(buffer, dateTime); - const npduType = (address !== this._transport.getBroadcastAddress()) ? baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU : baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU; - baBvlc.encode(buffer.buffer, npduType, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); } /** @@ -562,7 +491,7 @@ class Client extends EventEmitter { * console.log('value: ', value); * }); */ - readProperty(address, objectId, propertyId, options, next) { + readProperty(receiver, objectId, propertyId, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, @@ -570,13 +499,12 @@ class Client extends EventEmitter { invokeId: options.invokeId || this._getInvokeId(), arrayIndex: options.arrayIndex || baEnum.ASN1_ARRAY_ALL }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); const type = baEnum.PduTypes.CONFIRMED_REQUEST | (settings.maxSegments !== baEnum.MaxSegmentsAccepted.SEGMENTS_0 ? baEnum.PduConReqBits.SEGMENTED_RESPONSE_ACCEPTED : 0); baApdu.encodeConfirmedServiceRequest(buffer, type, baEnum.ConfirmedServiceChoice.READ_PROPERTY, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.readProperty.encode(buffer, objectId.type, objectId.instance, propertyId, settings.arrayIndex); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.readProperty.decodeAcknowledge(data.buffer, data.offset, data.length); @@ -613,7 +541,7 @@ class Client extends EventEmitter { * console.log('value: ', value); * }); */ - writeProperty(address, objectId, propertyId, values, options, next) { + writeProperty(receiver, objectId, propertyId, values, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, @@ -622,12 +550,11 @@ class Client extends EventEmitter { arrayIndex: options.arrayIndex || baEnum.ASN1_ARRAY_ALL, priority: options.priority }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.WRITE_PROPERTY, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.writeProperty.encode(buffer, objectId.type, objectId.instance, propertyId, settings.arrayIndex, settings.priority, values); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { next(err); }); @@ -659,20 +586,19 @@ class Client extends EventEmitter { * console.log('value: ', value); * }); */ - readPropertyMultiple(address, propertiesArray, options, next) { + readPropertyMultiple(receiver, propertiesArray, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); const type = baEnum.PduTypes.CONFIRMED_REQUEST | (settings.maxSegments !== baEnum.MaxSegmentsAccepted.SEGMENTS_0 ? baEnum.PduConReqBits.SEGMENTED_RESPONSE_ACCEPTED : 0); baApdu.encodeConfirmedServiceRequest(buffer, type, baEnum.ConfirmedServiceChoice.READ_PROPERTY_MULTIPLE, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.readPropertyMultiple.encode(buffer, propertiesArray); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.readPropertyMultiple.decodeAcknowledge(data.buffer, data.offset, data.length); @@ -715,24 +641,88 @@ class Client extends EventEmitter { * console.log('value: ', value); * }); */ - writePropertyMultiple(address, values, options, next) { + writePropertyMultiple(receiver, values, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.WRITE_PROPERTY_MULTIPLE, settings.maxSegments, settings.maxApdu, settings.invokeId); baServices.writePropertyMultiple.encodeObject(buffer, values); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { next(err); }); } + /** + * The confirmedCOVNotification command is used to push notifications to other + * systems that have registered with us via a subscribeCOV message. + * @function bacstack.confirmedCOVNotification + * @param {string} address - IP address of the target device. + * @param {object} monitoredObject - The object being monitored, from subscribeCOV. + * @param {number} monitoredObject.type - Object type. + * @param {number} monitoredObject.instance - Object instance. + * @param {number} subscribeId - Subscriber ID from subscribeCOV, + * @param {number} initiatingDeviceId - Our BACnet device ID. + * @param {number} lifetime - Number of seconds left until the subscription expires. + * @param {array} values - values for the monitored object. See example. + * @param {object=} options + * @param {MaxSegmentsAccepted=} options.maxSegments - The maximimal allowed number of segments. + * @param {MaxApduLengthAccepted=} options.maxApdu - The maximal allowed APDU size. + * @param {number=} options.invokeId - The invoke ID of the confirmed service telegram. + * @param {function} next - The callback containing an error, in case of a failure and value object in case of success. + * @example + * const bacnet = require('bacstack'); + * const client = new bacnet(); + * + * const settings = {deviceId: 123}; // our BACnet device + * + * // Items saved from subscribeCOV message + * const monitoredObject = {type: 1, instance: 1}; + * const subscriberProcessId = 123; + * + * client.confirmedCOVNotification( + * '192.168.1.43', + * monitoredObject, + * subscriberProcessId, + * settings.deviceId, + * 30, // should be lifetime of subscription really + * [ + * { + * property: { id: bacnet.enum.PropertyIdentifier.PRESENT_VALUE }, + * value: [ + * {value: 123, type: bacnet.enum.ApplicationTags.REAL}, + * ], + * }, + * ], + * (err) => { + * console.log('error: ', err); + * } + * ); + */ + confirmedCOVNotification(receiver, monitoredObject, subscribeId, initiatingDeviceId, lifetime, values, options, next) { + next = next || options; + const settings = { + maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, + maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, + invokeId: options.invokeId || this._getInvokeId() + }; + const buffer = this._getBuffer(); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); + 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); + this.sendBvlc(receiver, buffer); + this._addCallback(settings.invokeId, (err, data) => { + if (err) return next(err); + next(); + }); + } + /** * The deviceCommunicationControl command enables or disables network communication of the target device. * @function bacstack.deviceCommunicationControl @@ -753,7 +743,7 @@ class Client extends EventEmitter { * console.log('error: ', err); * }); */ - deviceCommunicationControl(address, timeDuration, enableDisable, options, next) { + deviceCommunicationControl(receiver, timeDuration, enableDisable, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, @@ -761,12 +751,11 @@ class Client extends EventEmitter { invokeId: options.invokeId || this._getInvokeId(), password: options.password }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.DEVICE_COMMUNICATION_CONTROL, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.deviceCommunicationControl.encode(buffer, timeDuration, enableDisable, settings.password); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { next(err); }); @@ -791,7 +780,7 @@ class Client extends EventEmitter { * console.log('value: ', value); * }); */ - reinitializeDevice(address, state, options, next) { + reinitializeDevice(receiver, state, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, @@ -799,30 +788,28 @@ class Client extends EventEmitter { invokeId: options.invokeId || this._getInvokeId(), password: options.password }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.REINITIALIZE_DEVICE, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.reinitializeDevice.encode(buffer, state, settings.password); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { next(err); }); } - writeFile(address, objectId, position, fileBuffer, options, next) { + writeFile(receiver, objectId, position, fileBuffer, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.ATOMIC_WRITE_FILE, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.atomicWriteFile.encode(buffer, false, objectId, position, fileBuffer); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.atomicWriteFile.decodeAcknowledge(data.buffer, data.offset, data.length); @@ -831,19 +818,18 @@ class Client extends EventEmitter { }); } - readFile(address, objectId, position, count, options, next) { + readFile(receiver, objectId, position, count, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.ATOMIC_READ_FILE, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.atomicReadFile.encode(buffer, true, objectId, position, count); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.atomicReadFile.decodeAcknowledge(data.buffer, data.offset, data.length); @@ -852,19 +838,18 @@ class Client extends EventEmitter { }); } - readRange(address, objectId, idxBegin, quantity, options, next) { + readRange(receiver, objectId, idxBegin, quantity, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.READ_RANGE, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.readRange.encode(buffer, objectId, baEnum.PropertyIdentifier.LOG_BUFFER, baEnum.ASN1_ARRAY_ALL, baEnum.ReadRangeType.BY_POSITION, idxBegin, new Date(), quantity); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.readRange.decodeAcknowledge(data.buffer, data.offset, data.length); @@ -873,132 +858,125 @@ class Client extends EventEmitter { }); } - subscribeCOV(address, objectId, subscribeId, cancel, issueConfirmedNotifications, lifetime, options, next) { + subscribeCov(receiver, objectId, subscribeId, cancel, issueConfirmedNotifications, lifetime, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.SUBSCRIBE_COV, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.subscribeCov.encode(buffer, subscribeId, objectId, cancel, issueConfirmedNotifications, lifetime); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); }); } - subscribeProperty(address, objectId, monitoredProperty, subscribeId, cancel, issueConfirmedNotifications, options, next) { + subscribeProperty(receiver, objectId, monitoredProperty, subscribeId, cancel, issueConfirmedNotifications, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.SUBSCRIBE_COV_PROPERTY, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.subscribeProperty.encode(buffer, subscribeId, objectId, cancel, issueConfirmedNotifications, 0, monitoredProperty, false, 0x0f); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); }); } - createObject(address, objectId, values, options, next) { + createObject(receiver, objectId, values, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.CREATE_OBJECT, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.createObject.encode(buffer, objectId, values); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); }); } - deleteObject(address, objectId, options, next) { + deleteObject(receiver, objectId, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.DELETE_OBJECT, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.deleteObject.encode(buffer, objectId); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); }); } - removeListElement(address, objectId, reference, values, options, next) { + removeListElement(receiver, objectId, reference, values, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.REMOVE_LIST_ELEMENT, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.addListElement.encode(buffer, objectId, reference.id, reference.index, values); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); }); } - addListElement(address, objectId, reference, values, options, next) { + addListElement(receiver, objectId, reference, values, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.ADD_LIST_ELEMENT, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.addListElement.encode(buffer, objectId, reference.id, reference.index, values); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); }); } - getAlarmSummary(address, options, next) { + getAlarmSummary(receiver, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.GET_ALARM_SUMMARY, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.alarmSummary.decode(data.buffer, data.offset, data.length); @@ -1007,19 +985,18 @@ class Client extends EventEmitter { }); } - getEventInformation(address, objectId, options, next) { + getEventInformation(receiver, objectId, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.GET_EVENT_INFORMATION, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baAsn1.encodeContextObjectId(buffer, 0, objectId.type, objectId.instance); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.eventInformation.decode(data.buffer, data.offset, data.length); @@ -1028,66 +1005,62 @@ class Client extends EventEmitter { }); } - acknowledgeAlarm(address, objectId, eventState, ackText, evTimeStamp, ackTimeStamp, options, next) { + acknowledgeAlarm(receiver, objectId, eventState, ackText, evTimeStamp, ackTimeStamp, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.ACKNOWLEDGE_ALARM, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.alarmAcknowledge.encode(buffer, 57, objectId, eventState, ackText, evTimeStamp, ackTimeStamp); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); }); } - confirmedPrivateTransfer(address, vendorId, serviceNumber, data, options, next) { + confirmedPrivateTransfer(receiver, vendorId, serviceNumber, data, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.CONFIRMED_PRIVATE_TRANSFER, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.privateTransfer.encode(buffer, vendorId, serviceNumber, data); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); }); } - unconfirmedPrivateTransfer(address, vendorId, serviceNumber, data) { - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, address); + unconfirmedPrivateTransfer(receiver, vendorId, serviceNumber, data) { + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduTypes.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.UNCONFIRMED_PRIVATE_TRANSFER); baServices.privateTransfer.encode(buffer, vendorId, serviceNumber, data); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); } - getEnrollmentSummary(address, acknowledgmentFilter, options, next) { + getEnrollmentSummary(receiver, acknowledgmentFilter, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.GET_ENROLLMENT_SUMMARY, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.getEnrollmentSummary.encode(buffer, acknowledgmentFilter, options.enrollmentFilter, options.eventStateFilter, options.eventTypeFilter, options.priorityFilter, options.notificationClassFilter); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.getEnrollmentSummary.decodeAcknowledge(data.buffer, data.offset, data.length); @@ -1096,28 +1069,26 @@ class Client extends EventEmitter { }); } - unconfirmedEventNotification(address, eventNotification) { - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, address); + unconfirmedEventNotification(receiver, eventNotification) { + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduTypes.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.UNCONFIRMED_EVENT_NOTIFICATION); baServices.eventNotifyData.encode(buffer, eventNotification); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); } - confirmedEventNotification(address, eventNotification, options, next) { + confirmedEventNotification(receiver, eventNotification, options, next) { next = next || options; const settings = { maxSegments: options.maxSegments || baEnum.MaxSegmentsAccepted.SEGMENTS_65, maxApdu: options.maxApdu || baEnum.MaxApduLengthAccepted.OCTETS_1476, invokeId: options.invokeId || this._getInvokeId() }; - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduTypes.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.CONFIRMED_EVENT_NOTIFICATION, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.eventNotifyData.encode(buffer, eventNotification); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, address); + this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); next(); @@ -1125,57 +1096,106 @@ 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 buffer = this._getBuffer(receiver && receiver.forwardedFrom); 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); - this._transport.send(buffer.buffer, buffer.offset, receiver); + this.sendBvlc(receiver, buffer); } readPropertyMultipleResponse(receiver, invokeId, values) { - const buffer = this._getBuffer(); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); 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); - this._transport.send(buffer.buffer, buffer.offset, receiver); + this.sendBvlc(receiver, buffer); } - 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 {object} receiver - address to send packet to, null for local broadcast. + * @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 talking to a BBMD, the IP address this message originally came from. The recipient may then try to contact the device directly using this IP. + * @param {string=} options.address - Where to send the packet to. Normally this will be the default value of null which will broadcast to the local subnet, but if communicating with a BBMD, the BBMD's address will go here. An object like {net: 65535} is also permitted. + * @param {number=} options.hops - Number of hops until packet should be dropped, default 255. + */ + iAmResponse(receiver, deviceId, segmentation, vendorId) { + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); 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()); + baServices.iAm.encode(buffer, deviceId, this._transport.getMaxPayload(), segmentation, vendorId); + this.sendBvlc(receiver, buffer); } - iHaveResponse(deviceId, objectId, objectName) { - const buffer = this._getBuffer(); - baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, this._transport.getBroadcastAddress()); + iHaveResponse(receiver, deviceId, objectId, objectName) { + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); + baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.EecodeUnconfirmedServiceRequest(buffer, baEnum.PduTypes.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.I_HAVE); - baServices.EncodeIhaveBroadcast(buffer, deviceId, objectId, objectName); - baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU, buffer.offset); - this._transport.send(buffer.buffer, buffer.offset, this._transport.getBroadcastAddress()); + baServices.iHave(buffer, deviceId, objectId, objectName); + this.sendBvlc(receiver, buffer); } simpleAckResponse(receiver, service, invokeId) { - const buffer = this._getBuffer(); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); 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); - this._transport.send(buffer.buffer, buffer.offset, receiver); + this.sendBvlc(receiver, buffer); } errorResponse(receiver, service, invokeId, errorClass, errorCode) { - const buffer = this._getBuffer(); + const buffer = this._getBuffer(receiver && receiver.forwardedFrom); 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); - this._transport.send(buffer.buffer, buffer.offset, receiver); + this.sendBvlc(receiver, buffer); + } + + sendBvlc(receiver, buffer) { + if (receiver && receiver.forwardedFrom) { + // Remote node address given, forward to BBMD + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.FORWARDED_NPDU, buffer.offset, receiver.forwardedFrom); + } else if (receiver && receiver.address) { + // Specific address, unicast + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset); + } else { + // No address, broadcast + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU, buffer.offset); + } + this._transport.send( + buffer.buffer, + buffer.offset, + (receiver && receiver.address) || null + ); + } + + /** + * 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); + baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.BVLC_RESULT, buffer.offset); + this._transport.send(buffer.buffer, buffer.offset, receiver.address); } /** @@ -1190,5 +1210,43 @@ class Client extends EventEmitter { close() { this._transport.close(); } + + /** + * Helper function to take an array of enums and produce a bitstring suitable + * for inclusion as a property. + * + * @example + * [bacnet.enum.PropertyIdentifier.PROTOCOL_OBJECT_TYPES_SUPPORTED]: [ + * {value: bacnet.createBitstring([ + * bacnet.enum.ObjectTypesSupported.ANALOG_INPUT, + * bacnet.enum.ObjectTypesSupported.ANALOG_OUTPUT, + * ]), + * type: bacnet.enum.ApplicationTags.BIT_STRING}, + * ], + */ + static createBitstring(items) { + let offset = 0; + let bytes = []; + let bitsUsed = 0; + while (items.length) { + // Find any values between offset and offset+8, for the next byte + let value = 0; + items = items.filter(i => { + if (i >= offset + 8) return true; // leave for future iteration + value |= 1 << (i - offset); + bitsUsed = Math.max(bitsUsed, i); + return false; // remove from list + }); + bytes.push(value); + offset += 8; + } + bitsUsed++; + + return { + value: bytes, + bitsUsed: bitsUsed, + }; + } + } module.exports = Client; diff --git a/lib/enum.js b/lib/enum.js index 842b5702..6846fa1c 100644 --- a/lib/enum.js +++ b/lib/enum.js @@ -1,5 +1,29 @@ 'use strict'; +/** + * Turn an enum into a string suitable for debugging. + * + * @param object group + * Enum group, e.g. bacnet.enum.ConfirmedServiceChoice. + * + * @param Number value + * Enum value, e.g. 1. Note that this *must* be an integer value, so you may + * need to use parseInt(). Non-integer values will result in an exception. + * + * @example + * const s = bacnet.enum.getEnumName( + * bacnet.enum.PropertyIdentifier, + * bacnet.enum.PropertyIdentifier.PRESENT_VALUE + * ); + * console.log(s); // "PRESENT_VALUE(85)" + */ +module.exports.getEnumName = function(group, value) { + if (!Number.isInteger(value)) { + throw new Error('getEnumName() can only be passed an integer value, was given "' + value + '"'); + } + return Object.keys(group).find(key => group[key] === value) + '(' + value + ')'; +} + module.exports.PDU_TYPE_MASK = 0xF0; module.exports.ASN1_MAX_OBJECT = 0x3FF; module.exports.ASN1_INSTANCE_BITS = 22; diff --git a/lib/npdu.js b/lib/npdu.js index e4c8f971..43f315bb 100644 --- a/lib/npdu.js +++ b/lib/npdu.js @@ -2,6 +2,7 @@ const baEnum = require('./enum'); +const DEFAULT_HOP_COUNT = 0xFF; const BACNET_PROTOCOL_VERSION = 1; const BacnetAddressTypes = { NONE: 0, @@ -103,7 +104,7 @@ module.exports.encode = (buffer, funct, destination, source, hopCount, networkMs } if (hasDestination) { - buffer.buffer[buffer.offset++] = hopCount; + buffer.buffer[buffer.offset++] = hopCount || DEFAULT_HOP_COUNT; } if ((funct & baEnum.NpduControlBits.NETWORK_LAYER_MESSAGE) > 0) { diff --git a/lib/services/i-am-broadcast.js b/lib/services/i-am.js similarity index 70% rename from lib/services/i-am-broadcast.js rename to lib/services/i-am.js index 688a974a..542e6f50 100644 --- a/lib/services/i-am-broadcast.js +++ b/lib/services/i-am.js @@ -1,3 +1,28 @@ +/** + * The iAm event represents the response to a whoIs request to detect all + * devices in a BACNET network. + * + * @event bacstack.iAm + * @param {number} deviceId - The BACNET device-id of the detected device. + * @param {number} maxApdu - The max APDU size the detected device supports. + * @param {number} segmentation - The type of segmentation the detected device supports. + * @param {number} vendorId - The BACNET vendor-id of the detected device. + * + * @example + * const bacnet = require('bacstack'); + * const client = new bacnet(); + * + * client.on('iAm', (msg) => { + * console.log( + * 'address: ', msg.header.address, + * ' - deviceId: ', msg.payload.deviceId, + * ' - maxApdu: ', msg.payload.maxApdu, + * ' - segmentation: ', msg.payload.segmentation, + * ' - vendorId: ', msg.payload.vendorId + * ); + * }); + */ + 'use strict'; const baAsn1 = require('../asn1'); diff --git a/lib/services/i-have-broadcast.js b/lib/services/i-have.js similarity index 100% rename from lib/services/i-have-broadcast.js rename to lib/services/i-have.js diff --git a/lib/services/index.js b/lib/services/index.js index a8cbb9cf..9d4a6431 100644 --- a/lib/services/index.js +++ b/lib/services/index.js @@ -6,6 +6,7 @@ module.exports.alarmSummary = require('./alarm-summary'); module.exports.atomicReadFile = require('./atomic-read-file'); module.exports.atomicWriteFile = require('./atomic-write-file'); module.exports.covNotify = require('./cov-notify'); +module.exports.covNotifyUnconfirmed = module.exports.covNotify; module.exports.createObject = require('./create-object'); module.exports.deleteObject = require('./delete-object'); module.exports.deviceCommunicationControl = require('./device-communication-control'); @@ -14,18 +15,20 @@ module.exports.eventInformation = require('./event-information'); module.exports.eventNotifyData = require('./event-notify-data'); module.exports.getEnrollmentSummary = require('./get-enrollment-summary'); module.exports.getEventInformation = require('./get-event-information'); -module.exports.iAmBroadcast = require('./i-am-broadcast'); -module.exports.iHaveBroadcast = require('./i-have-broadcast'); +module.exports.iAm = require('./i-am'); +module.exports.iHave = require('./i-have'); module.exports.lifeSafetyOperation = require('./life-safety-operation'); module.exports.privateTransfer = require('./private-transfer'); -module.exports.readPropertyMultiple = require('./read-property-multiple'); module.exports.readProperty = require('./read-property'); +module.exports.readPropertyMultiple = require('./read-property-multiple'); module.exports.readRange = require('./read-range'); +module.exports.registerForeignDevice = require('./register-foreign-device'); module.exports.reinitializeDevice = require('./reinitialize-device'); module.exports.subscribeCov = require('./subscribe-cov'); module.exports.subscribeProperty = require('./subscribe-property'); module.exports.timeSync = require('./time-sync'); +module.exports.timeSyncUTC = module.exports.timeSync; module.exports.whoHas = require('./who-has'); module.exports.whoIs = require('./who-is'); -module.exports.writePropertyMultiple = require('./write-property-multiple'); module.exports.writeProperty = require('./write-property'); +module.exports.writePropertyMultiple = require('./write-property-multiple'); diff --git a/lib/services/register-foreign-device.js b/lib/services/register-foreign-device.js new file mode 100644 index 00000000..7ea4aa2b --- /dev/null +++ b/lib/services/register-foreign-device.js @@ -0,0 +1,18 @@ +'use strict'; + +const baAsn1 = require('../asn1'); + +module.exports.encode = (buffer, ttl) => { + baAsn1.encodeUnsigned(buffer, ttl, 2); +}; + +module.exports.decode = (buffer, offset, length) => { + let len = 0; + let result; + result = baAsn1.decodeUnsigned(buffer, offset + len, 2); + len += result.len; + return { + len: len, + ttl: result.value, + }; +}; diff --git a/lib/services/time-sync.js b/lib/services/time-sync.js index f3d3903f..826682b5 100644 --- a/lib/services/time-sync.js +++ b/lib/services/time-sync.js @@ -1,3 +1,22 @@ +/** + * The timeSync event represents the request to synchronize the local time to + * the received time. + * + * @event bacstack.timeSync + * @param {date} dateTime - The time to be synchronized to. + * + * @example + * const bacnet = require('bacstack'); + * const client = new bacnet(); + * + * client.on('timeSync', (msg) => { + * console.log( + * 'address: ', msg.header.address, + * ' - dateTime: ', msg.payload.dateTime + * ); + * }); + */ + 'use strict'; const baAsn1 = require('../asn1'); diff --git a/lib/services/who-is.js b/lib/services/who-is.js index a7c473c8..aa157a8c 100644 --- a/lib/services/who-is.js +++ b/lib/services/who-is.js @@ -1,3 +1,24 @@ +/** + * The whoIs event represents the request for an IAm reponse to detect all + * devices in a BACNET network. + * + * @event bacstack.whoIs + * @param {number=} lowLimit - The lowest BACnet ID being queried. + * @param {number=} highLimit - The highest BACnet ID being queried. + * + * @example + * const bacnet = require('bacstack'); + * const client = new bacnet(); + * + * client.on('whoIs', (msg) => { + * console.log( + * 'address: ', msg.header.address, + * ' - lowLimit: ', msg.payload.lowLimit, + * ' - highLimit: ', msg.payload.highLimit + * ); + * }); + */ + 'use strict'; const baAsn1 = require('../asn1'); diff --git a/lib/transport.js b/lib/transport.js index 721574d9..ace8db25 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -3,12 +3,14 @@ const createSocket = require('dgram').createSocket; const EventEmitter = require('events').EventEmitter; +const DefaultBACnetPort = 47808; + class Transport extends EventEmitter { constructor(settings) { super(); this._settings = settings; this._server = createSocket({type: 'udp4', reuseAddr: true}); - this._server.on('message', (msg, rinfo) => this.emit('message', msg, rinfo.address)); + this._server.on('message', (msg, rinfo) => this.emit('message', msg, rinfo.address + (rinfo.port === DefaultBACnetPort ? '' : ':' + rinfo.port))); this._server.on('error', (err) => this.emit('message', err)); } @@ -21,7 +23,9 @@ class Transport extends EventEmitter { } send(buffer, offset, receiver) { - this._server.send(buffer, 0, offset, this._settings.port, receiver); + if (!receiver) receiver = this.getBroadcastAddress(); + const [address, port] = receiver.split(':'); + this._server.send(buffer, 0, offset, port || DefaultBACnetPort, address); } open() { diff --git a/test/unit/asn1.spec.js b/test/unit/asn1.spec.js index ed61be2e..f95d63c9 100644 --- a/test/unit/asn1.spec.js +++ b/test/unit/asn1.spec.js @@ -6,11 +6,6 @@ const baAsn1 = require('../../lib/asn1'); describe('bacstack - ASN1 layer', () => { describe('decodeUnsigned', () => { - it('should fail if unsuport length', () => { - const result = baAsn1.decodeUnsigned(Buffer.from([0xFF, 0xFF]), 0, 5); - expect(result).to.deep.equal({len: 5, value: NaN}); - }); - it('should successfully decode 8-bit unsigned integer', () => { const result = baAsn1.decodeUnsigned(Buffer.from([0x00, 0xFF, 0xFF, 0xFF, 0xFF]), 1, 1); expect(result).to.deep.equal({len: 1, value: 255}); diff --git a/test/unit/bvlc.spec.js b/test/unit/bvlc.spec.js index 572c303a..184ae522 100644 --- a/test/unit/bvlc.spec.js +++ b/test/unit/bvlc.spec.js @@ -12,21 +12,42 @@ describe('bacstack - BVLC layer', () => { expect(result).to.deep.equal({ len: 4, func: 10, - msgLength: 1482 + msgLength: 1482, + originatingIP: null, }); }); it('should successfuly encode and decode a forwarded package', () => { const buffer = utils.getBuffer(); - baBvlc.encode(buffer.buffer, 4, 1482); + baBvlc.encode(buffer.buffer, 4, 1482, '1.2.255.0'); const result = baBvlc.decode(buffer.buffer, 0); expect(result).to.deep.equal({ len: 10, func: 4, - msgLength: 1482 + msgLength: 1482, + originatingIP: '1.2.255.0', // omit port if default }); }); + it('should successfuly encode and decode a forwarded package on a different port', () => { + const buffer = utils.getBuffer(); + baBvlc.encode(buffer.buffer, 4, 1482, '1.2.255.0:47810'); + const result = baBvlc.decode(buffer.buffer, 0); + expect(result).to.deep.equal({ + len: 10, + func: 4, + msgLength: 1482, + originatingIP: '1.2.255.0:47810', // include port if non-default + }); + }); + + it('should fail forwarding a non FORWARDED_NPU', () => { + const buffer = utils.getBuffer(); + expect(() => { + baBvlc.encode(buffer.buffer, 3, 1482, '1.2.255.0'); + }).to.throw(Error); + }); + it('should fail if invalid BVLC type', () => { const buffer = utils.getBuffer(); baBvlc.encode(buffer.buffer, 10, 1482); @@ -52,7 +73,7 @@ describe('bacstack - BVLC layer', () => { it('should fail if unsuported function', () => { const buffer = utils.getBuffer(); - baBvlc.encode(buffer.buffer, 5, 1482); + baBvlc.encode(buffer.buffer, 99, 1482); const result = baBvlc.decode(buffer.buffer, 0); expect(result).to.equal(undefined); }); diff --git a/test/unit/client.spec.js b/test/unit/client.spec.js new file mode 100644 index 00000000..ea801761 --- /dev/null +++ b/test/unit/client.spec.js @@ -0,0 +1,38 @@ +'use strict'; + +const expect = require('chai').expect; +const utils = require('./utils'); +const baEnum = require('../../lib/enum'); +const client = require('../../lib/client'); + +describe('bacstack - client', () => { + it('should successfuly encode a bitstring > 32 bits', () => { + const result = client.createBitstring([ + baEnum.ServicesSupported.CONFIRMED_COV_NOTIFICATION, + baEnum.ServicesSupported.READ_PROPERTY, + baEnum.ServicesSupported.WHO_IS, + ]); + expect(result).to.deep.equal({ + value: [2, 16, 0, 0, 4], + bitsUsed: 35, + }); + }); + it('should successfuly encode a bitstring < 8 bits', () => { + const result = client.createBitstring([ + baEnum.ServicesSupported.GET_ALARM_SUMMARY, + ]); + expect(result).to.deep.equal({ + value: [8], + bitsUsed: 4, + }); + }); + it('should successfuly encode a bitstring of only one bit', () => { + const result = client.createBitstring([ + baEnum.ServicesSupported.ACKNOWLEDGE_ALARM, + ]); + expect(result).to.deep.equal({ + value: [1], + bitsUsed: 1, + }); + }); +}); diff --git a/test/unit/service-i-am.spec.js b/test/unit/service-i-am.spec.js index a0328fc5..5d822a60 100644 --- a/test/unit/service-i-am.spec.js +++ b/test/unit/service-i-am.spec.js @@ -4,11 +4,11 @@ const expect = require('chai').expect; const utils = require('./utils'); const baServices = require('../../lib/services'); -describe('bacstack - Services layer Iam unit', () => { +describe('bacstack - Services layer iAm unit', () => { it('should successfully encode and decode', () => { const buffer = utils.getBuffer(); - baServices.iAmBroadcast.encode(buffer, 47, 1, 1, 7); - const result = baServices.iAmBroadcast.decode(buffer.buffer, 0); + baServices.iAm.encode(buffer, 47, 1, 1, 7); + const result = baServices.iAm.decode(buffer.buffer, 0); delete result.len; expect(result).to.deep.equal({ deviceId: 47, diff --git a/test/unit/service-i-have-broadcast.spec.js b/test/unit/service-i-have.spec.js similarity index 62% rename from test/unit/service-i-have-broadcast.spec.js rename to test/unit/service-i-have.spec.js index 7cb0980c..f78a5fd4 100644 --- a/test/unit/service-i-have-broadcast.spec.js +++ b/test/unit/service-i-have.spec.js @@ -4,11 +4,11 @@ const expect = require('chai').expect; const utils = require('./utils'); const baServices = require('../../lib/services'); -describe('bacstack - Services layer IhaveBroadcast unit', () => { +describe('bacstack - Services layer iHave unit', () => { it('should successfully encode and decode', () => { const buffer = utils.getBuffer(); - baServices.iHaveBroadcast.encode(buffer, {type: 8, instance: 443}, {type: 0, instance: 4}, 'LgtCmd01'); - const result = baServices.iHaveBroadcast.decode(buffer.buffer, 0, buffer.offset); + baServices.iHave.encode(buffer, {type: 8, instance: 443}, {type: 0, instance: 4}, 'LgtCmd01'); + const result = baServices.iHave.decode(buffer.buffer, 0, buffer.offset); delete result.len; expect(result).to.deep.equal({ deviceId: {type: 8, instance: 443},