UNPKG

bacstack

Version:

A BACnet protocol stack written in pure JavaScript.

891 lines (850 loc) 79.5 kB
'use strict'; // Util Modules const EventEmitter = require('events').EventEmitter; const debug = require('debug')('bacstack'); // 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 DEFAULT_HOP_COUNT = 0xFF; const BVLC_HEADER_LENGTH = 4; /** * To be able to communicate to BACNET devices, you have to initialize a new bacstack instance. * @class bacstack * @param {object=} this._settings - The options object used for parameterizing the bacstack. * @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('bacstack'); * * 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 { constructor(options) { super(); options = options || {}; this._invokeCounter = 1; this._invokeStore = {}; this._lastSequenceNumber = 0; this._segmentStore = []; this._settings = { port: options.port || 47808, interface: options.interface, transport: options.transport, broadcastAddress: options.broadcastAddress || '255.255.255.255', apduTimeout: options.apduTimeout || 3000 }; this._transport = this._settings.transport || new baTransport({ port: this._settings.port, interface: this._settings.interface, broadcastAddress: this._settings.broadcastAddress }); // Setup code this._transport.on('message', this._receiveData.bind(this)); this._transport.on('error', this._receiveError.bind(this)); this._transport.open(); } // Helper utils _getInvokeId() { const id = this._invokeCounter++; if (id >= 256) this._invokeCounter = 1; return id - 1; } _invokeCallback(id, err, result) { const callback = this._invokeStore[id]; if (callback) return callback(err, result); debug('InvokeId ', id, ' not found -> drop package'); } _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); }; } _getBuffer() { return { buffer: Buffer.alloc(this._transport.getMaxPayload()), offset: BVLC_HEADER_LENGTH }; } // Service Handlers _processError(invokeId, buffer, offset, length) { const result = baServices.error.decode(buffer, offset, length); if (!result) return debug('Couldn`t decode Error'); this._invokeCallback(invokeId, new Error('BacnetError - Class:' + result.class + ' - Code:' + result.code)); } _processAbort(invokeId, reason) { this._invokeCallback(invokeId, new Error('BacnetAbort - Reason:' + reason)); } _segmentAckResponse(receiver, negative, server, originalInvokeId, sequencenumber, actualWindowSize) { const buffer = this._getBuffer(); 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); } _performDefaultSegmentHandling(sender, adr, type, service, invokeId, maxSegments, maxApdu, sequencenumber, first, moreFollows, buffer, offset, length) { if (first) { this._segmentStore = []; type &= ~baEnum.PduConReqBits.SEGMENTED_MESSAGE; let apduHeaderLen = 3; if ((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); } else { baApdu.encodeComplexAck(apdubuffer, type, service, 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 = []; type &= ~baEnum.PduConReqBits.SEGMENTED_MESSAGE; this._handlePdu(adr, type, apduBuffer, 0, apduBuffer.length); } } _processSegment(receiver, type, service, invokeId, maxSegments, maxApdu, server, sequencenumber, proposedWindowNumber, buffer, offset, length) { let first = false; if (sequencenumber === 0 && this._lastSequenceNumber === 0) { first = true; } else { if (sequencenumber !== this._lastSequenceNumber + 1) { return this._segmentAckResponse(receiver, true, server, invokeId, this._lastSequenceNumber, proposedWindowNumber); } } this._lastSequenceNumber = 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); } this._performDefaultSegmentHandling(this, receiver, type, service, invokeId, maxSegments, maxApdu, sequencenumber, first, moreFollows, buffer, offset, length); } _processConfirmedServiceRequest(address, type, service, maxSegments, maxApdu, invokeId, 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'); } } _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}); } else { debug('Received unsupported unconfirmed service request'); } } _handlePdu(address, type, buffer, offset, length) { let result; // Handle different PDU types switch (type & 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); 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}); 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}); } 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); } 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); break; case baEnum.PduTypes.ERROR: result = baApdu.decodeError(buffer, offset); this._processError(result.invokeId, buffer, offset + result.len, length - result.len); break; case baEnum.PduTypes.REJECT: case baEnum.PduTypes.ABORT: result = baApdu.decodeAbort(buffer, offset); this._processAbort(result.invokeId, result.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); } 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); } break; default: debug('Received unknown PDU type -> Drop package'); break; } } _handleNpdu(buffer, offset, msgLength, remoteAddress) { // Check data length if (msgLength <= 0) return debug('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.funct & baEnum.NpduControlBits.NETWORK_LAYER_MESSAGE) { return debug('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); } _receiveData(buffer, remoteAddress) { // Check data length if (buffer.length < baBvlc.BVLC_HEADER_LENGTH) return debug('Received invalid data -> Drop package'); // Parse BVLC header const result = baBvlc.decode(buffer, 0); if (!result) return debug('Received invalid BVLC header -> Drop package'); // 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'); } } _receiveError(err) { /** * @event bacstack.error * @param {error} err - The error object thrown by the underlying transport layer. * @example * const bacnet = require('bacstack'); * const client = new bacnet(); * * client.on('error', (err) => { * console.log('Error occurred: ', err); * client.close(); * }); */ this.emit('error', err); } /** * The whoIs command discovers all BACNET devices in a network. * @function bacstack.whoIs * @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. * @param {string=} options.address - Unicast address if command should address a device directly. * @fires bacstack.iAm * @example * const bacnet = require('bacstack'); * const client = new bacnet(); * * client.whoIs(); */ whoIs(options) { options = options || {}; const settings = { lowLimit: options.lowLimit, highLimit: options.highLimit, address: options.address || this._transport.getBroadcastAddress() }; 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); 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); } /** * The timeSync command sets the time of a target device. * @function bacstack.timeSync * @param {string} address - IP address of the target device. * @param {date} dateTime - The date and time to set on the target device. * @example * const bacnet = require('bacstack'); * const client = new bacnet(); * * client.timeSync('192.168.1.43', new Date()); */ timeSync(address, dateTime) { const buffer = this._getBuffer(); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, address); 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); } /** * The timeSyncUTC command sets the UTC time of a target device. * @function bacstack.timeSyncUTC * @param {string} address - IP address of the target device. * @param {date} dateTime - The date and time to set on the target device. * @example * const bacnet = require('bacstack'); * const client = new bacnet(); * * client.timeSyncUTC('192.168.1.43', new Date()); */ timeSyncUTC(address, dateTime) { const buffer = this._getBuffer(); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, address); 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); } /** * The readProperty command reads a single property of an object from a device. * @function bacstack.readProperty * @param {string} address - 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('bacstack'); * const client = new bacnet(); * * client.readProperty('192.168.1.43', {type: 8, instance: 44301}, 28, (err, value) => { * console.log('value: ', value); * }); */ readProperty(address, 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 || 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 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._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.readProperty.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) return next(new Error('INVALID_DECODING')); next(null, result); }); } /** * The writeProperty command writes a single property of an object to a device. * @function bacstack.writeProperty * @param {string} address - 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 {ApplicationTags} values.tag - 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. * @example * const bacnet = require('bacstack'); * const client = new bacnet(); * * client.writeProperty('192.168.1.43', {type: 8, instance: 44301}, 28, [ * {type: bacnet.enum.ApplicationTags.REAL, value: 100} * ], (err) => { * console.log('error: ', err); * }); */ writeProperty(address, 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 }; 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); 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._addCallback(settings.invokeId, (err) => next(err)); } /** * The readPropertyMultiple command reads multiple properties in multiple objects from a device. * @function bacstack.readPropertyMultiple * @param {string} address - 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('bacstack'); * const client = new bacnet(); * * const propertiesArray = [ * {objectId: {type: 8, instance: 4194303}, properties: [{id: 8}]} * ]; * client.readPropertyMultiple('192.168.1.43', propertiesArray, (err, value) => { * console.log('value: ', value); * }); */ readPropertyMultiple(address, 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 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._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.readPropertyMultiple.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) return next(new Error('INVALID_DECODING')); next(null, result); }); } /** * The writePropertyMultiple command writes multiple properties in multiple objects to a device. * @function bacstack.writePropertyMultiple * @param {string} address - 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 {ApplicationTags} values.values.value.tag - 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. * @example * const bacnet = require('bacstack'); * const client = new bacnet(); * * const values = [ * {objectId: {type: 8, instance: 44301}, values: [ * {property: {id: 28, index: 12}, value: [{type: bacnet.enum.ApplicationTags.BOOLEAN, value: true}], priority: 8} * ]} * ]; * client.writePropertyMultiple('192.168.1.43', values, (err) => { * console.log('error: ', err); * }); */ writePropertyMultiple(address, 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); 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._addCallback(settings.invokeId, (err) => next(err)); } /** * The deviceCommunicationControl command enables or disables network communication of the target device. * @function bacstack.deviceCommunicationControl * @param {string} address - 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. * @example * const bacnet = require('bacstack'); * const client = new bacnet(); * * client.deviceCommunicationControl('192.168.1.43', 0, bacnet.enum.EnableDisable.DISABLE, (err) => { * console.log('error: ', err); * }); */ deviceCommunicationControl(address, 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(); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); 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._addCallback(settings.invokeId, (err) => next(err)); } /** * The reinitializeDevice command initiates a restart of the target device. * @function bacstack.reinitializeDevice * @param {string} address - 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. * @example * const bacnet = require('bacstack'); * const client = new bacnet(); * * client.reinitializeDevice('192.168.1.43', bacnet.enum.ReinitializedState.COLDSTART, (err) => { * console.log('error: ', err); * }); */ reinitializeDevice(address, 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(); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBits.EXPECTING_REPLY, address); 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._addCallback(settings.invokeId, (err) => next(err)); } /** * The writeFile command writes a file buffer to a specific position of a file object. * @function bacstack.writeFile * @param {string} address - IP address of the target device. * @param {object} objectId - The BACNET object ID representing the file object. * @param {number} objectId.type - The BACNET object type representing the file object. * @param {number} objectId.instance - The BACNET object instance representing the file object. * @param {number} position - The position in the file to write at. * @param {Array.<number[]>} fileBuffer - The content to be written to the file. * @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(); * * client.writeFile('192.168.1.43', {type: 8, instance: 44301}, 0, [[5, 6, 7, 8], [5, 6, 7, 8]], (err, value) => { * console.log('value: ', value); * }); */ writeFile(address, 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); 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._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.atomicWriteFile.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) return next(new Error('INVALID_DECODING')); next(null, result); }); } /** * The readFile command reads a number of bytes at a specific position of a file object. * @function bacstack.readFile * @param {string} address - IP address of the target device. * @param {object} objectId - The BACNET object ID representing the file object. * @param {number} objectId.type - The BACNET object type representing the file object. * @param {number} objectId.instance - The BACNET object instance representing the file object. * @param {number} position - The position in the file to read at. * @param {number} count - The number of octets 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 {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(); * * client.readFile('192.168.1.43', {type: 8, instance: 44301}, 0, 100, (err, value) => { * console.log('value: ', value); * }); */ readFile(address, 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); 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._addCallback(settings.invokeId, (err, data) => { if (err) return next(err); const result = baServices.atomicReadFile.decodeAcknowledge(data.buffer, data.offset, data.length); if (!result) return next(new Error('INVALID_DECODING')); next(null, result); }); } /** * The readRang