UNPKG

@widesky/node-bacstack

Version:

The BACnet protocol library written in pure JavaScript.

1,165 lines (1,117 loc) 71.4 kB
'use strict'; // Util Modules const EventEmitter = require('events').EventEmitter; const debug = require('debug')('bacnet:client:debug'); const trace = require('debug')('bacnet:client:trace'); const usc = require('underscore'); // Local Modules const baTransport = require('./transport'); const baServices = require('./services'); const baAsn1 = require('./asn1'); const baApdu = require('./apdu'); const baNpdu = require('./npdu'); const baBvlc = require('./bvlc'); const baEnum = require('./enum'); const ALL_INTERFACES = '0.0.0.0'; const LOCALHOST_INTERFACES_IPV4 = '127.0.0.1'; const BROADCAST_ADDRESS = '255.255.255.255'; 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 bacnet instance. * @class bacnet * @param {object=} this._settings - The options object used for parameterizing the bacnet. * @param {number=} [options.port=47808] - BACNET communication port for listening and sending. * @param {string=} options.interface - Specific BACNET communication interface if different from primary one. * @param {string=} [options.broadcastAddress=255.255.255.255] - The address used for broadcast messages. * @param {number=} [options.apduTimeout=3000] - The timeout in milliseconds until a transaction should be interpreted as error. * @example * const bacnet = require('node-bacnet'); * * const client = new bacnet({ * port: 47809, // Use BAC1 as communication port * interface: '192.168.251.10', // Listen on a specific interface * broadcastAddress: '192.168.251.255', // Use the subnet broadcast address * apduTimeout: 6000 // Wait twice as long for response * }); */ class Client extends EventEmitter { /** * * @param options */ constructor(options) { super(); options = options || {}; this._invokeCounter = 1; this._invokeStore = {}; this._lastSequenceNumber = 0; this._segmentStore = []; this._settings = { port: options.port || 47808, interface: options.interface || ALL_INTERFACES, transport: options.transport, broadcastAddress: options.broadcastAddress || BROADCAST_ADDRESS, apduTimeout: options.apduTimeout || 3000 }; options.reuseAddr = options.reuseAddr === undefined ? true : !!options.reuseAddr; this._transport = this._settings.transport || new baTransport({ port: this._settings.port, interface: this._settings.interface, broadcastAddress: this._settings.broadcastAddress, reuseAddr: options.reuseAddr }); // Setup code this._transport.on('message', this._receiveData.bind(this)); this._transport.on('error', this._receiveError.bind(this)); this._transport.on('listening', () => this.emit('listening')); this._transport.open(); } /** * * @returns {number} * @private */ _getInvokeId() { const id = this._invokeCounter++; if (id >= 256) this._invokeCounter = 1; return id - 1; } /** * * @param id * @param err * @param result * @returns {*} * @private */ _invokeCallback(id, err, result) { const callback = this._invokeStore[id]; if (callback) { return void callback(err, result); } debug('InvokeId', id, 'not found -> drop package'); } /** * * @param id * @param callback * @private */ _addCallback(id, callback) { const timeout = setTimeout(() => { delete this._invokeStore[id]; callback(new Error('ERR_TIMEOUT')); }, this._settings.apduTimeout); this._invokeStore[id] = (err, data) => { clearTimeout(timeout); delete this._invokeStore[id]; callback(err, data); }; } /** * * @param isForwarded * @returns {{offset: (number), buffer: *}} * @private */ _getBuffer(isForwarded) { return Object.assign({}, { buffer: Buffer.alloc(this._transport.getMaxPayload()), offset: isForwarded ? BVLC_FWD_HEADER_LENGTH : BVLC_HEADER_LENGTH }); } /** * * @param invokeId * @param buffer * @param offset * @param length * @returns {*} * @private */ _processError(invokeId, buffer, offset, length) { const result = baServices.error.decode(buffer, offset, length); trace('Received error:', result); if (!result) { return debug('Couldn`t decode Error'); } const err = new Error(baServices.error.buildMessage(result)); err.bacnetErrorClass = result.class; err.bacnetErrorCode = result.code; this._invokeCallback(invokeId, err); } /** * * @param invokeId * @param reason * @private */ _processAbort(invokeId, reason) { const err = new Error('BacnetAbort - Reason:' + reason); err.bacnetAbortReason = reason; this._invokeCallback(invokeId, err); } /** * * @param receiver * @param negative * @param server * @param originalInvokeId * @param sequencenumber * @param actualWindowSize * @private */ _segmentAckResponse(receiver, negative, server, originalInvokeId, sequencenumber, actualWindowSize) { 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.PduType.SEGMENT_ACK | (negative ? baEnum.PduSegAckBit.NEGATIVE_ACK : 0) | (server ? baEnum.PduSegAckBit.SERVER : 0), originalInvokeId, sequencenumber, actualWindowSize); this.sendBvlc(receiver, buffer); } /** * * @param msg * @param first * @param moreFollows * @param buffer * @param offset * @param length * @private */ _performDefaultSegmentHandling(msg, first, moreFollows, buffer, offset, length) { if (first) { this._segmentStore = []; msg.type &= ~baEnum.PduConReqBit.SEGMENTED_MESSAGE; let apduHeaderLen = 3; if ((msg.type & baEnum.PDU_TYPE_MASK) === baEnum.PduType.CONFIRMED_REQUEST) { apduHeaderLen = 4; } const apdubuffer = this._getBuffer(); apdubuffer.offset = 0; buffer.copy(apdubuffer.buffer, apduHeaderLen, offset, offset + length); if ((msg.type & baEnum.PDU_TYPE_MASK) === baEnum.PduType.CONFIRMED_REQUEST) { baApdu.encodeConfirmedServiceRequest( apdubuffer, msg.type, msg.service, msg.maxSegments, msg.maxApdu, msg.invokeId, 0, 0 ); } else { baApdu.encodeComplexAck(apdubuffer, msg.type, msg.service, msg.invokeId, 0, 0); } this._segmentStore.push(apdubuffer.buffer.slice(0, length + apduHeaderLen)); } else { this._segmentStore.push(buffer.slice(offset, offset + length)); } if (!moreFollows) { const apduBuffer = Buffer.concat(this._segmentStore); this._segmentStore = []; msg.type &= ~baEnum.PduConReqBit.SEGMENTED_MESSAGE; this._handlePdu(apduBuffer, 0, apduBuffer.length, msg.header); } } /** * * @param msg * @param server * @param buffer * @param offset * @param length * @private */ _processSegment(msg, server, buffer, offset, length) { let first = false; if (msg.sequencenumber === 0 && this._lastSequenceNumber === 0) { first = true; } else { if (msg.sequencenumber !== this._lastSequenceNumber + 1) { return this._segmentAckResponse(msg.header.address, true, server, msg.invokeId, this._lastSequenceNumber, msg.proposedWindowNumber); } } this._lastSequenceNumber = msg.sequencenumber; const moreFollows = msg.type & baEnum.PduConReqBit.MORE_FOLLOWS; if (!moreFollows) { this._lastSequenceNumber = 0; } if ((msg.sequencenumber % msg.proposedWindowNumber) === 0 || !moreFollows) { this._segmentAckResponse(msg.header.address, false, server, msg.invokeId, msg.sequencenumber, msg.proposedWindowNumber); } this._performDefaultSegmentHandling(msg, first, moreFollows, buffer, offset, length); } /** * * @param serviceMap * @param content * @param buffer * @param offset * @param length * @returns {*} * @private */ _processServiceRequest(serviceMap, content, buffer, offset, length) { let result; const sender = content.header.sender; if (sender.address === LOCALHOST_INTERFACES_IPV4) { debug('Received and skipped localhost service request:', content.service); return; } const name = serviceMap[content.service]; if (!name) { debug('Received unsupported service request:', content.service); return; } const id = content.invokeId ? '@' + content.invokeId : ''; trace(`Received service request${id}:`, name); // Find a function to decode the packet. const serviceHandler = baServices[name]; if (serviceHandler) { try { content.payload = serviceHandler.decode(buffer, offset, length); trace(`Handled service request${id}:`, name, JSON.stringify(content)); } 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('No serviceHandler defined for:', name); // Call the callback anyway, just with no payload. } //trace('Passing payload over to callback:', content); // Call the user code, if they've defined a callback. if (this.listenerCount(name)) { trace('listener count by name emits ' + name + ' with content'); this.emit(name, content); } else { if (this.listenerCount('unhandledEvent')) { trace('unhandled event emiting with content'); this.emit('unhandledEvent', content); } else { // 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. trace('no unhandled event handler with header: ' + JSON.stringify(content.header)); 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 ); } } } } /** * * @param buffer * @param offset * @param length * @param header * @private */ _handlePdu(buffer, offset, length, header) { let msg; trace('handlePdu Header: ', header); // Handle different PDU types switch (header.apduType & baEnum.PDU_TYPE_MASK) { case baEnum.PduType.UNCONFIRMED_REQUEST: 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.PduType.SIMPLE_ACK: 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.PduType.COMPLEX_ACK: msg = baApdu.decodeComplexAck(buffer, offset); msg.header = header; if ((header.apduType & baEnum.PduConReqBit.SEGMENTED_MESSAGE) === 0) { this._invokeCallback(msg.invokeId, null, {msg: msg, buffer: buffer, offset: offset + msg.len, length: length - msg.len}); } else { this._processSegment(msg, true, buffer, offset + msg.len, length - msg.len); } break; case baEnum.PduType.SEGMENT_ACK: msg = baApdu.decodeSegmentAck(buffer, offset); msg.header = header; this._processSegment(msg, true, buffer, offset + msg.len, length - msg.len); break; case baEnum.PduType.ERROR: msg = baApdu.decodeError(buffer, offset); this._processError(msg.invokeId, buffer, offset + msg.len, length - msg.len); break; case baEnum.PduType.REJECT: case baEnum.PduType.ABORT: msg = baApdu.decodeAbort(buffer, offset); this._processAbort(msg.invokeId, msg.reason); break; case baEnum.PduType.CONFIRMED_REQUEST: msg = baApdu.decodeConfirmedServiceRequest(buffer, offset); msg.header = header; msg.header.confirmedService = true; if ((header.apduType & baEnum.PduConReqBit.SEGMENTED_MESSAGE) === 0) { this._processServiceRequest(confirmedServiceMap, msg, buffer, offset + msg.len, length - msg.len); } else { this._processSegment(msg, true, buffer, offset + msg.len, length - msg.len); } break; default: debug(`Received unknown PDU type ${header.apduType} -> Drop packet`); break; } } /** * * @param buffer * @param offset * @param msgLength * @param header * @returns {*} * @private */ _handleNpdu(buffer, offset, msgLength, header) { // Check data length if (msgLength <= 0) { return trace('No NPDU data -> Drop package'); } // Parse baNpdu header const result = baNpdu.decode(buffer, offset); if (!result) { return trace('Received invalid NPDU header -> Drop package'); } if (result.source) { // Assign the decoded network details of source device if (header.sender) { Object.assign(header.sender, result.source); } } if (result.funct & baEnum.NpduControlBit.NETWORK_LAYER_MESSAGE) { return trace('Received network layer message -> Drop package'); } offset += result.len; msgLength -= result.len; if (msgLength <= 0) { return trace('No APDU data -> Drop package'); } header.apduType = baApdu.getDecodedType(buffer, offset); header.expectingReply = !!(result.funct & baEnum.NpduControlBit.EXPECTING_REPLY); this._handlePdu(buffer, offset, msgLength, header); } /** * * @param buffer * @param remoteAddress * @returns {*} * @private */ _receiveData(buffer, remoteAddress) { // Check data length if (buffer.length < baEnum.BVLC_HEADER_LENGTH) { return trace('Received invalid data -> Drop package'); } // Parse BVLC header const result = baBvlc.decode(buffer, 0); if (!result) { return 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 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 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; } } /** * @event bacnet.error * @param {error} err - The error object thrown by the underlying transport layer. * @example * const bacnet = require('node-bacnet'); * const client = new bacnet(); * * client.on('error', (err) => { * console.log('Error occurred: ', err); * client.close(); * }); */ _receiveError(err) { this.emit('error', err); } /** * The whoIs command discovers all BACNET devices in a network. * @function bacnet.whoIs * @param {string} receiver - IP address of the target device. * @param {object=} options * @param {number=} options.lowLimit - Minimal device instance number to search for. * @param {number=} options.highLimit - Maximal device instance number to search for. * @fires bacnet.iAm * @example * const bacnet = require('node-bacnet'); * const client = new bacnet(); * * client.whoIs(); */ whoIs(receiver, options) { if (!options) { if ( receiver && typeof receiver === 'object' && receiver.address === undefined && receiver.forwardedFrom === undefined && (receiver.lowLimit !== undefined || receiver.highLimit !== undefined) ) { // receiver seems to be an options object options = receiver; receiver = undefined; } } options = options || {}; const settings = { lowLimit: options.lowLimit, highLimit: options.highLimit, }; 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.PduType.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.WHO_IS); baServices.whoIs.encode(buffer, settings.lowLimit, settings.highLimit); this.sendBvlc(receiver, buffer); } /** * The timeSync command sets the time of a target device. * @function bacnet.timeSync * @param {string} receiver - IP address of the target device. * @param {date} dateTime - The date and time to set on the target device. * @example * const bacnet = require('node-bacnet'); * const client = new bacnet(); * * client.timeSync('192.168.1.43', new Date()); */ timeSync(receiver, dateTime) { const buffer = this._getBuffer(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduType.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.TIME_SYNCHRONIZATION); baServices.timeSync.encode(buffer, dateTime); this.sendBvlc(receiver, buffer); } /** * The timeSyncUTC command sets the UTC time of a target device. * @function bacnet.timeSyncUTC * @param {string} receiver - IP address of the target device. * @param {date} dateTime - The date and time to set on the target device. * @example * const bacnet = require('node-bacnet'); * const client = new bacnet(); * * client.timeSyncUTC('192.168.1.43', new Date()); */ timeSyncUTC(receiver, dateTime) { const buffer = this._getBuffer(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver); baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduType.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.UTC_TIME_SYNCHRONIZATION); baServices.timeSync.encode(buffer, dateTime); this.sendBvlc(receiver, buffer); } /** * The readProperty command reads a single property of an object from a device. * @function bacnet.readProperty * @param {string} receiver - IP address of the target device. * @param {object} objectId - The BACNET object ID to read. * @param {number} objectId.type - The BACNET object type to read. * @param {number} objectId.instance - The BACNET object instance to read. * @param {number} propertyId - The BACNET property id in the specified object to read. * @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 {number=} options.arrayIndex - The array index of the property to be read. * @param {function} next - The callback containing an error, in case of a failure and value object in case of success. * @example * const bacnet = require('node-bacnet'); * const client = new bacnet(); * * client.readProperty('192.168.1.43', {type: 8, instance: 44301}, 28, (err, value) => { * console.log('value: ', value); * }); */ readProperty(receiver, objectId, propertyId, 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(), arrayIndex: (options.arrayIndex || options.arrayIndex === 0) ? options.arrayIndex : baEnum.ASN1_ARRAY_ALL }; const buffer = this._getBuffer(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); const type = baEnum.PduType.CONFIRMED_REQUEST | (settings.maxSegments !== baEnum.MaxSegmentsAccepted.SEGMENTS_0 ? baEnum.PduConReqBit.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); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) { return void next(err); } const result = baServices.readProperty.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) { return void next(new Error('INVALID_DECODING')); } next(null, result); }); } /** * The writeProperty command writes a single property of an object to a device. * @function bacnet.writeProperty * @param {string} receiver - IP address of the target device. * @param {object} objectId - The BACNET object ID to write. * @param {number} objectId.type - The BACNET object type to write. * @param {number} objectId.instance - The BACNET object instance to write. * @param {number} propertyId - The BACNET property id in the specified object to write. * @param {object[]} values - A list of values to be written to the specified property. * @param {ApplicationTag} values.type - The data-type of the value to be written. * @param {number} values.value - The actual value to be written. * @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 {number=} options.arrayIndex - The array index of the property to be read. * @param {number=} options.priority - The priority of the value to be written. * @param {function} next - The callback containing an error, in case of a failure and value object in case of success. * @example * const bacnet = require('node-bacnet'); * const client = new bacnet(); * * client.writeProperty('192.168.1.43', {type: 8, instance: 44301}, 28, [ * {type: bacnet.enum.ApplicationTag.REAL, value: 100} * ], (err, value) => { * console.log('value: ', value); * }); */ writeProperty(receiver, objectId, propertyId, 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(), arrayIndex: options.arrayIndex || baEnum.ASN1_ARRAY_ALL, priority: options.priority || baEnum.ASN1_NO_PRIORITY }; const buffer = this._getBuffer(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.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); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { next(err); }); } /** * The readPropertyMultiple command reads multiple properties in multiple objects from a device. * @function bacnet.readPropertyMultiple * @param {string} receiver - IP address of the target device. * @param {object[]} propertiesArray - List of object and property specifications to be read. * @param {object} propertiesArray.objectId - Specifies which object to read. * @param {number} propertiesArray.objectId.type - The BACNET object type to read. * @param {number} propertiesArray.objectId.instance - The BACNET object instance to read. * @param {object[]} propertiesArray.properties - List of properties to be read. * @param {number} propertiesArray.properties.id - The BACNET property id in the specified object to read. Also supports 8 for all properties. * @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('node-bacnet'); * const client = new bacnet(); * * const requestArray = [ * {objectId: {type: 8, instance: 4194303}, properties: [{id: 8}]} * ]; * client.readPropertyMultiple('192.168.1.43', requestArray, (err, value) => { * console.log('value: ', value); * }); */ 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(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); const type = baEnum.PduType.CONFIRMED_REQUEST | (settings.maxSegments !== baEnum.MaxSegmentsAccepted.SEGMENTS_0 ? baEnum.PduConReqBit.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); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) { return void next(err); } const result = baServices.readPropertyMultiple.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) { return void next(new Error('INVALID_DECODING')); } next(null, result); }); } /** * The writePropertyMultiple command writes multiple properties in multiple objects to a device. * @function bacnet.writePropertyMultiple * @param {string} receiver - IP address of the target device. * @param {object[]} values - List of object and property specifications to be written. * @param {object} values.objectId - Specifies which object to read. * @param {number} values.objectId.type - The BACNET object type to read. * @param {number} values.objectId.instance - The BACNET object instance to read. * @param {object[]} values.values - List of properties to be written. * @param {object} values.values.property - Property specifications to be written. * @param {number} values.values.property.id - The BACNET property id in the specified object to write. * @param {number} values.values.property.index - The array index of the property to be written. * @param {object[]} values.values.value - A list of values to be written to the specified property. * @param {ApplicationTag} values.values.value.type - The data-type of the value to be written. * @param {object} values.values.value.value - The actual value to be written. * @param {number} values.values.priority - The priority to be used for writing to the property. * @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('node-bacnet'); * const client = new bacnet(); * * const values = [ * {objectId: {type: 8, instance: 44301}, values: [ * {property: {id: 28, index: 12}, value: [{type: bacnet.enum.ApplicationTag.BOOLEAN, value: true}], priority: 8} * ]} * ]; * client.writePropertyMultiple('192.168.1.43', values, (err, value) => { * console.log('value: ', value); * }); */ 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(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.WRITE_PROPERTY_MULTIPLE, settings.maxSegments, settings.maxApdu, settings.invokeId); baServices.writePropertyMultiple.encodeObject(buffer, values); 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 bacnet.confirmedCOVNotification * @param {string} receiver - 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('node-bacnet'); * 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.ApplicationTag.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.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.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 void next(err); } next(); }); } /** * The deviceCommunicationControl command enables or disables network communication of the target device. * @function bacnet.deviceCommunicationControl * @param {string} receiver - IP address of the target device. * @param {number} timeDuration - The time to hold the network communication state in seconds. 0 for infinite. * @param {EnableDisable} enableDisable - The network communication state to set. * @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 {string=} options.password - The optional password used to set the network communication state. * @param {function} next - The callback containing an error, in case of a failure and value object in case of success. * @example * const bacnet = require('node-bacnet'); * const client = new bacnet(); * * client.deviceCommunicationControl('192.168.1.43', 0, bacnet.enum.EnableDisable.DISABLE, (err) => { * console.log('error: ', err); * }); */ deviceCommunicationControl(receiver, timeDuration, enableDisable, 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(), password: options.password }; const buffer = this._getBuffer(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.DEVICE_COMMUNICATION_CONTROL, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.deviceCommunicationControl.encode(buffer, timeDuration, enableDisable, settings.password); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { next(err); }); } /** * The reinitializeDevice command initiates a restart of the target device. * @function bacnet.reinitializeDevice * @param {string} receiver - IP address of the target device. * @param {ReinitializedState} state - The type of restart to be initiated. * @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 {string=} options.password - The optional password used to restart the device. * @param {function} next - The callback containing an error, in case of a failure and value object in case of success. * @example * const bacnet = require('node-bacnet'); * const client = new bacnet(); * * client.reinitializeDevice('192.168.1.43', bacnet.enum.ReinitializedState.COLDSTART, (err, value) => { * console.log('value: ', value); * }); */ reinitializeDevice(receiver, state, 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(), password: options.password }; const buffer = this._getBuffer(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.REINITIALIZE_DEVICE, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.reinitializeDevice.encode(buffer, state, settings.password); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { next(err); }); } /** * * @param receiver * @param objectId * @param position * @param fileBuffer * @param options * @param 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(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.ATOMIC_WRITE_FILE, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.atomicWriteFile.encode(buffer, false, objectId, position, fileBuffer); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) { return void next(err); } const result = baServices.atomicWriteFile.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) { return void next(new Error('INVALID_DECODING')); } next(null, result); }); } /** * * @param receiver * @param objectId * @param position * @param count * @param options * @param 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(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.ATOMIC_READ_FILE, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.atomicReadFile.encode(buffer, true, objectId, position, count); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) { return void next(err); } const result = baServices.atomicReadFile.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) { return void next(new Error('INVALID_DECODING')); } next(null, result); }); } /** * * @param receiver * @param objectId * @param idxBegin * @param quantity * @param options * @param 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(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.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); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) { return void next(err); } const result = baServices.readRange.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) { return void next(new Error('INVALID_DECODING')); } next(null, result); }); } /** * * @param receiver * @param objectId * @param subscribeId * @param cancel * @param issueConfirmedNotifications * @param lifetime * @param options * @param 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(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.SUBSCRIBE_COV, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0); baServices.subscribeCov.encode(buffer, subscribeId, objectId, cancel, issueConfirmedNotifications, lifetime); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) { return void next(err); } next(); }); } /** * * @param receiver * @param objectId * @param monitoredProperty * @param subscribeId * @param cancel * @param issueConfirmedNotifications * @param options * @param 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(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver); baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.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); this.sendBvlc(receiver, buffer); this._addCallback(settings.invokeId, (err, data) => { if (err) { return void next(err); } next(); }); } /** * The unconfirmedCOVNotification command sends an unconfirmed COV notification to a device * @function bacnet.unconfirmedCOVNotification * @param {string} receiver - IP address of the target device. * @param {number} subscriberProcessId - The process id which was used by a target device in the subscription. * @param {number} initiatingDeviceId - The id of this device. * @param {object} monitoredObjectId - Specifies about which object the notification is. * @param {number} monitoredObjectId.type - The BACNET object type of the notification. * @param {number} monitoredObjectId.instance - The BACNET object instance of the notification. * @param {number} timeRemaining - How long the subscription is still active in seconds. * @param {object[]} values - List of properties with updated values. * @param {object} values.property - Property specifications. * @param {number} values.property.id - The updated BACNET property id. * @param {number} values.property.index - The array index of the updated property. * @param {object[]} values.value - A list of updated values. * @param {ApplicationTag} values.value.type - The data-type of the updated value. * @param {object} values.value.value - The actual updated value. * @param {number} va